Documentation
¶
Overview ¶
Package editor provides the text-buffer primitives, the syntax highlighter, and the Tab type that combines them with view state and rendering. The buffer is intentionally simple: one Go string per line. That's plenty fast for the review-and-light-edit workloads this editor is aimed at, and keeps the rendering and tokenisation code uncluttered.
Index ¶
- Constants
- Variables
- func ClusterAt(runes []rune, i, visualCol int) (end, width int)
- func ClusterStart(runes []rune, i int) int
- func DetectIndent(lines []string, path string) string
- func FirstMatchAtOrAfter(matches []Match, cursor Position) int
- func Highlight(filename, src string, t theme.Theme) [][]tcell.Style
- func HighlightVisible(filename string, lines []string, startLine, height int, t theme.Theme) [][]tcell.Style
- func HighlightWindow(filename string, lines []string, startLine, height int, t theme.Theme) ([][]tcell.Style, int, int)
- func IsWordChar(r rune) bool
- func LineCommentPrefix(path string) (string, bool)
- func LineVisualCol(runes []rune, runeCol int) int
- func NextCluster(runes []rune, i int) int
- func PosLess(a, b Position) bool
- func PosOrdered(a, b Position) (Position, Position)
- func PrevCluster(runes []rune, i int) int
- func RuneColAtVisual(runes []rune, targetVisualCol int) int
- func RuneVisualWidth(r rune, visualCol int) int
- func WordLeft(runes []rune, col int) int
- func WordRangeAt(runes []rune, col int) (start, end int, ok bool)
- func WordRight(runes []rune, col int) int
- func WrapRowOfCol(segs []int, col int) int
- func WrapSegments(runes []rune, width int) []int
- type BracketMatch
- type Buffer
- func (b *Buffer) Clamp(p Position) Position
- func (b *Buffer) DeleteRange(a, c Position) Position
- func (b *Buffer) EndPos() Position
- func (b *Buffer) InsertString(p Position, text string) Position
- func (b *Buffer) LineCount() int
- func (b *Buffer) LineRunes(i int) []rune
- func (b *Buffer) String() string
- func (b *Buffer) Substring(a, c Position) string
- func (b *Buffer) TextWith(sep string) string
- type GitLineChange
- type LineEnding
- type Match
- type Position
- type Tab
- func (t *Tab) Backspace()
- func (t *Tab) CanRedo() bool
- func (t *Tab) CanRevert() bool
- func (t *Tab) CanUndo() bool
- func (t *Tab) CenterOnCursor(viewH int)
- func (t *Tab) ClampCursorToView(viewH int)
- func (t *Tab) ClearFind()
- func (t *Tab) Delete()
- func (t *Tab) DeleteSelection()
- func (t *Tab) DisplayName() string
- func (t *Tab) DuplicateLines()
- func (t *Tab) EnsureVisible(viewW, viewH int)
- func (t *Tab) FindNext()
- func (t *Tab) FindPrev()
- func (t *Tab) FocusCurrentMatch()
- func (t *Tab) GoToMatchingBracket() bool
- func (t *Tab) HasMatchingBracket() bool
- func (t *Tab) HasSelection() bool
- func (t *Tab) HitTest(localX, localY, w, h int) (Position, bool)
- func (t *Tab) InsertNewline()
- func (t *Tab) InsertRune(r rune)
- func (t *Tab) InsertString(s string)
- func (t *Tab) IsImage() bool
- func (t *Tab) IsPreview() bool
- func (t *Tab) JumpToLine(n int)
- func (t *Tab) MoveCursor(dLine, dCol int, extend bool)
- func (t *Tab) MoveCursorTo(p Position, extend bool)
- func (t *Tab) MoveLineEnd(extend bool)
- func (t *Tab) MoveLineHome(extend bool)
- func (t *Tab) MoveLinesDown()
- func (t *Tab) MoveLinesUp()
- func (t *Tab) MoveWordLeft(extend bool)
- func (t *Tab) MoveWordRight(extend bool)
- func (t *Tab) Pin()
- func (t *Tab) Redo() bool
- func (t *Tab) Reload() error
- func (t *Tab) ReloadKeepHistory() error
- func (t *Tab) Render(scr tcell.Screen, th theme.Theme, x, y, w, h int)
- func (t *Tab) ReplaceAllMatches(repl string) int
- func (t *Tab) ReplaceCurrentMatch(repl string) bool
- func (t *Tab) ReplaceLines(newLines map[int]string) int
- func (t *Tab) RestoreView(cursor Position, scrollY int)
- func (t *Tab) RevertFile() bool
- func (t *Tab) Save() error
- func (t *Tab) Scroll(deltaLines int)
- func (t *Tab) ScrollH(deltaCols int)
- func (t *Tab) ScrollTargetForClick(viewH, clickY int) int
- func (t *Tab) ScrollbarVisible(viewH int) bool
- func (t *Tab) SelectWordAt(p Position)
- func (t *Tab) SelectionText() string
- func (t *Tab) SetFindQuery(query string)
- func (t *Tab) SetWrap(on bool)
- func (t *Tab) ToggleLineComment() (changed bool, ok bool)
- func (t *Tab) Undo() bool
- type TabList
- func (l *TabList) Activate(t *Tab) bool
- func (l *TabList) ActivateAt(i int)
- func (l *TabList) Active() *Tab
- func (l *TabList) ActiveIndex() int
- func (l *TabList) Append(t *Tab)
- func (l *TabList) At(i int) *Tab
- func (l *TabList) IndexOf(t *Tab) int
- func (l *TabList) InsertPreview(t *Tab)
- func (l *TabList) Len() int
- func (l *TabList) Lookup(path string) *Tab
- func (l *TabList) Preview() *Tab
- func (l *TabList) Remove(t *Tab) bool
- func (l *TabList) Tabs() []*Tab
Constants ¶
const TabStop = 4
TabStop is the visual cell width of a hard tab. Four matches what most modern editors and viewers default to (cat, less, GitHub) and matches the editor's typical insert width when the user presses Tab.
Variables ¶
var ErrBinaryFile = errors.New("looks like a binary file")
ErrBinaryFile marks a refusal to open non-text content into a text buffer. Callers surface it as a flash; image formats never hit it — they take the image-tab path first.
var ErrFileTooLarge = errors.New("file is too large to open")
ErrFileTooLarge marks a refusal to open a file above maxOpenBytes. Callers surface it as a flash exactly the way they surface ErrBinaryFile; the wrapped message names the file's size and the cap so the user can tell "seen and declined" from "missing".
var ErrImageTooLarge = errors.New("image dimensions too large to preview")
ErrImageTooLarge marks a refusal to decode an image whose declared dimensions exceed maxImagePixels — a guard against decode bombs, not a judgment about the file. Callers flash it like ErrFileTooLarge.
var ErrNotUTF8 = errors.New("not valid UTF-8 — convert the file to UTF-8 to edit it")
ErrNotUTF8 marks a refusal to open content that is not valid UTF-8. The buffer edits text through rune slices, and Go maps invalid bytes to U+FFFD on decode — so editing any line holding such bytes would silently rewrite them on save. Refusing at the door is the same tradeoff as ErrBinaryFile: skiff edits UTF-8 text, and says so instead of corrupting quietly.
Functions ¶
func ClusterAt ¶ added in v0.2.1
ClusterAt measures the grapheme cluster that starts at rune index i: it returns the index one past the cluster's last rune and the number of terminal cells the cluster occupies when it starts at visualCol. i must be a valid index into runes and should be a cluster boundary — pass ClusterStart's answer if that is not already guaranteed.
end always advances (end > i), so a walk over a line can never stall.
func ClusterStart ¶ added in v0.2.1
ClusterStart snaps a rune index back to the first rune of the grapheme cluster containing it, so no caller can leave the caret sitting between a base character and its combining mark. An index already on a boundary (including len(runes), the end-of-line caret position) is returned unchanged.
The scan starts from the nearest index a boundary is guaranteed at rather than from column 0, which makes the ASCII case O(1) and the worst case no worse than the O(line) visual-column walks the renderer already runs on every frame.
func DetectIndent ¶
DetectIndent picks the indent unit a freshly-opened buffer should use when the user presses Tab. The algorithm in priority order:
- Walk every line; classify lines that start with whitespace as either tab-indented or space-indented. Count both.
- If tab-indented lines outnumber space-indented ones, return "\t".
- If space-indented wins, return that many spaces (using the smallest non-zero leading-space count as the indent width — that matches what "infer" tools in other editors do).
- With no signal, fall back to the path's extension: ".go" / Makefiles / ".tsv" default to tabs (those file types either require or strongly prefer tabs); everything else defaults to four spaces.
The result is what the *Tab* key inserts; existing characters in the file are not rewritten.
func FirstMatchAtOrAfter ¶
FirstMatchAtOrAfter returns the index into matches of the first hit at or after cursor, or 0 when cursor sits past the last match (we wrap around to the top — that's what the user expects after typing a query at the bottom of a file).
Returns -1 when matches is empty so callers can short-circuit without re-checking the length.
func Highlight ¶
Highlight tokenises src using a Chroma lexer chosen by filename (falling back to content-based detection, then to a plain-text lexer) and returns a per-line slice of styles parallel to the buffer's lines: styles[i][j] is the style for rune j on line i.
Returning a per-rune style grid keeps the renderer simple — it just looks up the style for each cell it draws — at the cost of some memory. For files small enough to comfortably review, that's a fine trade.
func HighlightVisible ¶
func HighlightVisible(filename string, lines []string, startLine, height int, t theme.Theme) [][]tcell.Style
HighlightVisible returns a style grid for the current viewport. Only visible rows are kept in the output so keystroke cost follows terminal height, not file size. To stay correct inside multi-line comments and strings that span the viewport, tokenisation starts a bounded lead above the top and ends a bounded lead below the bottom; those lead rows are styled then thrown away.
func HighlightWindow ¶ added in v0.1.1
func HighlightWindow(filename string, lines []string, startLine, height int, t theme.Theme) ([][]tcell.Style, int, int)
HighlightWindow tokenises a window around the viewport — the visible rows plus the same lead HighlightVisible uses — but keeps every row it computed, returning the styled grid plus the window's [start, end) span. The caller caches the result and reuses it while the viewport stays inside the window, so scrolling costs nothing until the view nears an edge; re-lexing on every wheel tick is what made scrolling crawl over remote links.
The grid is indexed by ABSOLUTE buffer line, not by window offset; rows outside [start, end) come back nil. Both render paths keep it as Tab.Styles and read t.Styles[lineIdx] directly (tab.go, wrap.go), so a window-relative grid would push a "- start" correction into every consumer, including the cursor and hit-test paths where an off-by-one mis-colours silently. The price is one nil slice header per file line, paid only when the window is actually re-tokenised.
func IsWordChar ¶ added in v0.2.1
IsWordChar reports whether r counts as part of a word for selection and caret motion. Underscore is included because snake_case identifiers read as one token to a programmer; everything else — punctuation, whitespace, operators — is a boundary. See the file comment for why letters outside ASCII (including CJK) are in.
func LineCommentPrefix ¶
LineCommentPrefix returns the single-line comment marker for path. The boolean is false for file types that do not have a safe line-comment syntax.
func LineVisualCol ¶
LineVisualCol returns the visual column (0-based) at the rune position runeCol within runes. Tabs in the prefix expand to tab stops and wide glyphs count double. Used for cursor placement and selection / find highlighting.
runeCol is clamped to [0, len(runes)] so callers can pass an end-of-line position without bounds-checking, and a runeCol that lands *inside* a grapheme cluster reports the column the cluster starts at: a caret can sit before or after "é" but there is no cell between the e and its accent to report.
func NextCluster ¶ added in v0.2.1
NextCluster returns the rune index of the cluster boundary after the cluster containing i — where a rightward caret step or a forward Delete lands. Past the last rune it returns the rune count, the end-of-line caret position.
func PosLess ¶
PosLess is the exported version of posLess for use from neighbouring packages that need to compare positions.
func PosOrdered ¶
PosOrdered returns (a, b) sorted in document order.
func PrevCluster ¶ added in v0.2.1
PrevCluster returns the rune index of the cluster boundary before i — where a leftward caret step or a Backspace lands. An i sitting inside a cluster snaps to that cluster's start rather than skipping a whole character, so a caret that got there by some other route is repaired instead of stepping over text the user can see.
func RuneColAtVisual ¶
RuneColAtVisual returns the rune index of the cluster whose cells cover targetVisualCol, snapping clicks inside a multi-cell glyph back to its first rune. Used by mouse hit-testing — clicking the right half of a CJK ideograph or anywhere in a 4-cell tab places the cursor on that character, not somewhere "inside" it, and never between a base rune and its combining marks.
When targetVisualCol is past the line's end, the rune count is returned (cursor lands at the end-of-line virtual position). Zero-width clusters are skipped rather than returned: there is no cell to click on them with.
func RuneVisualWidth ¶
RuneVisualWidth returns the cell count occupied by r when it lands at the given visualCol (0-based, measured from the start of the line). Hard tabs expand to fill enough cells to reach the next TabStop boundary; every other rune reports the width Unicode gives it in a monospace grid — 0 for combining marks, ZWJ, and control characters, 2 for east-asian wide and fullwidth glyphs and emoji, 1 for the rest.
This is a per-RUNE answer, and a rune is not always a character: U+FE0F reports 0 here but widens the emoji in front of it, and the five runes of a family emoji report 2+0+2+0+2 while the family itself paints in two cells. Layout code walks clusters through ClusterAt for exactly that reason. Reach for this only with a rune that has no neighbours.
func WordLeft ¶ added in v0.2.1
WordLeft returns the column a leftward word motion from col lands on: skip backwards over any boundary clusters, then over the word run behind them, so the caret stops at the START of the word to its left. col is clamped into runes at both ends — the caret can legitimately sit one past the last rune, and clamping here means no caller has to.
The walk steps by grapheme cluster and classifies a cluster by its base rune, so a motion can never stop between a letter and its accent.
func WordRangeAt ¶ added in v0.2.1
WordRangeAt returns the half-open [start, end) column span of the word covering col. ok is false when col does not sit on a word rune (the click landed in whitespace or punctuation) — callers treat that as "select nothing" rather than selecting the empty range, which would silently drop whatever selection the user already had.
A caret sitting immediately after a word (col == end of the run) still resolves to that word: that is where a double-click at the right edge of a token puts you, and it is what makes Alt+Right then double-click agree.
func WordRight ¶ added in v0.2.1
WordRight returns the column a rightward word motion from col lands on: skip forward over any boundary clusters, then over the word run past them, so the caret stops at the END of the word to its right. This mirrors WordLeft — left lands on a word's first rune, right lands one past its last — which is what makes a left-then-right round trip re-select the same token.
func WrapRowOfCol ¶ added in v0.1.5
WrapRowOfCol returns the segment index that renders rune column col: the largest i with segs[i] <= col. A col at a segment boundary belongs to the later row (the cursor sits at that row's first cell), and the end-of-line position belongs to the last row.
func WrapSegments ¶ added in v0.1.5
WrapSegments returns the rune start index of each visual row of a line wrapped to width cells. The result always holds at least [0], and every segment starts strictly after the previous one, so callers can rely on progress even for pathological widths. Whitespace never triggers a break — a run of spaces or tabs hangs past the right edge (painting clips it) so continuation rows never start with the wrapping space.
The walk steps by grapheme cluster, so a break can never land between a base rune and its combining mark, and a two-cell glyph that would straddle the right edge moves down whole rather than being sliced in half. Every returned index is therefore a cluster boundary, which is what lets callers hand a segment's rune subslice to the visual-column helpers as if it were a line of its own.
Types ¶
type BracketMatch ¶ added in v0.2.1
BracketMatch is the answer to "what bracket is the caret touching, and where is its partner". Found is false both when the caret is not on a bracket and when the scan hit bracketScanLimit without deciding — in either case the honest render is no highlight at all, so the two collapse into one flag on purpose. Matched distinguishes "found the partner" from "this bracket is unbalanced", which render differently.
func MatchBracketAt ¶ added in v0.2.1
func MatchBracketAt(buf *Buffer, p Position) BracketMatch
MatchBracketAt returns the bracket the caret at p is touching together with its partner. The scan starts on the bracket itself so its own depth contribution needs no special case.
type Buffer ¶
type Buffer struct {
Lines []string
// contains filtered or unexported fields
}
Buffer is a simple editable text buffer backed by one string per line. String-per-line keeps the surrounding code readable; Go string ops are fast enough that rebuilding a single line on each edit is fine for the file sizes this editor actually opens (review + small edits).
Lines never carry a line terminator: NewBuffer strips the CR of a CRLF pair on load and Tab restores the file's own ending on save (see Tab.LineEnding), so everything between load and save deals in one convention.
func NewBuffer ¶
NewBuffer constructs a Buffer from a string by splitting on newlines. A trailing newline produces an empty final line, mirroring how files commonly end and what most editors display. The CR of a CRLF pair is stripped: leaving it on would append an invisible extra column to every line of a Windows-authored file, widen every rune count, and get written straight back to disk. The file's real ending is recorded on the Tab and restored by Tab.Save.
func (*Buffer) Clamp ¶
Clamp adjusts a position so that Line and Col fall within the buffer. Col is clamped to the rune length of its line (so it can sit one past the last rune, which is where the cursor lives at end-of-line).
func (*Buffer) DeleteRange ¶
DeleteRange removes everything between a and b (in any order) and returns the resulting position (the smaller of the two). Both endpoints are clamped first; an empty range is a no-op.
func (*Buffer) EndPos ¶
EndPos returns the position just after the last rune of the buffer. Useful for select-all and end-of-document navigation.
func (*Buffer) InsertString ¶
InsertString inserts text (which may contain newlines) at p and returns the position immediately after the inserted text. p is clamped first.
func (*Buffer) LineRunes ¶
LineRunes returns the runes of the line at i, or nil if i is out of range. The caller MUST treat the returned slice as read-only: it is shared with the buffer's decode cache, so writing through it corrupts every later reader of that line.
The decode is memoised because the render and soft-wrap paths ask for the same handful of lines several times per frame — wrap.go walks the viewport once for EnsureVisible, again for the clamp, and again to paint — and a bare []rune conversion per call allocates O(viewport x lineLen) on every repaint. Freshness is checked against the source string instead of being tracked by invalidation calls, so an edit through any of the writers of Lines (here, comment.go, lineops.go, undo.go, or an outside caller) can never be served stale runes.
func (*Buffer) String ¶
String serialises the buffer back to a single LF-joined string. Use TextWith when writing to disk so the file keeps the ending it came in with.
type GitLineChange ¶
type GitLineChange int
GitLineChange describes the marker rendered in the editor gutter for a line.
const ( GitLineNone GitLineChange = iota GitLineModified GitLineAdded GitLineDeleted )
type LineEnding ¶ added in v0.2.1
type LineEnding int
LineEnding is the newline convention a file uses on disk. Buffers are normalised to bare LF-separated lines on load (see NewBuffer) and the recorded ending is written back on save, so editing one line of a CRLF file doesn't turn the whole file into a diff.
const ( // LineEndingLF is "\n". It is the zero value, so a Tab built by hand // (tests, scratch buffers) writes POSIX-style. LineEndingLF LineEnding = iota // LineEndingCRLF is "\r\n" — the convention Windows-authored files // arrive with. LineEndingCRLF )
func (LineEnding) Newline ¶ added in v0.2.1
func (e LineEnding) Newline() string
Newline returns the bytes this ending writes between lines.
type Match ¶
Match describes one find hit. Line and Col follow the same rune-indexed convention as Position; Width is the rune count of the query so the renderer can paint the right number of cells without re-running the matcher.
func FindAll ¶
FindAll returns every substring match of query inside buf, in document order. Matching is smart-case: an all-lowercase query matches any case, any uppercase letter in the query makes the match exact — so "id" finds ID and id, while "ID" finds only ID. An empty query returns nil — the caller is expected to clear its UI rather than show "0 of 0" results. Matches do not overlap: after a hit the scanner advances past the matched run, so "aaaa" with query "aa" yields two matches at columns 0 and 2.
type Position ¶
Position is a buffer location measured in lines and rune-indexed columns. Line is 0-based; Col is 0-based and counts runes — not bytes, not screen cells, and not characters as a user counts them. A CJK glyph is one column but two cells; an "é" written as e + U+0301 is two columns, one cell, and one character. cluster.go converts between the three, and the caret only ever rests on a column that starts a grapheme cluster.
func MatchEndPosition ¶
MatchEndPosition returns the position one past the end of m — useful when the caller wants to set a selection that covers the match.
func MatchPosition ¶
MatchPosition returns the cursor-friendly Position at the start of m. Trivial helper, but it keeps callers from constructing Position literals by hand (which loses the "rune-indexed" intent at the call site).
type Tab ¶
type Tab struct {
Path string // Empty for an unsaved/scratch tab.
Buffer *Buffer
Cursor Position // Where new typed text appears.
Anchor Position // Selection anchor; equals Cursor when nothing is selected.
ScrollY int // Index of the first visible line.
ScrollX int // Index of the first visible column (rune-indexed). Always 0 with Wrap on.
Dirty bool
// LineEnding is the convention the file had on disk. The buffer
// holds unterminated lines, so this is the only record of how the
// file wants to be written back; Save re-joins with it.
LineEnding LineEnding
// Wrap turns on soft wrap: long lines flow onto continuation rows
// instead of panning horizontally. Stamped by the app from the user
// config on tab creation and flipped via SetWrap. ScrollSeg is the
// wrap-mode half of the scroll anchor — the segment index within
// ScrollY's line of the first visible row (always 0 with Wrap off).
// lastWrapW caches the content width of the last wrap-mode render so
// wheel scrolling between frames can do visual-row math; 0 means
// "never rendered", which falls back to line scrolling. See wrap.go.
Wrap bool
ScrollSeg int
// ScrollbarActive is a pure presentational flag: the app sets it
// while the user is dragging this tab's scrollbar thumb so
// renderScrollbar can brighten the thumb to Accent, exactly the way
// drawSplitter brightens the sidebar splitter mid-drag. It never
// affects geometry or scroll state — see App.setScrollbarDrag.
ScrollbarActive bool
Styles [][]tcell.Style
StyleStale bool
GitLines map[int]GitLineChange
// Mtime is the file's modification time as of the last successful
// read or write. The app's periodic disk-reconcile loop compares it
// against the live mtime to detect external edits.
Mtime time.Time
// DiskGone is set when the most recent disk check found the file
// missing. It exists so we only flash the "deleted on disk" warning
// once, instead of re-flashing every reconcile tick.
DiskGone bool
// Mode is "" for a normal text tab and imageMode (= "image") for a
// read-only image preview. Image tabs reuse the Tab type so the
// app's tab list, switcher, and modal-routing all just work — the
// content-mutating methods short-circuit on imageMode and Render
// delegates to renderImage. See image.go for the render path.
Mode string
Image image.Image // populated when Mode == imageMode
ImageFmt string // "png" / "jpeg" / "gif" — for the status bar
// Find state — populated when the user opens the find bar and
// types a query. The UI layer (App) owns the bar geometry and
// keystroke routing; the tab owns the query, the resolved match
// list, and the index of the "current" match so the query
// survives switching tabs and re-opening the bar.
FindQuery string
FindMatches []Match
FindIndex int // -1 = no current match; otherwise an index into FindMatches.
// Preview marks a tab opened by single-clicking the file tree: the
// next single-click preview replaces it in place instead of piling
// up tabs (VS Code / druk behavior). Editing or an explicit open
// "pins" the tab. Always read through IsPreview(), which treats a
// dirty buffer as pinned regardless of this flag.
Preview bool
// IndentUnit is the string the editor inserts when the user presses
// Tab. Detected on file open (DetectIndent) so the editor matches
// whatever the file already does — a tab-indented Go file gets a
// real tab; a 2-space-indented file gets two spaces. Mixed-style
// files take the dominant signal.
IndentUnit string
// contains filtered or unexported fields
}
Tab is a single open file. It owns the on-disk path, the in-memory buffer, the per-tab view state (scroll position, cursor, selection anchor), the cached syntax-highlight styles, and a dirty flag.
func NewTab ¶
NewTab opens path and returns a Tab. If the file does not exist, the tab is created with an empty buffer that will be written on first save — matching what most editors do when you "open" a brand-new file path. When path looks like an image we recognise (PNG / JPEG / GIF), the tab is opened in read-only image-preview mode instead of as text.
func (*Tab) Backspace ¶
func (t *Tab) Backspace()
Backspace deletes the character before the cursor (or the selection if any). "Character" means grapheme cluster, not rune: backspacing over "é" takes the accent with the e rather than stranding a combining mark on the letter in front of it, and one press removes one thing the user can see. Coalesces with adjacent backspaces inside the undo window. No-op on image tabs.
func (*Tab) CanRevert ¶
CanRevert reports whether the buffer differs from the original state captured at NewTab / Reload time. Used to gate the Revert menu row.
func (*Tab) CanUndo ¶
CanUndo reports whether there is anything to roll back. The action menu uses this to enable / disable the Undo row.
func (*Tab) CenterOnCursor ¶ added in v0.1.1
CenterOnCursor scrolls so the cursor's line sits mid-viewport. Used by goto-line so the target lands with context above and below it instead of hugging the edge the way plain EnsureVisible leaves it. viewH <= 0 (headless callers, pre-first-draw) is a no-op — the next Render's EnsureVisible still guarantees visibility.
func (*Tab) ClampCursorToView ¶ added in v0.2.13
ClampCursorToView moves the caret to the nearest visible line when a viewport-only scroll (wheel, scrollbar) has left it off screen — the optional "caret follows scroll" behavior, called by the app only when the user turned it on. Guards, in order: an active selection is never clobbered (silently moving the selection head mid-scroll would destroy what the user highlighted), and a caret already inside the viewport is left untouched so the render pass sees no cursor motion at all. The clamped position is inside the viewport by construction, which keeps EnsureVisible a no-op afterwards — scroll → cursor never feeds back into cursor → scroll, so the one-directional cursorMoved contract survives with this feature enabled. Wrap width comes from lastWrapW, the same source Scroll uses.
func (*Tab) ClearFind ¶
func (t *Tab) ClearFind()
ClearFind drops every piece of find state. The app calls this when the buffer has been edited enough that the cached match list is stale and can't safely be re-used; the user will re-type their query.
func (*Tab) Delete ¶
func (t *Tab) Delete()
Delete removes the character after the cursor (or the selection if any), again a whole grapheme cluster so a forward delete can't behead a character and leave its marks behind. Coalesces with adjacent forward-deletes inside the undo window. No-op on image tabs.
func (*Tab) DeleteSelection ¶
func (t *Tab) DeleteSelection()
DeleteSelection removes the selected range and collapses the cursor to the start of the selection. A no-op when nothing is selected.
func (*Tab) DisplayName ¶
DisplayName returns the basename of Path, or "untitled" for unsaved tabs.
func (*Tab) DuplicateLines ¶ added in v0.1.1
func (t *Tab) DuplicateLines()
DuplicateLines inserts a copy of the selected line block directly below itself. The cursor (and anchor) land on the copy at the same column, so "duplicate then edit the copy" needs no extra movement.
func (*Tab) EnsureVisible ¶
EnsureVisible scrolls the viewport so the cursor is on screen. The caller passes the editor area's width and height because the Tab itself doesn't know its render rect.
func (*Tab) FindNext ¶
func (t *Tab) FindNext()
FindNext advances FindIndex by one (wrapping at the end) and moves the cursor onto the new match. No-op when there are no matches. Used by Enter inside the find bar and by the Esc-g "again" leader.
func (*Tab) FindPrev ¶
func (t *Tab) FindPrev()
FindPrev moves FindIndex backwards by one (wrapping at the start) and moves the cursor onto the new match. Used by Shift-Enter inside the find bar.
func (*Tab) FocusCurrentMatch ¶
func (t *Tab) FocusCurrentMatch()
FocusCurrentMatch moves the cursor (and anchor — we don't want a dangling selection from an earlier action) to the start of the currently-pointed match. No-op when FindIndex is out of range, so callers don't have to re-check it themselves.
func (*Tab) GoToMatchingBracket ¶ added in v0.2.1
GoToMatchingBracket jumps the caret onto the partner of the bracket it is touching and returns true. False means there was nothing to jump to, so the caller can say so instead of leaving the user wondering.
The jump lands ON the partner rather than past it, which means the pair stays highlighted from the far end and pressing the action again returns you to where you started.
func (*Tab) HasMatchingBracket ¶ added in v0.2.1
HasMatchingBracket reports whether the caret is touching a bracket whose partner we found. The action menu uses it to dim "Go to matching bracket" when the jump would do nothing.
func (*Tab) HasSelection ¶
HasSelection reports whether the tab currently has a non-empty selection.
func (*Tab) HitTest ¶
HitTest converts screen coordinates within this tab's render area to a buffer position. ok=false means the click was outside any line.
func (*Tab) InsertNewline ¶ added in v0.2.1
func (t *Tab) InsertNewline()
InsertNewline is what Enter does: split the line at the caret and open the new line with the same indentation the old one had, plus one level when the caret was sitting after an opening brace / bracket / paren (or, in Python and YAML, a colon). Without this, every line in an indented block starts at column 0 and the user re-types the leading whitespace by hand, which is the single most-noticed thing a terminal editor can get wrong.
The whole press is one InsertString call and therefore exactly one undo step: Enter-then-undo returns the buffer to where it was, rather than stranding the user on a half-indented line.
The indent is read from the text BEFORE the caret at the point the split will happen — the start of the selection, when there is one. Deleting a selection never touches the runes ahead of its start, so reading the prefix up front gives the same answer as reading it after the delete, and avoids splitting the operation into two undo entries.
Only "\n" is ever inserted; the file's own ending is restored by Save (see Tab.LineEnding), so a CRLF file must not get a CR spliced into the middle of a line here.
func (*Tab) InsertRune ¶
InsertRune inserts a single typed character at the cursor. Coalesces with adjacent runes inside the undo window so a typed word collapses into a single undo step rather than one entry per keystroke. No-op on image tabs.
func (*Tab) InsertString ¶
InsertString inserts s at the cursor (replacing any selection first) and advances the cursor past the inserted text. Always recorded as a structural undo step — pasted text or "\n" presses shouldn't merge with the surrounding typing burst. No-op on image tabs.
func (*Tab) IsImage ¶
IsImage reports whether the tab is an image-preview, not a text editor. Callers use this to skip text-only behaviour (cursor placement, key dispatch, save, etc.) without having to know about Mode strings.
func (*Tab) IsPreview ¶ added in v0.1.1
IsPreview reports whether the tab is still replaceable by the next tree-click preview. A dirty buffer is never a preview — the user's edits pin it implicitly, so a half-typed change can't be silently swapped out from under them.
func (*Tab) JumpToLine ¶ added in v0.1.1
JumpToLine moves the cursor to column 0 of the 1-based line n, clamping to the buffer. The selection collapses — a goto is navigation, not extension — and cursorMoved is set so the next Render scrolls the target into view even if the caller never centers the viewport.
func (*Tab) MoveCursor ¶
MoveCursor shifts the cursor by dLine lines and dCol characters. When extend is true the anchor is left in place so the user is extending a selection.
dCol counts grapheme clusters, taken one step at a time: an arrow key walks past "é" or an emoji in a single press, and a multi-column delta that runs off the end of a line keeps stepping onto the next line instead of losing the overshoot. Vertical motion keeps the rune column (the historical behaviour — it is not a "sticky visual column") but snaps it onto a cluster boundary in the new line.
func (*Tab) MoveCursorTo ¶
MoveCursorTo sets the cursor to a specific buffer position. Position is clamped within the buffer and snapped back to a grapheme boundary — a caret parked between a base rune and its accent would render a cell to the left of where it edits — and extend=true preserves the selection anchor.
func (*Tab) MoveLineEnd ¶
MoveLineEnd moves the cursor to the last column of the current line.
func (*Tab) MoveLineHome ¶
MoveLineHome moves the cursor to column 0 of the current line.
func (*Tab) MoveLinesDown ¶ added in v0.1.1
func (t *Tab) MoveLinesDown()
MoveLinesDown is the mirror gesture: the block swaps with the line below it, stopping dead at the bottom of the buffer.
func (*Tab) MoveLinesUp ¶ added in v0.1.1
func (t *Tab) MoveLinesUp()
MoveLinesUp swaps the selected line block with the line above it. The cursor and anchor ride along, so a selection survives repeated nudges. At the top of the buffer it's a hard no-op — no dirty flag, no undo entry.
func (*Tab) MoveWordLeft ¶ added in v0.2.1
MoveWordLeft walks the caret one word to the left, extending the selection when extend is set. At column 0 the motion wraps to the end of the previous line — one step, not "wrap and keep hunting" — so the gesture stays predictable at a line boundary, exactly like Left does.
func (*Tab) MoveWordRight ¶ added in v0.2.1
MoveWordRight walks the caret one word to the right, extending the selection when extend is set. Past the last rune of a line the motion wraps to column 0 of the next line, mirroring MoveWordLeft.
func (*Tab) Pin ¶ added in v0.1.1
func (t *Tab) Pin()
Pin makes a preview tab permanent (drops the italic, stops the replace-in-place behavior). Safe to call on any tab.
func (*Tab) Redo ¶
Redo re-applies a step that was just undone. Returns true when a step was actually redone.
func (*Tab) Reload ¶
Reload re-reads the file from disk into the buffer and resets undo history. Image tabs decode the file again instead of replacing the text buffer. Use this for reloads the user explicitly chose (the disk-conflict prompt's "Reload" button) — history is discarded because the user is knowingly taking the disk version. For reloads the user did NOT ask for (format-on-save, the background external-change reconcile), use ReloadKeepHistory instead so their prior edits stay undoable.
func (*Tab) ReloadKeepHistory ¶ added in v0.2.11
ReloadKeepHistory re-reads the file from disk like Reload but keeps the tab's undo history, pushing the pre-reload buffer as its own undoable step. This is the right shape for reloads the user did NOT explicitly ask for — format-on-save and the background external-change reconcile — where wiping history would destroy work the user never chose to give up.
func (*Tab) Render ¶
Render draws the editor's content (line numbers, code with syntax highlighting, selection, cursor) into the rectangle (x, y, w, h). Image tabs delegate to renderImage instead of drawing text.
func (*Tab) ReplaceAllMatches ¶ added in v0.1.1
ReplaceAllMatches swaps every match for repl as ONE undo step and returns how many were replaced. Matches are applied last-to-first so earlier spans stay valid while later ones are rewritten.
func (*Tab) ReplaceCurrentMatch ¶ added in v0.1.1
ReplaceCurrentMatch swaps the current find match for repl and re-runs the query so the highlights (and the match count) stay truthful. The cursor lands just after the replacement and the current index stays put, so "replace, replace, replace" walks the file forward naturally. Returns false when there is nothing to replace.
func (*Tab) ReplaceLines ¶ added in v0.1.2
ReplaceLines swaps whole lines (0-based index → new content) as ONE undo step — project-wide replace routes open buffers through here so a tab keeps its history and its dirty-state semantics. Out-of-range indexes are ignored (the caller verified against a live buffer, but buffers move). Returns how many lines were actually swapped.
func (*Tab) RestoreView ¶ added in v0.2.12
RestoreView puts a tab back on a remembered place: the caret at cursor (clamped, selection collapsed) and the viewport at scrollY. It is the seam the app restores a session, a reopened tab, or any other saved spot through, so nothing outside this package has to assign Cursor, Anchor and ScrollY by hand — which is what used to let a restore skip cursorMoved (the next Render would not scroll the caret into view) and skip the undo-group break (typing right after a restore would coalesce into whatever burst was in flight before it).
ScrollSeg resets because scrollY names a BUFFER LINE: a remembered place carries no segment to go with it, and wrap.go's anchor is the pair (ScrollY, ScrollSeg) — a stale segment left behind would open the file part-way down a wrapped line. Row 0 of that line is the honest reading of "scrolled to line N", and it is what CenterOnCursor settles on for the same reason.
func (*Tab) RevertFile ¶
RevertFile rewinds the buffer all the way back to the snapshot captured when the file was first opened (or last reloaded). The current state is pushed onto the undo stack first so the user can recover from an accidental Revert with one Undo. Returns true when the buffer actually changed; false if it was already at the original.
func (*Tab) Save ¶
Save writes the buffer to disk and clears Dirty. It is an error to call Save on an untitled tab — callers should prompt for a path first. Mtime is refreshed so the disk-reconcile loop doesn't immediately think the file we just wrote was changed by someone else. Image tabs return an error since the editor only knows how to read those, not re-encode them.
func (*Tab) Scroll ¶
Scroll moves the viewport by delta lines (negative = up). Render runs clampScroll afterwards so the user never scrolls into pure void; here we just adjust the raw value. In wrap mode a "line" of scrolling is a visual row, so a wheel tick over a long wrapped line moves one row, not one whole paragraph; before the first render (no cached width yet) we fall back to buffer-line motion.
func (*Tab) ScrollH ¶
ScrollH moves the viewport horizontally by delta rune-columns (negative = left). Clamped at zero; the right side is naturally bounded by Render's contentW window — scrolling past the longest visible line just shows blank space, which is fine. Lives next to Scroll so the app's mouse-wheel dispatcher can treat horizontal and vertical wheels symmetrically. A no-op in wrap mode — nothing extends past the right edge, so a horizontal wheel has nothing to reveal.
func (*Tab) ScrollTargetForClick ¶ added in v0.1.1
ScrollTargetForClick maps a click at bar-local row clickY to the ScrollY it requests — the app's mouse handler funnels both the initial press and thumb drags through this.
func (*Tab) ScrollbarVisible ¶ added in v0.1.1
ScrollbarVisible reports whether the tab draws a scrollbar in a viewH-row viewport — text tabs taller than the view only.
func (*Tab) SelectWordAt ¶ added in v0.2.1
SelectWordAt selects the word under the buffer position p, or does nothing when p sits in whitespace / punctuation. Used by double-click in the app layer.
cursorMoved is deliberately NOT set: the word was just clicked, so it is on screen by construction, and flagging a scroll here would fight the auto-scroll a drag may already be running.
func (*Tab) SelectionText ¶
SelectionText returns the currently selected text, or "" if nothing is selected. The text is always returned in document order.
func (*Tab) SetFindQuery ¶
SetFindQuery installs a new search query on the tab, recomputes the match list against the current buffer, and points FindIndex at the first match at or after the cursor (so the user lands on the nearest hit, not always the first hit in the file). An empty query clears all find state — symmetrical with closing the bar via Esc.
The cursor is left where it is; SetFindQuery only updates state. It is the caller's job to call FocusCurrentMatch when they want the cursor to actually move (which is what happens on the first non-empty query and on every Enter / Shift-Enter press).
func (*Tab) SetWrap ¶ added in v0.1.5
SetWrap switches soft wrap on or off for this tab and resets the view state the other mode owns: wrap clears any horizontal pan, unwrap clears the segment anchor. cursorMoved is set so the next Render brings the cursor back into view in the new geometry.
func (*Tab) ToggleLineComment ¶
ToggleLineComment comments or uncomments the selected lines. It returns ok=false when the active file type has no known line-comment marker.
type TabList ¶ added in v0.1.7
type TabList struct {
// contains filtered or unexported fields
}
TabList owns the ordered set of open tabs, the active tab, and the preview-slot rules. Its interface speaks tab identity (*Tab) — never list position and never path — so a reference held across a mutation (a dirty-close modal's callback, an async format result) can never act on the wrong tab. Indexes appear only as transient values for the tab strip's geometry; they must not be stored.
func (*TabList) Activate ¶ added in v0.1.7
Activate makes t the active tab. Returns false when t is not in the list — a stale reference to a closed tab activates nothing.
func (*TabList) ActivateAt ¶ added in v0.1.7
ActivateAt activates the tab at position i, clamped into range — for tab strip clicks, where the index comes fresh from the hit test.
func (*TabList) Active ¶ added in v0.1.7
Active returns the active tab, or nil when the list is empty.
func (*TabList) ActiveIndex ¶ added in v0.1.7
ActiveIndex returns the active tab's position — transient, for the tab strip's scroll math.
func (*TabList) At ¶ added in v0.1.7
At returns the tab at position i, or nil when i is out of range — for the tab strip's row-by-row drawing only.
func (*TabList) IndexOf ¶ added in v0.1.7
IndexOf returns t's position, or -1 when t is not in the list.
func (*TabList) InsertPreview ¶ added in v0.1.7
InsertPreview places the preview tab t: reusing the existing preview tab's slot when there is one — tab order is part of the user's spatial memory, so browsing must not reshuffle it — and appending otherwise. Activates t either way.
func (*TabList) Lookup ¶ added in v0.1.7
Lookup returns the open tab for path, or nil. Paths are unique in the list: the open path always reuses an existing tab.
func (*TabList) Preview ¶ added in v0.1.7
Preview returns the current preview tab, or nil. There is at most one: every preview open either replaces or pins it.
func (*TabList) Remove ¶ added in v0.1.7
Remove takes t out of the list by identity. Removing a background tab keeps the current active tab active; removing the active tab activates its right neighbour (or the new last tab). Returns false when t is not in the list — a stale reference removes nothing.