First step towards implementing code folding

This commit is contained in:
Vicente Bergas 2024-03-16 20:06:56 +01:00
parent 5510317942
commit dbf524615c
8 changed files with 361 additions and 33 deletions

View file

@ -1283,6 +1283,26 @@ func (h *BufPane) MoveLinesDown() bool {
return true
}
// FoldCollapse
func (h *BufPane) FoldCollapse() bool {
return h.Buf.FoldCollapse(h.Cursor.Loc.Y, false, false)
}
// FoldCollapseRecursive
func (h *BufPane) FoldCollapseRecursive() bool {
return h.Buf.FoldCollapse(h.Cursor.Loc.Y, false, true)
}
// FoldExpand
func (h *BufPane) FoldExpand() bool {
return h.Buf.FoldCollapse(h.Cursor.Loc.Y, true, false)
}
// FoldExpandRecursive
func (h *BufPane) FoldExpandRecursive() bool {
return h.Buf.FoldCollapse(h.Cursor.Loc.Y, true, true)
}
// Paste whatever is in the system clipboard into the buffer
// Delete and paste if the user has a selection
func (h *BufPane) Paste() bool {

View file

@ -757,6 +757,10 @@ var BufKeyActions = map[string]BufKeyAction{
"DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp,
"MoveLinesDown": (*BufPane).MoveLinesDown,
"FoldCollapse": (*BufPane).FoldCollapse,
"FoldCollapseRecursive": (*BufPane).FoldCollapseRecursive,
"FoldExpand": (*BufPane).FoldExpand,
"FoldExpandRecursive": (*BufPane).FoldExpandRecursive,
"IndentSelection": (*BufPane).IndentSelection,
"OutdentSelection": (*BufPane).OutdentSelection,
"Autocomplete": (*BufPane).Autocomplete,
@ -877,6 +881,10 @@ var MultiActions = map[string]bool{
"DeleteLine": true,
"MoveLinesUp": true,
"MoveLinesDown": true,
"FoldCollapse": true,
"FoldCollapseRecursive": true,
"FoldExpand": true,
"FoldExpandRecursive": true,
"IndentSelection": true,
"OutdentSelection": true,
"OutdentLine": true,

View file

@ -157,11 +157,121 @@ func (b *SharedBuffer) MarkModified(start, end int) {
b.Highlighter.HighlightMatches(b, start, l)
}
b.DepthAssign(start, end + 1)
for i := start; i <= end; i++ {
b.LineArray.invalidateSearchMatches(i)
}
}
// TODO: A parser for the applicable language should parse the buffer and assign the AST depth of each line.
// If different tokens from the same line have different depths, assign the minimum of them all.
// Of course, it is also acceptable to use other algorithms that produce acceptable results, maybe
// something like counting openning and closing brackets for the appropriate filetype.
func (b *SharedBuffer) DepthAssign(start, end int) {
// TODO: When a parser for the applicable language would be overkill, add an entry to the runtime/syntax/*.yaml file to select one of the predefined algorithms.
ft := b.FileType()
offSide := ft == "cmake" || ft == "coffeescript" || ft == "elm" || ft == "gdscript" || ft == "haml" || ft == "makefile" || ft == "nim" || ft == "python" || ft == "python2" || ft == "yaml"
c := ft == "c" || ft == "c++"
modified := false
str := highlight.Groups["constant.string"];
cmt := highlight.Groups["comment"];
tod := highlight.Groups["todo"];
err := highlight.Groups["error"];
preproc := highlight.Groups["preproc"];
prevgrp := b.GetGroup(Loc{0, start}.left(b.LineArray))
prev_cmt_str := prevgrp == str || prevgrp == cmt || prevgrp == tod || prevgrp == err
for y := start; y < end; y++ {
min_d := 0
if offSide {
// depth set by indentation
l := b.LineBytes(y)
t := len(l)
w := len(util.GetLeadingWhitespace(l))
if t > w {
min_d = w
} else if y > 0 {
min_d = b.lines[y-1].min_depth
}
} else {
// depth set by braces
d := 0
if y > 0 {
d = b.lines[y-1].depth
}
min_d = d
l := []rune(string(b.LineBytes(y)))
for x := 0; x < len(l); x++ {
grp := b.GetGroup(Loc{x, y})
cmt_str := grp == str || grp == cmt || grp == tod || grp == err
if prev_cmt_str != cmt_str {
prev_cmt_str = cmt_str
if cmt_str {
d++
} else {
d--
if min_d > d {
min_d = d
}
}
}
if cmt_str {
continue
}
if c && grp == preproc && l[x] == '#' {
i := x + 1
for ; l[i] == ' ' || l[i] == '\t' ; i++ {}
if l[i] == 'i' && l[i+1] == 'f' { // if
d++
} else if l[i] == 'e' && (l[i+1] == 'l' || l[i+1] == 'n') { // else, elif, endif
d--
if min_d > d {
min_d = d
}
if l[i+1] == 'l' { // else, elif
d++
}
}
}
if l[x] == '{' || l[x] == '(' || l[x] == '[' {
d++
} else if (l[x] == '}' || l[x] == ')' || l[x] == ']') && d > 0 {
d--
if min_d > d {
min_d = d
}
}
}
if b.lines[y].depth != d {
b.lines[y].depth = d
modified = true
if y + 1 == end && end < b.LinesNum() {
end++
}
}
}
if b.lines[y].min_depth != min_d {
b.lines[y].min_depth = min_d
modified = true
if y + 1 == end && end < b.LinesNum() {
end++
}
}
}
if modified {
if start > 0 {
start--
}
for lineN := start; lineN < end; lineN++ {
if b.lines[lineN].collapsed && !b.FoldCollapsible(lineN) {
b.lines[lineN].collapsed = false
}
}
}
}
// DisableReload disables future reloads of this sharedbuffer
func (b *SharedBuffer) DisableReload() {
b.ReloadDisabled = true
@ -511,10 +621,99 @@ func (b *Buffer) Remove(start, end Loc) {
}
// FileType returns the buffer's filetype
func (b *Buffer) FileType() string {
func (b *SharedBuffer) FileType() string {
return b.Settings["filetype"].(string)
}
// lineN is collapsible if min_depth(lineN) < min_depth(lineN+1)
func (b *SharedBuffer) FoldCollapsible(lineN int) bool {
return lineN + 1 < b.LinesNum() && b.lines[lineN + 1].min_depth > b.lines[lineN].min_depth
}
// lineN is the end of a collapsible region if min_depth(lineN-1) > min_depth(lineN)
func (b *SharedBuffer) FoldCollapseEnd(lineN int) bool {
return lineN > 0 && b.lines[lineN - 1].min_depth > b.lines[lineN].min_depth
}
// lineN is hidden if any collapsible parent is collapsed
func (b *Buffer) Hidden(lineN int) bool {
d := b.lines[lineN].min_depth
for lineT := lineN - 1; lineT >= 0 && d > 0; lineT-- {
if d > b.lines[lineT].min_depth {
d = b.lines[lineT].min_depth
if b.lines[lineT].collapsed {
return true
}
}
}
return false
}
// lineN must be collapsible
func (b *Buffer) FoldCollapseOne(lineN int, recursive bool) {
b.lines[lineN].collapsed = true
if !recursive {
return
}
d := b.lines[lineN].min_depth
for lineT := lineN + 1; lineT < b.LinesNum() && b.lines[lineT].min_depth > d; lineT++ {
if b.FoldCollapsible(lineT) {
b.lines[lineT].collapsed = true
}
}
}
// lineN should be collapsible
func (b *Buffer) FoldExpandOne(lineN int, recursive bool) {
b.lines[lineN].collapsed = false
if !recursive {
return
}
d := b.lines[lineN].min_depth
for lineT := lineN + 1; lineT < b.LinesNum() && b.lines[lineT].min_depth > d; lineT++ {
b.lines[lineT].collapsed = false
}
}
// Expand all parent folds of lineN to make it visible.
func (b *Buffer) FoldExpandOut(lineN int) {
d := b.lines[lineN].min_depth
for lineT := lineN - 1; lineT >= 0 && d > 0; lineT-- {
if d > b.lines[lineT].min_depth {
d = b.lines[lineT].min_depth
b.lines[lineT].collapsed = false
}
}
}
// FoldCollapse
func (b *Buffer) FoldCollapse(lineN int, expand bool, recursive bool) bool {
// If this is not a collapsible line, collapse the region it pertains.
if !b.FoldCollapsible(lineN) {
for d := b.lines[lineN].min_depth; lineN >= 0 && b.lines[lineN].min_depth >= d; lineN-- {}
}
if lineN >= 0 {
if expand {
b.FoldExpandOne(lineN, recursive)
} else {
b.FoldCollapseOne(lineN, recursive)
}
return true
}
// Collapse everything
lineN = 0
for d := b.lines[lineN].min_depth; lineN < b.LinesNum() - 1; lineN++ {
if b.lines[lineN].min_depth == d && b.lines[lineN + 1].min_depth > d {
if expand {
b.FoldExpandOne(lineN, recursive)
} else {
b.FoldCollapseOne(lineN, recursive)
}
}
}
return true
}
// ExternallyModified returns whether the file being edited has
// been modified by some external process
func (b *Buffer) ExternallyModified() bool {
@ -971,6 +1170,7 @@ func (b *Buffer) UpdateRules() {
go func() {
b.Highlighter.HighlightStates(b)
b.Highlighter.HighlightMatches(b, 0, b.End().Y)
b.DepthAssign(0, b.LinesNum())
screen.Redraw()
}()
}
@ -1157,55 +1357,69 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar = curLine[start.X-1]
}
this_open := startChar == braceType[0];
left_open := leftChar == braceType[0];
this_close := startChar == braceType[1];
left_close := leftChar == braceType[1];
startLoc := start
if !this_open && (left_open || left_close) {
startLoc.X--
}
startGroup := b.GetGroup(startLoc)
var i int
if startChar == braceType[0] || (leftChar == braceType[0] && startChar != braceType[1]) {
if this_open || (left_open && !this_close) {
for y := start.Y; y < b.LinesNum(); y++ {
l := []rune(string(b.LineBytes(y)))
l := []rune(string(b.lines[y].data))
xInit := 0
if y == start.Y {
if startChar == braceType[0] {
if this_open {
xInit = start.X
} else {
xInit = start.X - 1
}
}
for x := xInit; x < len(l); x++ {
curGroup := b.GetGroup(Loc{x, y})
if curGroup != startGroup {
continue
}
r := l[x]
if r == braceType[0] {
i++
} else if r == braceType[1] {
i--
if i == 0 {
if startChar == braceType[0] {
return Loc{x, y}, false, true
}
return Loc{x, y}, true, true
return Loc{x, y}, !this_open, true
}
}
}
}
} else if startChar == braceType[1] || leftChar == braceType[1] {
} else if this_close || left_close {
for y := start.Y; y >= 0; y-- {
l := []rune(string(b.lines[y].data))
xInit := len(l) - 1
if y == start.Y {
if startChar == braceType[1] {
if this_close {
xInit = start.X
} else {
xInit = start.X - 1
}
}
for x := xInit; x >= 0; x-- {
curGroup := b.GetGroup(Loc{x, y})
if curGroup != startGroup {
continue
}
r := l[x]
if r == braceType[1] {
i++
} else if r == braceType[0] {
i--
if i == 0 {
if startChar == braceType[1] {
return Loc{x, y}, false, true
}
return Loc{x, y}, true, true
return Loc{x, y}, !this_close, true
}
}
}

View file

@ -69,6 +69,7 @@ func (c *Cursor) Goto(b Cursor) {
// the current cursor its selection too
func (c *Cursor) GotoLoc(l Loc) {
c.X, c.Y = l.X, l.Y
c.buf.FoldExpandOut(l.Y)
c.StoreVisualX()
}
@ -238,22 +239,35 @@ func (c *Cursor) AddLineToSelection() {
// UpN moves the cursor up N lines (if possible)
func (c *Cursor) UpN(amount int) {
proposedY := c.Y - amount
incr := -1
if amount < 0 {
incr = 1
}
proposedY := c.Y
for amount != 0 {
proposedY += incr
if proposedY < 0 {
proposedY = 0
} else if proposedY >= len(c.buf.lines) {
break
}
if proposedY >= len(c.buf.lines) {
proposedY = len(c.buf.lines) - 1
break
}
if !c.buf.Hidden(proposedY) {
amount += incr
}
}
bytes := c.buf.LineBytes(proposedY)
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
if c.X > util.CharacterCount(bytes) || (amount < 0 && proposedY == c.Y) {
if c.X > util.CharacterCount(bytes) || (incr > 0 && proposedY == c.Y) {
c.X = util.CharacterCount(bytes)
c.StoreVisualX()
}
if c.X < 0 || (amount > 0 && proposedY == c.Y) {
if c.X < 0 || (incr < 0 && proposedY == c.Y) {
c.X = 0
c.StoreVisualX()
}

View file

@ -49,6 +49,9 @@ type Line struct {
state highlight.State
match highlight.LineMatch
lock sync.Mutex
collapsed bool // TODO: This should a per-buffer attribute.
min_depth int
depth int
// The search states for the line, used for highlighting of search matches,
// separately from the syntax highlighting.
@ -363,6 +366,33 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match
}
func (la *LineArray) GetGroup(pos Loc) highlight.Group {
lMatch := la.Match(pos.Y)
closest := -1
for key := range lMatch {
if key > closest && key <= pos.X && lMatch[key] != 0 {
closest = key
}
}
if closest >= 0 {
return lMatch[closest]
}
for pos.Y > 0 {
pos.Y--
lMatch = la.Match(pos.Y)
closest = -1
for key := range lMatch {
if key > closest {
closest = key
}
}
if closest >= 0 {
return lMatch[closest]
}
}
return 0
}
// Locks the whole LineArray
func (la *LineArray) Lock() {
la.lock.Lock()
@ -373,6 +403,10 @@ func (la *LineArray) Unlock() {
la.lock.Unlock()
}
func (la *LineArray) Collapsed(lineN int) bool {
return la.lines[lineN].collapsed
}
// SearchMatch returns true if the location `pos` is within a match
// of the last search for the buffer `b`.
// It is used for efficient highlighting of search matches (separately

View file

@ -138,7 +138,7 @@ func (w *BufWindow) updateDisplayInfo() {
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
w.maxLineNumLength = len(strconv.Itoa(b.LinesNum()))
w.maxLineNumLength = 1 + len(strconv.Itoa(b.LinesNum()))
w.gutterOffset = 0
if w.hasMessage {
@ -217,6 +217,7 @@ func (w *BufWindow) Relocate() bool {
activeC := w.Buf.GetActiveCursor()
scrollmargin := int(b.Settings["scrollmargin"].(float64))
b.FoldExpandOut(activeC.Loc.Y)
c := w.SLocFromLoc(activeC.Loc)
bStart := SLoc{0, 0}
bEnd := w.SLocFromLoc(b.End())
@ -330,8 +331,21 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
}
lineNum := []rune(strconv.Itoa(util.Abs(lineInt)))
// Indicate if this is a collapsed region
fold := ' '
if w.Buf.FoldCollapsible(bloc.Y) {
if w.Buf.Collapsed(bloc.Y) {
fold = '+'
} else {
fold = '-'
}
} else if w.Buf.FoldCollapseEnd(bloc.Y) {
fold = '^'
}
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, fold, nil, lineNumStyle)
vloc.X++
// Write the spaces before the line number if necessary
for i := 0; i < w.maxLineNumLength-len(lineNum) && vloc.X < w.gutterOffset; i++ {
for i := 0; i < w.maxLineNumLength-1-len(lineNum) && vloc.X < w.gutterOffset; i++ {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
@ -466,6 +480,13 @@ func (w *BufWindow) displayBuffer() {
for ; vloc.Y < w.bufHeight; vloc.Y++ {
vloc.X = 0
for bloc.Y < b.LinesNum() && b.Hidden(bloc.Y) {
bloc.Y++
}
if bloc.Y >= b.LinesNum() {
break
}
currentLine := false
for _, c := range cursors {
if bloc.Y == c.Y && w.active {

View file

@ -290,17 +290,31 @@ func (w *BufWindow) diff(s1, s2 SLoc) int {
// which means scrolling up. The returned location is guaranteed to be
// within the buffer boundaries.
func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
if !w.Buf.Settings["softwrap"].(bool) {
s.Line += n
if w.Buf.Settings["softwrap"].(bool) {
// TODO: account for hidden lines when softwrap
return w.scroll(s, n)
}
incr := 1
if n < 0 {
incr = -1
n = -n;
}
for i := 0; i < n; {
s.Line += incr
if s.Line < 0 || s.Line >= w.Buf.LinesNum() {
break
}
if !w.Buf.Hidden(s.Line) {
i++
}
}
if s.Line < 0 {
s.Line = 0
}
if s.Line > w.Buf.LinesNum()-1 {
if s.Line >= w.Buf.LinesNum() {
s.Line = w.Buf.LinesNum() - 1
}
return s
}
return w.scroll(s, n)
}
// Diff returns the difference (the vertical distance) between two SLocs.

View file

@ -53,6 +53,9 @@ var statusInfo = map[string]func(*buffer.Buffer) string{
"percentage": func(b *buffer.Buffer) string {
return strconv.Itoa((b.GetActiveCursor().Y + 1) * 100 / b.LinesNum())
},
"group": func(b *buffer.Buffer) string {
return b.GetGroup(b.GetActiveCursor().Loc).String()
},
}
func SetStatusInfoFnLua(fn string) {