| 🛟 Updated. 28d5985 k33g 16h ago | 1 | # editor |
| 2 | |
| 3 | The text-editing widget: a scrolling, colouring viewport onto one buffer. |
| 4 | |
| 5 | This is where the pure packages meet the screen — `buffer` holds the text, `syntax` colours it, `theme` says in what, `ui` draws it — and it is the only place that knows how all four fit together. |
| 6 | |
| 7 | ## What a view draws |
| 8 | |
| 9 | ``` |
| 10 | 1 package main ▲ ← line-number gutter, then the text, then |
| 11 | 2 ▓ the vertical scroll bar |
| 12 | 3 import "fmt" ░ |
| 13 | ◄▓░░░░░░░░░░░░░░░░░░░░░░░► ← the horizontal one along the bottom |
| 14 | ``` |
| 15 | |
| 16 | Each character is drawn in its **syntax colour**, unless the selection covers it. On the cursor's own line the syntax colours keep their foreground but take the current-line background, so the highlight shows *through* the coloured tokens instead of being punched full of holes. |
| 17 | |
| 18 | Tabs are expanded to the buffer's tab width, and the view scrolls in both directions to keep the cursor visible. |
| 19 | |
| 20 | The **cursor is marked twice**. The theme's `editor.cursor` background is sent to the terminal as the colour of its *own* cursor, and the cell underneath is painted in the same style as a fallback for terminals that ignore that. Painting alone is not enough: a terminal draws its cursor over the cell, in whatever colour the user configured for some other palette, so on a dark theme it is very often a dark cursor covering whatever is beneath it. Only the terminal cursor's own colour can win, and only the theme should decide it. |
| 21 | |
| 22 | The colours must be a **distinct pair**, never a reversal of the line — terminals that draw their cursor by inverting the cell would invert it straight back into invisibility. Tests hold every theme to that, and to a minimum distance from the line it sits on. Only the **active** window shows a cursor: a window tells its content whether it is focused. |
| 23 | |
| 24 | The theme-arithmetic tests live here for the same reason the drawing does: this is where colour meets the screen. Two measures, and both are needed. **Channel distance** answers "can the eye see these are two colours" — right for a cursor, a highlight, a selection. **WCAG relative luminance** answers "can this be read" — right for prose, and the reason `syntax.comment` is held to 4.5:1 in every theme this project authors. Turbo Classic's comments cleared the first by a mile and failed the second, and were hard to read for exactly that long. |
| 25 | |
| 26 | ## Keys |
| 27 | |
| 28 | Turbo C's bindings where they still make sense, modern ones where they do not. |
| 29 | |
| 30 | | | | |
| 31 | | --- | --- | |
| 32 | | Arrows, `Home`, `End`, `PgUp`, `PgDn` | Move | |
| 33 | | `Ctrl-←` `Ctrl-→` | By word | |
| 34 | | `Ctrl-Home` `Ctrl-End` | Start and end of the file | |
| 35 | | `Shift`+any movement | Extend the selection | |
| 36 | | `Enter` | New line, keeping the indent | |
| 37 | | `Tab` / `Shift-Tab` | Indent / unindent — the whole selection when there is one | |
| 38 | | `Ctrl-Z` `Ctrl-Y` | Undo, redo | |
| 39 | | `Ctrl-C` `Ctrl-X` `Ctrl-V` | Clipboard | |
| 40 | | `Ctrl-Ins` `Shift-Del` `Shift-Ins` | The same, spelled the Turbo C way | |
| 41 | | `Ctrl-A` | Select all | |
| 42 | | `Ctrl-Space` | Ask for completion | |
| 43 | |
| 44 | Typing a `.` asks for completion too, since that is where the list is most useful. Mouse: click to place the cursor, drag to select, wheel to scroll. |
| 45 | |
| 46 | ## Public API |
| 47 | |
| 48 | | | | |
| 49 | | --- | --- | |
| 50 | | `NewView(buf, clipboard) *View` | A view onto a buffer, sharing a clipboard with the other views | |
| 51 | | `(*View) Buffer() *buffer.Buffer` | The text being edited | |
| 52 | | `(*View) Draw`, `HandleKey`, `HandleMouse`, `Bounds`, `SetBounds` | The `ui.Widget` contract | |
| 53 | | `(*View) Cut/Copy/Paste/Undo/Redo/SelectAll() bool` | What the Edit menu calls | |
| 54 | | `(*View) Indent() / Unindent()` | Block indentation, one undo step | |
| 55 | | `(*View) InsertSnippet(string)` | Insert text at the cursor, re-indenting the lines after the first to match the current line; one undo step | |
| 56 | | `(*View) GoToLine(int)` | Jump, counting from one | |
| 57 | | `(*View) EnsureCursorVisible()` | Scroll so the cursor is on screen | |
| 58 | | `(*View) CursorStatus() string` | The `line:column` the status bar shows | |
| 59 | | `(*View) WordBeforeCursor() string` | The identifier being typed — what a completion list filters on | |
| 60 | | `(*View) ReplaceWordBeforeCursor(string)` | Accept a completion | |
| 61 | | `(*View) LineNumbers() / SetLineNumbers(bool)` | Show or hide the gutter | |
| 62 | | `(*View) RefreshSyntax()` | Re-decide whether this file can be coloured, after a rename | |
| 63 | | `(*View) SetMarks(map[int]Severity)`, `(*View) Marks()` | Which lines have something wrong with them | |
| 64 | | `(*View) InsertLine()`, `(*View) DeleteLine()` | Turbo C's Ctrl-N and Ctrl-Y | |
| 65 | | `(*View) SetClock(func() time.Time)` | The clock two clicks are timed against; only a test calls it | |
| 66 | | `Clipboard` | The text shared between views by cut, copy and paste | |
| 67 | |
| 68 | Three callbacks let the app follow along without the view knowing what an app is: `OnChange`, `OnCursorMove`, `OnCompletionRequest`. |
| 69 | |
| 70 | ## Why a snippet is re-indented |
| 71 | |
| 72 | `InsertSnippet` copies the current line's leading whitespace onto every line of the text after the first. Inserting a multi-line body verbatim restarts it at column zero, which is wrong everywhere except the top level of a file — and an `if err != nil` is inserted inside something by definition. Copying the line's *own* prefix follows whatever the file already uses, tabs or spaces, rather than imposing a choice. |
| 73 | |
| 74 | A blank line in the body stays blank: padding it to the indent leaves trailing whitespace that every formatter then strips, which is noise in the very next diff. |
| 75 | |
| 76 | ## Why its own clipboard |
| 77 | |
| 78 | A terminal program cannot read the host's clipboard portably. A clipboard shared between the editor's own windows is what Turbo C offered, and it is what this offers. |
| 79 | |
| 80 | ## Tests |
| 81 | |
| 82 | ```sh |
| 83 | make test |
| 84 | go test ./editor/ |
| 85 | ``` |
| 86 | |
| 87 | ## Marks, and why this package has never heard of a language server |
| 88 | |
| 89 | A mark is a character in the gutter beside a line something is wrong with. The severities are `editor.Severity` — this package's own type — and not the protocol's, because this package colours and draws text and knows nothing about protocols. Whoever holds the diagnostics translates on the way in. The alternative was importing `lsp` here to draw one character, which would put the protocol underneath the drawing. |
| 90 | |
| 91 | The mark goes in the column that **separates the line numbers from the text**. The gutter is already `digits + 1` wide, the +1 being that separator, so a mark costs no layout: not the text's left edge, not the cursor's screen column, not the column a mouse click lands on. The consequence, stated where anyone changing it will read it: **hiding the line numbers hides the marks.** The alternative — a one-column gutter appearing when the numbers are off — would make the text jump sideways the moment a server reported anything. |
| 92 | |
| 93 | The three `diagnostic.*` theme keys are what they are drawn in. They had been defined in every shipped theme, tested for readability, and drawn nowhere at all. |
| 94 | |
| 95 | ## Counting clicks |
| 96 | |
| 97 | A second press on the **same cell** within 400 ms selects the word under it. Same cell, not merely near: a press one column over is a different word, and selecting the first because the pointer drifted is worse than selecting nothing. |
| 98 | |
| 99 | Two details are what make this work rather than nearly work. tcell sends `Button1` for every event of a **drag** as well as for a press, so clicks are counted only when no drag is in progress — otherwise a slow drag becomes a double click halfway through. And the clock is a **field**, set with `SetClock`, for the same reason `app.App` has one: a double click measured against the real clock is a test that passes or fails by how fast the machine is. |
| 100 | |
| 101 | After a double click the drag carries on from the word, so holding and moving extends from the **start of the word**, by character. That is not the word-by-word drag a large editor does; it is the cheap half of it, and it beats the alternative, which is the word collapsing back to a single character the moment the pointer twitches. |
| 102 | |
| 103 | The counter keeps going past two, so a triple click can be given a meaning later without reworking any of this. |