package editor import "strings" // Inline markdown formatting: wrap (or unwrap) the selection in markers, keeping // the markup visible in the buffer (TASK-009). With no selection a marker pair is // inserted with the cursor between. Symmetric markers (bold/italic/code) toggle // off when the selection is already wrapped — either with the markers inside the // selection or sitting immediately outside it. // WrapBold wraps the selection in ** (toggles off if already bold). func (e *Editor) WrapBold() { e.toggleWrap("**") } // WrapItalic wraps the selection in _ (toggles off if already italic). func (e *Editor) WrapItalic() { e.toggleWrap("_") } // WrapCode wraps the selection in ` (toggles off if already inline code). func (e *Editor) WrapCode() { e.toggleWrap("`") } func (e *Editor) toggleWrap(marker string) { if !e.HasSelection() { e.insertPair(marker, marker, len([]rune(marker))) return } start, end, _ := e.selRange() n := len([]rune(marker)) inner := e.SelectedText() // Toggle off: markers inside the selection. if len([]rune(inner)) >= 2*n && strings.HasPrefix(inner, marker) && strings.HasSuffix(inner, marker) { stripped := string([]rune(inner)[n : len([]rune(inner))-n]) e.replaceRange(start, end, stripped) newEnd := Position{Row: start.Row, Col: start.Col + len([]rune(stripped))} e.setSelection(start, newEnd) return } // Toggle off: markers immediately outside the selection (single line). if start.Row == end.Row && e.markersOutside(start, end, marker) { line := []rune(e.Lines[start.Row]) newLine := string(line[:start.Col-n]) + string(line[start.Col:end.Col]) + string(line[end.Col+n:]) e.Lines[start.Row] = newLine e.Dirty = true ns := Position{Row: start.Row, Col: start.Col - n} ne := Position{Row: end.Row, Col: end.Col - n} e.setSelection(ns, ne) return } // Wrap: insert marker after the selection, then before it. e.insertAt(end, marker) e.insertAt(start, marker) ns := Position{Row: start.Row, Col: start.Col + n} var ne Position if start.Row == end.Row { ne = Position{Row: end.Row, Col: end.Col + n} } else { ne = end } e.setSelection(ns, ne) } // WrapLink turns the selection into [sel](), cursor inside the parens; with no // selection inserts [](), cursor inside the brackets. Toggles a full // [text](url) selection back to its label text. func (e *Editor) WrapLink() { if !e.HasSelection() { e.insertPair("[", "]()", 1) // cursor between [ and ] return } start, end, _ := e.selRange() inner := e.SelectedText() // Toggle off: selection is a complete [label](url) — unwrap to the label. if start.Row == end.Row && strings.HasPrefix(inner, "[") && strings.HasSuffix(inner, ")") { if i := strings.Index(inner, "]("); i >= 0 { label := inner[1:i] e.replaceRange(start, end, label) e.setSelection(start, Position{Row: start.Row, Col: start.Col + len([]rune(label))}) return } } e.insertAt(end, "]()") e.insertAt(start, "[") // Park the cursor inside the parens, just before the closing ). if start.Row == end.Row { e.Cursor = Position{Row: end.Row, Col: end.Col + 3} // +"[" +"](" } else { e.Cursor = Position{Row: end.Row, Col: end.Col + 2} } e.anchor = nil e.setGoal() e.followCursor() } // wrapPair maps an opening punctuation rune to its closing rune for // surround-on-type: with a selection, typing one of these wraps the selection // instead of replacing it (repeated presses nest, e.g. * then * → **). func wrapPair(r rune) (rune, bool) { switch r { case '*', '_', '`', '"', '\'', '~': return r, true case '(': return ')', true case '[': return ']', true case '{': return '}', true case '<': return '>', true } return 0, false } // surroundSelection wraps the active selection in left/right, leaving the // selection on the inner text so repeated wraps nest. func (e *Editor) surroundSelection(left, right string) { start, end, ok := e.selRange() if !ok { return } e.insertAt(end, right) e.insertAt(start, left) nL := len([]rune(left)) ns := Position{Row: start.Row, Col: start.Col + nL} ne := end if start.Row == end.Row { ne = Position{Row: end.Row, Col: end.Col + nL} } e.setSelection(ns, ne) } // insertPair inserts left+right at the cursor and parks the cursor `offset` // runes past the original position (i.e. between the two halves). func (e *Editor) insertPair(left, right string, offset int) { pos := e.Cursor line := []rune(e.Lines[pos.Row]) newLine := string(line[:pos.Col]) + left + right + string(line[pos.Col:]) e.Lines[pos.Row] = newLine e.Dirty = true e.Cursor = Position{Row: pos.Row, Col: pos.Col + offset} e.anchor = nil e.setGoal() e.followCursor() } // insertAt inserts s at position p (single-line column splice). func (e *Editor) insertAt(p Position, s string) { line := []rune(e.Lines[p.Row]) e.Lines[p.Row] = string(line[:p.Col]) + s + string(line[p.Col:]) e.Dirty = true } // markersOutside reports whether marker sits immediately before start and after // end on the same line. func (e *Editor) markersOutside(start, end Position, marker string) bool { n := len([]rune(marker)) line := []rune(e.Lines[start.Row]) if start.Col < n || end.Col+n > len(line) { return false } return string(line[start.Col-n:start.Col]) == marker && string(line[end.Col:end.Col+n]) == marker } // replaceRange replaces the text in [start,end) (same row) with s. func (e *Editor) replaceRange(start, end Position, s string) { line := []rune(e.Lines[start.Row]) e.Lines[start.Row] = string(line[:start.Col]) + s + string(line[end.Col:]) e.Dirty = true } // setSelection anchors at a and parks the cursor at b. func (e *Editor) setSelection(a, b Position) { c := a e.anchor = &c e.Cursor = b e.setGoal() e.followCursor() }