package editor import ( "strings" "github.com/charmbracelet/lipgloss" ) // selectionForRow returns the row-local rune range [a,b) of vr that is selected. func (e *Editor) selectionForRow(vr vrow) (a, b int, ok bool) { start, end, has := e.selRange() if !has || vr.logRow < start.Row || vr.logRow > end.Row { return 0, 0, false } rowStart, rowEnd := vr.start, vr.start+vr.runes lo, hi := rowStart, rowEnd if vr.logRow == start.Row && start.Col > lo { lo = start.Col } if vr.logRow == end.Row && end.Col < hi { hi = end.Col } if lo >= hi { return 0, 0, false } return lo - rowStart, hi - rowStart, true } // overlaySelection restyles the runes in [a,b) of the row's spans with sel, // keeping the rest as-is. The concatenated text is unchanged. func overlaySelection(spans []Span, a, b int, sel lipgloss.Style) []Span { total := 0 for _, sp := range spans { total += len([]rune(sp.Text)) } out := sliceSpans(spans, 0, a) mid := sliceSpans(spans, a, b) for i := range mid { mid[i].Style = sel mid[i].Wavy = false // selection highlight supersedes the misspell undercurl } out = append(out, mid...) return append(out, sliceSpans(spans, b, total)...) } // before reports whether position a comes before b in the document. func (a Position) before(b Position) bool { if a.Row != b.Row { return a.Row < b.Row } return a.Col < b.Col } // HasSelection reports whether a non-empty selection is active. func (e *Editor) HasSelection() bool { return e.anchor != nil && *e.anchor != e.Cursor } // ClearSelection drops any active selection. func (e *Editor) ClearSelection() { e.anchor = nil } // MouseAnchor moves the cursor to a visual click position and drops any active // selection — the press that begins a click or a drag (TASK-027). func (e *Editor) MouseAnchor(vi, col int) { e.MoveToVisual(vi, col) e.ClearSelection() } // MouseExtendTo extends a mouse selection to the drag point, anchoring at the // press position on the first motion and holding that anchor across subsequent // motions (TASK-027). Soft-wrap aware via MoveToVisual. func (e *Editor) MouseExtendTo(vi, col int) { e.startSelection() e.MoveToVisual(vi, col) } // startSelection anchors a selection at the current cursor if none is active. func (e *Editor) startSelection() { if e.anchor == nil { c := e.Cursor e.anchor = &c } } // selRange returns the ordered selection bounds and whether a selection exists. func (e *Editor) selRange() (start, end Position, ok bool) { if !e.HasSelection() { return Position{}, Position{}, false } a, b := *e.anchor, e.Cursor if b.before(a) { a, b = b, a } return a, b, true } // SelectedText returns the selected text (with newlines for multi-line spans), // or "" when there is no selection. func (e *Editor) SelectedText() string { start, end, ok := e.selRange() if !ok { return "" } if start.Row == end.Row { r := []rune(e.Lines[start.Row]) return string(r[start.Col:end.Col]) } var b strings.Builder first := []rune(e.Lines[start.Row]) b.WriteString(string(first[start.Col:])) for row := start.Row + 1; row < end.Row; row++ { b.WriteByte('\n') b.WriteString(e.Lines[row]) } b.WriteByte('\n') last := []rune(e.Lines[end.Row]) b.WriteString(string(last[:end.Col])) return b.String() } // DeleteSelection removes the selected text, places the cursor at its start, and // clears the selection. Returns true if anything was deleted. func (e *Editor) DeleteSelection() bool { start, end, ok := e.selRange() if !ok { return false } first := []rune(e.Lines[start.Row]) last := []rune(e.Lines[end.Row]) merged := string(first[:start.Col]) + string(last[end.Col:]) e.Lines[start.Row] = merged if end.Row > start.Row { e.Lines = append(e.Lines[:start.Row+1], e.Lines[end.Row+1:]...) } e.Cursor = start e.anchor = nil e.Dirty = true e.invalidate() e.setGoal() e.followCursor() return true } // InsertText inserts s at the cursor (replacing any selection), handling // newlines — used by paste. func (e *Editor) InsertText(s string) { e.invalidate() if e.HasSelection() { e.DeleteSelection() } for _, r := range s { if r == '\n' { e.InsertNewline() } else { e.InsertRune(r) } } }