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
- 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 LineCommentPrefix(path string) (string, bool)
- func LineVisualCol(runes []rune, runeCol int) int
- func PosLess(a, b Position) bool
- func PosOrdered(a, b Position) (Position, Position)
- func RuneColAtVisual(runes []rune, targetVisualCol int) int
- func RuneVisualWidth(r rune, visualCol int) int
- 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
- type GitLineChange
- 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) ClearFind()
- func (t *Tab) Delete()
- func (t *Tab) DeleteSelection()
- func (t *Tab) DisplayName() string
- func (t *Tab) EnsureVisible(viewW, viewH int)
- func (t *Tab) FindNext()
- func (t *Tab) FindPrev()
- func (t *Tab) FocusCurrentMatch()
- func (t *Tab) HasSelection() bool
- func (t *Tab) HitTest(localX, localY, w, h int) (Position, bool)
- func (t *Tab) InsertRune(r rune)
- func (t *Tab) InsertString(s string)
- func (t *Tab) IsImage() bool
- 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) Redo() bool
- func (t *Tab) Reload() error
- func (t *Tab) Render(scr tcell.Screen, th theme.Theme, x, y, w, h 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) SelectAll()
- func (t *Tab) SelectionText() string
- func (t *Tab) SetFindQuery(query string)
- func (t *Tab) ToggleLineComment() (changed bool, ok bool)
- func (t *Tab) Undo() bool
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 ¶
This section is empty.
Functions ¶
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 ¶ added in v0.0.42
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 LineCommentPrefix ¶ added in v0.0.38
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. 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.
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 RuneColAtVisual ¶
RuneColAtVisual returns the rune index whose start column is at or just before targetVisualCol, snapping clicks inside a tab's visual span back to the tab's start. Used by mouse hit-testing — clicking anywhere in a 4-cell tab places the cursor at the tab character itself, not somewhere "inside" it (which would be a no-op anyway).
When targetVisualCol is past the line's end, the rune count is returned (cursor lands at the end-of-line virtual position).
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; everything else is one cell. This deliberately ignores east-asian wide chars and combining marks — the rest of the buffer code treats one rune as one cell and adding wide-char support is a separate, larger project.
Types ¶
type Buffer ¶
type Buffer struct {
Lines []string
}
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).
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.
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 should treat the returned slice as read-only.
type GitLineChange ¶ added in v0.0.42
type GitLineChange int
GitLineChange describes the marker rendered in the editor gutter for a line.
const ( GitLineNone GitLineChange = iota GitLineModified GitLineAdded GitLineDeleted )
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 case-insensitive substring match of query inside buf, in document order. 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), so a multi-byte character or a CJK glyph each count as one column.
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).
Dirty 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.
// 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). 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) 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). 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) 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) 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) 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) MoveCursor ¶
MoveCursor shifts the cursor by (dLine, dCol). When extend is true the anchor is left in place so the user is extending a selection.
func (*Tab) MoveCursorTo ¶
MoveCursorTo sets the cursor to a specific buffer position. Position is clamped within the buffer; 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) 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. Cursor and anchor are clamped to the new content (so the user keeps roughly their place instead of getting snapped to line 0); ScrollY is left alone and gets clamped on the next render. Dirty is cleared and the syntax cache is invalidated. Image tabs decode the file again instead of replacing the text buffer.
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) 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.
func (*Tab) ScrollH ¶ added in v0.0.39
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.
func (*Tab) SelectAll ¶
func (t *Tab) SelectAll()
SelectAll selects the entire buffer (anchor at start, cursor at end).
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) ToggleLineComment ¶ added in v0.0.38
ToggleLineComment comments or uncomments the selected lines. It returns ok=false when the active file type has no known line-comment marker.