Johan Rong 1 kuukausi sitten
vanhempi
sitoutus
06e94dcfab
3 muutettua tiedostoa jossa 167 lisäystä ja 40 poistoa
  1. 100 26
      internal/buffer/buffer.go
  2. 51 4
      internal/tui/model.go
  3. 16 10
      internal/tui/update.go

+ 100 - 26
internal/buffer/buffer.go

@@ -2,6 +2,7 @@ package buffer
 
 import (
 	"unicode/utf8"
+	"slices"
 
 	"github.com/zyedidia/rope"
 )
@@ -19,50 +20,68 @@ func (buf *Buffer) ensureRope() {
 
 func (buf *Buffer) Insert(content rune) {
 	buf.ensureRope()
+	buf.CM.DeduplicateAndSort()
+
+	delta := 0
 	data := []byte(string(content))
 	shift := len(data)
 
 	for i := range buf.CM.Cursors {
 		cur := &buf.CM.Cursors[i]
-		if cur.Offset < 0 {
-			cur.Offset = 0
+
+		pos := cur.Offset + delta
+
+		if pos < 0 {
+			pos = 0
 		}
-		if cur.Offset > buf.Rope.Len() {
-			cur.Offset = buf.Rope.Len()
+		if pos > buf.Rope.Len() {
+			pos = buf.Rope.Len()
 		}
 
-		buf.Rope.Insert(cur.Offset, data)
-		cur.Offset += shift
-	}
+		buf.Rope.Insert(pos, data)
+		cur.Offset = pos + shift
 
-	buf.CM.DeduplicateAndSort()
+		delta += shift
+	}
 }
 
 func (buf *Buffer) Delete() {
 	buf.ensureRope()
 
+	buf.CM.DeduplicateAndSort()
+
+	delta := 0
+
 	for i := range buf.CM.Cursors {
 		cur := &buf.CM.Cursors[i]
-		if cur.Offset <= 0 {
+
+		pos := cur.Offset + delta
+
+		if pos <= 0 {
+			cur.Offset = 0
 			continue
 		}
 
-		if cur.Offset > buf.Rope.Len() {
-			cur.Offset = buf.Rope.Len()
+		if pos > buf.Rope.Len() {
+			pos = buf.Rope.Len()
 		}
 
-		left := buf.Rope.Slice(0, cur.Offset)
+		left := buf.Rope.Slice(0, pos)
 		_, size := utf8.DecodeLastRune(left)
 		if size <= 0 {
 			size = 1
 		}
 
-		start := cur.Offset - size
+		start := pos - size
 		if start < 0 {
 			start = 0
 		}
 
-		buf.Rope.Remove(start, cur.Offset)
+		buf.Rope.Remove(start, pos)
+
+		deleted := pos - start
+		delta -= deleted
+
 		cur.Offset = start
 	}
 
@@ -82,6 +101,8 @@ func (buf *Buffer) MoveHoriz(dir int) {
 		_, goal := LineCol(buf.Rope, cur.Offset)
 		cur.Goal = goal
 	}
+
+	buf.CM.DeduplicateAndSort()
 }
 
 func (buf *Buffer) MoveVert(dir int) {
@@ -91,27 +112,80 @@ func (buf *Buffer) MoveVert(dir int) {
 		cur := &buf.CM.Cursors[i]
 		line, _ := LineCol(buf.Rope, cur.Offset)
 
-		if line + dir < 0 || line + dir > LineCount(buf.Rope) {
+		targetLine := line + dir
+		if targetLine < 0 || targetLine >= LineCount(buf.Rope) {
 			continue
 		}
 
-		line += dir
-		cur.Offset = OffsetForLine(buf.Rope, line) + cur.Goal
+		lineStart := OffsetForLine(buf.Rope, targetLine)
+		lineEnd := OffsetForLine(buf.Rope, targetLine+1)
+		lineLen := lineEnd - lineStart
+		if lineLen < 0 {
+			lineLen = 0
+		}
+
+		goal := cur.Goal
+		if goal > lineLen {
+			goal = lineLen
+		}
+
+		cur.Offset = lineStart + goal
 	}
+
+	buf.CM.DeduplicateAndSort()
 }
 
-func LineCount(r *rope.Node) int {
-    if r.Len() == 0 {
-        return 0
-    }
+func (buf *Buffer) AddCursorVert(dir int) {
+	buf.ensureRope()
 
-    lines := r.Count(0, r.Len(), []byte{'\n'})
+	var newCursors []Cursor 
 
-    if r.At(r.Len()-1) != '\n' {
-        lines++
-    }
+	for i := range buf.CM.Cursors {
+		cur := &buf.CM.Cursors[i]
+
+		line, _ := LineCol(buf.Rope, cur.Offset)
 
-    return lines
+		targetLine := line + dir
+		if targetLine < 0 || targetLine >= LineCount(buf.Rope) {
+			continue
+		}
+
+		lineStart := OffsetForLine(buf.Rope, targetLine)
+		lineEnd := OffsetForLine(buf.Rope, targetLine+1)
+		lineLen := lineEnd - lineStart
+		if lineLen < 0 {
+			lineLen = 0
+		}
+
+		goal := cur.Goal
+		if goal > lineLen {
+			goal = lineLen
+		}
+
+		newCursors = append(newCursors, Cursor{
+			Offset: lineStart + goal,
+			Goal: goal,
+		})
+	}
+
+	buf.CM.Cursors = slices.Concat(buf.CM.Cursors, newCursors)
+
+	buf.CM.DeduplicateAndSort()
+}
+
+func (buf *Buffer) ClearCursors() {
+	primaryCursor := &Cursor{
+		Offset: buf.CM.Cursors[buf.CM.PrimaryIdx].Offset,
+		Goal: buf.CM.Cursors[buf.CM.PrimaryIdx].Goal,
+	}
+
+	buf.CM.Cursors = buf.CM.Cursors[:0]
+	buf.CM.Cursors = append(buf.CM.Cursors, *primaryCursor)
+	buf.CM.PrimaryIdx = 0
+}
+
+func LineCount(r *rope.Node) int {
+	return r.Count(0, r.Len(), []byte{'\n'}) + 1
 }
 
 func LineCol(r *rope.Node, offset int) (line, col int) {

+ 51 - 4
internal/tui/model.go

@@ -9,7 +9,7 @@ import (
 )
 
 type Keymap = struct {
-	left, right, up, down, tab, backtab, newline, delete, quit key.Binding
+	left, right, up, down, cursorUp, cursorDown, clearCursors, tab, backtab, newline, delete, quit key.Binding
 }
 
 type EditorModel struct {
@@ -24,15 +24,14 @@ func NewEditorModel() EditorModel {
 	initialBuf := buffer.Buffer{
 		Rope: nil,
 		CM: buffer.CursorManager{
-			Cursors: []buffer.Cursor{{Offset: 0}},
+			Cursors: []buffer.Cursor{{Offset: 0, Goal: 0}},
 			PrimaryIdx: 0,
 		},
 	}
 
 	vp := viewport.New()
-	vp.SetContent(initialBuf.String())
 
-	return EditorModel{
+	model := EditorModel{
 		Buffer:   initialBuf,
 		Viewport: vp,
 		Keymap: Keymap{
@@ -48,6 +47,15 @@ func NewEditorModel() EditorModel {
 			down: key.NewBinding(
 				key.WithKeys("down"),
 			),
+			cursorUp: key.NewBinding(
+				key.WithKeys("shift+up"),
+			),
+			cursorDown: key.NewBinding(
+				key.WithKeys("shift+down"),
+			),
+			clearCursors: key.NewBinding(
+				key.WithKeys("esc"),
+			),
 			tab: key.NewBinding(
 				key.WithKeys("tab"),
 			),
@@ -65,6 +73,9 @@ func NewEditorModel() EditorModel {
 			),
 		},
 	}
+
+	model.Viewport.SetContent(model.renderedContent())
+	return model
 }
 
 func (m EditorModel) Init() tea.Cmd {
@@ -76,3 +87,39 @@ func (m EditorModel) View() tea.View {
 	v.AltScreen = true
 	return v
 }
+
+func (m EditorModel) renderedContent() string {
+    content := []rune(m.Buffer.String())
+
+    cursorMap := make(map[int]bool)
+    for _, cur := range m.Buffer.CM.Cursors {
+        offset := cur.Offset
+        if offset < 0 {
+            offset = 0
+        }
+        if offset > len(content) {
+            offset = len(content)
+        }
+        cursorMap[offset] = true
+    }
+
+	out := make([]rune, 0, len(content)+len(cursorMap))
+
+    for i, r := range content {
+        if cursorMap[i] {
+            out = append(out, '█')
+            
+            if r == '\n' {
+                out = append(out, r)
+            }
+        } else {
+            out = append(out, r)
+        }
+    }
+
+    if cursorMap[len(content)] {
+        out = append(out, '█')
+    }
+
+    return string(out)
+}

+ 16 - 10
internal/tui/update.go

@@ -23,14 +23,20 @@ func (m EditorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		switch {
 		case key.Matches(msg, m.Keymap.quit):
 			return m, tea.Quit
-        case key.Matches(msg, m.Keymap.left):
-            m.Buffer.MoveHoriz(-1)
-        case key.Matches(msg, m.Keymap.right):
-            m.Buffer.MoveHoriz(1)
-        case key.Matches(msg, m.Keymap.down):
-            m.Buffer.MoveVert(1)
-        case key.Matches(msg, m.Keymap.up):
-            m.Buffer.MoveVert(-1)
+		case key.Matches(msg, m.Keymap.left):
+			m.Buffer.MoveHoriz(-1)
+		case key.Matches(msg, m.Keymap.right):
+			m.Buffer.MoveHoriz(1)
+		case key.Matches(msg, m.Keymap.down):
+			m.Buffer.MoveVert(1)
+		case key.Matches(msg, m.Keymap.cursorUp):
+			m.Buffer.AddCursorVert(-1)
+		case key.Matches(msg, m.Keymap.cursorDown):
+			m.Buffer.AddCursorVert(1)
+		case key.Matches(msg, m.Keymap.clearCursors):
+			m.Buffer.ClearCursors()
+		case key.Matches(msg, m.Keymap.up):
+			m.Buffer.MoveVert(-1)
 		case key.Matches(msg, m.Keymap.newline):
 			m.Buffer.Insert('\n')
 		case key.Matches(msg, m.Keymap.delete):
@@ -44,8 +50,8 @@ func (m EditorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 
-		m.Viewport.SetContent(m.Buffer.String())
-        
+		m.Viewport.SetContent(m.renderedContent())
+		return m, nil
 	}
 
 	return m, nil