@specfork/text-editor
Desktop text and source code editor. Tabs, encodings and line endings, large files, find and replace with regex and find in files, syntax highlighting, multi-cursor, macros, session restore.
Text editor
One person edits plain text and source code files on their own computer. Files open in tabs, keep their encoding and line endings byte for byte, and can be converted on request. Find and replace works in one file, all open files, or a folder tree, with regular expressions. Languages are described by data files that drive highlighting, comments and folding. Several carets and rectangular selections edit many lines at once, macros repeat edits, and the whole session, unsaved text included, comes back after a restart or crash. Behaviour derived from Notepad++'s open-source code (GPL-3.0-or-later), written fresh, stack-free and for any desktop OS. Not affiliated with or endorsed by Don Ho or the Notepad++ project. The name is used only to credit the source.
| Area | Prefix | Covers |
|---|---|---|
| documents.md | DOC | Tabs, untitled documents, open and close, recent files, split view, command line |
| files.md | FIL | Encodings, detection, conversion, line endings, large and binary files, external changes |
| editing.md | EDT | Undo, multi-cursor, column editing, line and case operations, indentation, bookmarks |
| search.md | FND | Find, replace, extended escapes, regex, mark, find in files, results panel |
| languages.md | LNG | Language-definition contract, detection, themes, folding, comments |
| macros.md | MAC | Record, replay, repeat, save macros |
| session.md | SES | Session restore, unsaved buffers, periodic snapshots, backups on save |
| view.md | VEW | Zoom, word wrap, whitespace, margins, status bar |
| commands.md | CMD | Command list, shortcut mapping, minimal plugin command interface |
Single user, no account, no network: everything runs on one computer (Windows, macOS, Linux).
Shortcuts are defaults and can be remapped (CMD-2); Mod = Ctrl on Windows
and Linux, Cmd on macOS; Alt = Option on macOS. Line and column numbers shown to the user start at 1.
Commands
Status: Draft · Prefix: CMD
Purpose
Every action is a named command with a remappable shortcut, and add-ons can contribute commands through one small interface.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| CMD-1 | Every menu action is a command {id (dotted, stable, e.g. "file.save"), title, category, default_shortcut?}. Ids never change between versions; renamed actions keep their id. |
menu "Save" → command file.save |
| CMD-2 | Shortcut mapping lists every command, macro and add-on command with its shortcut; the user can assign, clear, or reset one or all. Assigning a shortcut already used asks to move it (the other loses it) or cancel. Saved in the user config folder as JSON {id: shortcut} with only differences from defaults. |
assign Mod+S to another command → conflict prompt |
| CMD-3 | Shortcut syntax: modifiers Mod, Ctrl, Alt, Shift, Meta joined by + with one key; two-step chords are not supported. On macOS Mod = Cmd and Ctrl is the Control key. |
Mod+Shift+K on macOS → Cmd+Shift+K |
| CMD-4 | Settings live in one JSON file in the user config folder (per OS convention); portable mode: when a marker file portable sits beside the app, config, sessions, snapshots and macros live beside the app instead. |
marker present → no files written to the user config folder |
| CMD-5 | Add-on: a folder in the add-ons folder with manifest.json {id, name, version, api_version, entry, commands: [{id, title, shortcut?}]}. Its commands appear in an add-ons menu under its name and in the shortcut mapping as <addon id>.<command id>. |
install add-on with 2 commands → 2 menu items |
| CMD-6 | Add-on interface (api_version 1): on its command, an add-on can read the active document's text, path, language, encoding, line ending, selections and carets; replace a range or the selections (one undo step per call); open a file; create a new document with text; show a message. Nothing else of the editor is reachable. |
add-on uppercases selection → one undo restores |
| CMD-7 | Add-on events (subscribe in the manifest): document_opened, document_activated, document_saved, document_closed, each with the document's path or untitled name. |
subscribe document_saved, save → add-on receives path |
| CMD-8 | Load safety: an add-on with an unsupported api_version, invalid manifest or failing load is skipped with a message naming it; an add-on error during a command is reported and the document is left as before that call. --no-plugins disables all add-ons. |
manifest api_version 9 → skipped with message |
Out of scope
- Add-on discovery, download, update and signing; add-on UI panels, docking windows and custom lexers.
- The source's native plugin binary interface and its message list.
- Command palette (implementations may add one from CMD-1).
Decisions
- Add-ons get a narrow, versioned document API instead of full editor access: enough for text transforms, safe to keep stable across stacks.
Documents
Status: Draft · Prefix: DOC
Purpose
Open documents as tabs: creating, opening, switching, closing and reopening them, in one or two side-by-side views.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| DOC-1 | Each open document is one tab: a file path or an untitled buffer. A new untitled document (Mod+N, or double-click on empty tab bar space) is named new <n> with the lowest free n ≥ 1; it can be renamed without saving. |
close new 1 with new 2 open, Mod+N → new 1 |
| DOC-2 | Tab shows the file name, a modified marker while unsaved, a read-only marker, and the full path as tooltip. Middle-click or the tab's close button closes it. Tabs reorder by drag. | edit → marker shown; save → marker gone |
| DOC-3 | Open (Mod+O, multi-select, drag and drop of files onto the window, or command line) a file already open activates its tab instead of opening it again; paths compare case-insensitively on case-insensitive file systems. |
open same file twice → one tab |
| DOC-4 | Opening a file while the only tab is an empty, unmodified untitled document replaces that tab. | fresh start, open file → 1 tab |
| DOC-5 | Closing (Mod+W) a modified document asks save / don't save / cancel; "close all", "close all but this", "close all to the left/right" and "close all unchanged" ask once per modified document, with "save all" and "discard all" choices. Closing the last tab leaves one empty untitled document. |
close all with 3 modified → prompts offer "save all" |
| DOC-6 | Reopen closed document Mod+Shift+T reopens the most recently closed file at its last caret position, repeatable through the last 20 closed. Recent files list: 10 entries by default (0..30), newest first, missing files removed when chosen with a message. |
close 2 files, Mod+Shift+T twice → both back |
| DOC-7 | Switch documents: Ctrl+Tab/Ctrl+Shift+Tab in most-recently-used order (setting, default on) or tab order, Ctrl+PgDn/Ctrl+PgUp in tab order. |
MRU on: A, B, C used in that order, Ctrl+Tab from C → B |
| DOC-8 | Read-only: a file the OS marks read-only, or a document toggled read-only by the user, refuses edits with a short notice; saving such a file asks for "save as". | toggle read-only, type → text unchanged |
| DOC-9 | Two views: a document can be moved or cloned to the second view (side by side or stacked, setting). A clone shares the same buffer: edits appear in both, caret and scroll are separate. Optional synchronised vertical and horizontal scrolling. | clone, type in view 1 → visible in view 2 |
| DOC-10 | Single instance by default: launching again forwards its files and options to the running instance and exits; --new-instance starts another one. |
open file from file manager → opens as a tab in the running window |
| DOC-11 | Command line: [files…] --line <n> --column <n> --language <id> --read-only --no-session --new-instance --no-plugins; --line/--column place the caret in each given file; unknown options are reported and ignored. |
--line 120 big.txt → caret on line 120 |
| DOC-12 | Document actions: copy full path, copy file name, open containing folder in the OS file manager, rename file on disk (renames an open file and keeps its tab), delete file (to the OS trash after confirmation). | rename a.txt → b.txt → tab shows b.txt, undo history kept |
Out of scope
- Project or workspace panels, folder tree side panel, document list panel.
- Printing and print preview.
Decisions
- File deletion only to the OS trash: never a permanent delete from the editor.
- MRU switching default on: it is the common desktop behaviour on all three OSs.
Editing
Status: Draft · Prefix: EDT
Purpose
Changing text fast: undo, several carets, rectangular selections, number columns, line and case operations, indentation, comments and bookmarks.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| EDT-1 | Undo Mod+Z, redo Mod+Y and Mod+Shift+Z, limited only by memory. Consecutive typed characters group into one step until the caret moves or 1 s passes. Saving keeps the history; undoing back to the saved state clears the modified marker. |
type, save, type, undo → not modified |
| EDT-2 | Multiple carets: Mod+click adds a caret (setting, default on); Mod+D adds the next occurrence of the selection (or of the word at the caret) as a new selection; Esc returns to one caret. Typing, deleting, moving and selecting apply at every caret. |
3 carets, type x → 3 x inserted |
| EDT-3 | Paste with N carets when the clipboard holds exactly N lines puts one line at each caret, in caret order; otherwise the whole clipboard at each caret. | 3 carets, paste a\nb\nc → a, b, c |
| EDT-4 | Rectangular selection: Alt+drag or Alt+Shift+arrows. Typing or pasting replaces the block on every line; past a line's end, spaces are added as needed (virtual space). Copying a block puts one line per row on the clipboard and marks it rectangular, so pasting re-creates a block. |
select 3-line block, type # → # on 3 lines at same column |
| EDT-5 | Column editor (on a rectangular selection or the caret column through the following lines): insert text, or numbers with initial, step, repeat (each value N times), base (dec, hex, oct, bin) and pad (none, zeros, spaces) to the widest value. |
5 lines, initial 8, step 1, pad zeros → 08..12 |
| EDT-6 | Line commands on the selected lines or the caret line: duplicate Mod+Shift+D, delete Mod+Shift+K, move up Alt+Up, move down Alt+Down, join (with one space), split at the edge column. |
caret line 3, Alt+Up → becomes line 2 |
| EDT-7 | Sort lines (selection or whole document): ascending or descending; text (code-point order), text_ignore_case, locale, integer, decimal (both use the first number on the line; lines without one sort first); stable. |
sort integer 10, 9, a → a, 9, 10 |
| EDT-8 | Clean-up commands: remove duplicate lines (all or consecutive only, keeping the first), remove empty lines (or also whitespace-only), trim trailing, trim leading, trim both, tabs→spaces and spaces→tabs (leading only or all) using the tab width. Each is one undo step. | a, b, a remove duplicates → a, b |
| EDT-9 | Case: upper Mod+Shift+U, lower Mod+U, title case, sentence case, invert; Unicode-aware. |
džemal title → Džemal |
| EDT-10 | Indentation per language: tab_width (default 4, 1..16) and use_spaces (default off). Enter keeps the previous line's indentation (setting, default on). Tab/Shift+Tab on a multi-line selection indents or outdents every selected line. |
select 3 lines, Tab → each starts with one tab |
| EDT-11 | Comments from the language definition (LNG-1): toggle line comment Mod+/ (adds when any selected line lacks it, else removes); block comment Mod+Shift+/ wraps the selection; languages without the needed form disable the command. |
toggle on 2 Python lines → both start with # |
| EDT-12 | Matching pairs: the bracket at or before the caret and its partner are highlighted (default on); Mod+B jumps to the partner. Auto-closing of ( [ { " ' (each optional, default off) inserts the closer and types over it when typed next. |
caret after ( → its ) highlighted |
| EDT-13 | Word completion: after completion_min_chars (default 3, 1..9) word characters, a list offers matching words from the document and the language's keywords, sorted alphabetically; Enter or Tab accepts, Esc closes. Setting off by default for large files (FIL-12). |
type fun in a JS file → function offered |
| EDT-14 | Bookmarks: toggle Mod+F2, next F2, previous Shift+F2, clear all; shown in a margin; they move with their lines and are removed with them. Commands to cut, copy, delete or keep only bookmarked lines. |
bookmark line 5, insert 2 lines above → bookmark on line 7 |
| EDT-15 | Go to line Mod+G: a line number, or a character offset when chosen; out-of-range values go to the last line. Overwrite mode toggles with Insert. |
Mod+G 9999 in a 100-line file → line 100 |
Out of scope
- Code intelligence: symbol lists, go to definition, call tips, linting.
- Spell checking; snippets.
Decisions
Mod+Dadds the next occurrence and duplicate moves toMod+Shift+D: the multi-caret binding is what users of current editors expect; the source uses other keys.- Delete line is
Mod+Shift+Kto avoid clashing with select-all-occurrence bindings.
Files
Status: Draft · Prefix: FIL
Purpose
Read and write files exactly: encodings, byte order marks, line endings, detection, explicit conversion, very large or binary files, and changes made by other programs.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| FIL-1 | Encodings: utf-8 (no BOM), utf-8-bom, utf-16le and utf-16be (with BOM; also read without), and legacy code pages, at least windows-1250..1258, ISO-8859-1..16 (not 12), KOI8-R, KOI8-U, IBM437, IBM850, Mac Roman, Shift_JIS, EUC-JP, GBK, GB18030, Big5, EUC-KR, TIS-620. Names follow the WHATWG/IANA labels. |
open each test file → text shown correctly |
| FIL-2 | Round trip: opening and saving without edits writes the same bytes: same encoding, BOM, line endings (mixed ones too) and trailing newline or none. | open + save 50 sample files → all byte-identical |
| FIL-3 | Detection order on open: BOM (UTF-8, UTF-16 LE/BE) → UTF-16 without BOM when, in the first 4 KiB, ≥ 40 % of bytes at one parity are 0x00 and none at the other → an encoding declared in the first 1 KiB (<?xml … encoding=…?>, <meta charset=…>) → valid UTF-8 (pure ASCII counts) → statistical guess (setting, default on) → the fallback_encoding setting (default windows-1252). |
UTF-8 file without BOM containing é → utf-8 |
| FIL-4 | Bytes invalid in the document's encoding are shown as visible hex markers (e.g. xE9), kept byte for byte when saving, and counted in the status bar tooltip. |
UTF-8 file with one stray 0xE9 → marker shown, saved unchanged |
| FIL-5 | "Reinterpret as " reloads the file bytes with that encoding, changes nothing on disk and does not mark the document modified; on a modified document it first asks to discard unsaved changes. | reinterpret windows-1251 file as KOI8-R → text changes, not modified |
| FIL-6 | "Convert to " keeps the text, changes the encoding used on the next save and marks the document modified. Characters the target cannot represent are listed (first 20 with line:column) and the conversion asks to proceed (they become ?) or cancel. |
convert € text to ISO-8859-1 → warning names line:column |
| FIL-7 | New documents use default_encoding (default utf-8). A setting also applies it to opened pure-ASCII files (default on), so typed non-ASCII text saves as UTF-8. |
new doc, type ü, save → bytes C3 BC |
| FIL-8 | Line endings crlf, lf, cr. A document's type = the first line ending in the file; a file with none uses default_eol (default: CRLF on Windows, LF elsewhere). Enter inserts the document's type; pasted text is normalised to it (setting, default on). |
LF file, press Enter → 0A inserted |
| FIL-9 | "Convert line endings to " rewrites every line ending, mixed ones included, as one undo step, and marks the document modified. | file with CRLF and LF lines → all LF after convert |
| FIL-10 | Saving (Mod+S) writes to a temporary file in the same folder and replaces the original, keeping its permissions; where that is not possible (no rights to the folder) it writes in place. Failure leaves the old file intact and the document modified. "Save as" Mod+Alt+S, "save a copy as", "save all" Mod+Shift+S. |
disk full during save → original unchanged |
| FIL-11 | Saving to a location that needs higher OS rights tells the user and offers "save as"; the editor never elevates itself silently. | save to a protected folder → message, no write |
| FIL-12 | Large files: at or above large_file_size (default 200 MiB, settable 1..4096 MiB) the document opens without syntax styling, word wrap, matching-pair highlight, auto-completion, occurrence highlight or clickable links, each re-enableable per setting. Opening shows progress with cancel. Files over 2 GiB ask to confirm first. |
open 300 MiB log → opens plain, scrolling stays smooth |
| FIL-13 | A file with a 0x00 byte in its first 8 KiB that is not UTF-16 opens as text with visible markers for control bytes and saves byte for byte. | open small binary, save → identical |
| FIL-14 | External changes: on window focus and through a file watcher, each open file is checked (size, modification time). Setting on_external_change: ask (default), reload (silent when the document has no unsaved changes, else ask), off; plus follow_end (after reload, scroll to the end). The prompt appears when that document is active. |
ask, another app appends a line → prompt on focus; yes → line shown |
| FIL-15 | Reload keeps caret line, scroll position and bookmarks where those lines still exist, and adds an undo point so the reload can be undone. | reload after append, Mod+Z → previous text |
| FIL-16 | A file deleted or moved by another program: the prompt offers keep (document stays open, marked modified so it can be saved again) or close. | delete open file outside → prompt; keep → modified marker |
Out of scope
- Remote files (SFTP, cloud); file locking against other programs.
- Hex editing view.
Decisions
- Pure-ASCII files are treated as UTF-8 by default (the source treats them as the system code page): typed non-ASCII text then saves portably on every OS.
- Fallback legacy code page is a setting instead of the OS code page: macOS and Linux have no system "ANSI" code page.
- Mixed line endings are preserved until the user converts; the document type is only for new lines.
Languages
Status: Draft · Prefix: LNG
Purpose
Languages are data: a language-definition file says how to colour, comment and fold a kind of file, and a theme says how each kind of token looks.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| LNG-1 | Language definition (JSON): {id, name, extensions[], first_line[] (regex), case_sensitive, line_comment, block_comment {open, close}, keyword_groups (≤ 8 lists of words), operators[], delimiters (≤ 8: {open, close, escape}), numbers {prefixes[], suffixes[], extra_chars[], range_separator}, folds {code: [{open, middle?, close}] (≤ 3), comment: [{open, close}]}}. Every field but id and name is optional. |
load a minimal {id, name} → valid, everything plain |
| LNG-2 | Tokens the highlighter emits: default, comment, comment_line, number, keyword1..keyword8, operator, delimiter1..delimiter8, fold_open, fold_close. Built-in languages may add their own token classes, each falling back to one of these for themes that do not name it. |
theme without string → strings shown as delimiter1 |
| LNG-3 | Matching rules: keywords match whole words, ignoring case unless case_sensitive; a keyword ending in * matches any word starting with it; delimiters span lines until their close and honour their escape character; comments and delimiters win over keywords inside them. |
"a\"b" with escape \ → one delimiter token |
| LNG-4 | The editor ships definitions for at least: plain text, C, C++, C#, Java, JavaScript, TypeScript, JSON, Python, Ruby, PHP, Go, Rust, Shell, PowerShell, Batch, SQL, HTML, XML, CSS, Markdown, YAML, INI, Makefile, Lua, Perl, Diff. Users add or override definitions by dropping files into a user folder; a user file with a built-in id replaces it. |
user file id: python → used instead of built-in |
| LNG-5 | Language of a document, first match: user choice for that document (menu or --language) → extension (case-insensitive; user definitions before built-ins) → first line: #! interpreter name (python3 → python), <?xml → xml, <!DOCTYPE html/<html → html, <?php → php, then each definition's first_line patterns → plain text. Saving with a new extension re-detects unless the user chose. |
script with #!/usr/bin/env bash → shell |
| LNG-6 | Theme (JSON): {name, dark: bool, default: style, styles: {token: style}, ui: {caret, selection, current_line, margin, fold_margin, whitespace, edge, marks}} where style = {fg, bg, bold, italic, underline, font?, size?} with colours #RRGGBB; missing fields inherit from default. Ships a light and a dark theme; the default follows the OS appearance. |
OS switches to dark → dark theme applied |
| LNG-7 | Highlighting is incremental: the visible range is styled first; after an edit only from the edited line onwards until the styling state matches the previous run; typing in a 10 MB file stays under 16 ms per keystroke. | type in 10 MB C file → no visible lag |
| LNG-8 | Folding: a margin shows fold points from folds (and from indentation for languages that set fold_by_indent); fold/unfold current Mod+Alt+[/Mod+Alt+], fold all Alt+0, unfold all Alt+Shift+0, fold level n Alt+<n> (1..8). Editing inside a folded block unfolds it. |
fold all in a 3-function file → 3 folded blocks |
| LNG-9 | A language editor lets users create, copy, rename, delete, import and export definitions (LNG-1 files) and preview them on sample text; export then import gives an identical definition. | export, delete, import → identical JSON |
| LNG-10 | A broken definition or theme file (invalid JSON, unknown field type, > 8 groups) is skipped with a message naming the file and problem; the rest load. | malformed file → message, other languages fine |
Out of scope
- Language servers, semantic highlighting, code outline panel.
- The source's per-language lexer internals; grammar formats from other editors.
Decisions
- One JSON contract for user and built-in languages: the source has a compiled lexer per language and a separate user format; here built-ins may still use richer engines but must expose the same token names.
- Theme follows OS appearance by default on all three OSs.
Macros
Status: Draft · Prefix: MAC
Purpose
Record a sequence of edits once and replay it, a set number of times or down to the end of the document, and keep useful macros with names and shortcuts.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| MAC-1 | Start and stop recording Mod+Shift+R; a recording indicator shows while active. Recording a new macro replaces the current unsaved one. |
record → indicator; stop → indicator gone |
| MAC-2 | Recorded steps: typed text, caret moves and selections by keyboard, editing commands (EDT-6..EDT-9, clipboard, indent, comment), and find or replace runs with all their options. Mouse clicks, dialogs that open files, and settings changes are not recorded; the user is told when recording stops which were skipped. | record with a mouse click → "1 action not recorded" |
| MAC-3 | Play Mod+Shift+P runs the current macro once at the active caret. |
record Home, #, Down; play → next line commented |
| MAC-4 | Run multiple: N times (1..100000) or until the end of the document; "until end" stops when a run reaches the last line, when the caret did not advance during a run, or when a recorded find finds nothing. | 10-line file, macro edits and moves down → stops after line 10 |
| MAC-5 | A whole playback (all repetitions) is one undo step. | run 50 times, Mod+Z once → document as before |
| MAC-6 | Save current macro with a name (unique, 1..64 chars) and optional shortcut (CMD-2 conflict rules); saved macros appear in a macro menu, survive restarts, and can be renamed, re-bound or deleted. | save "prefix", restart → listed and runnable |
| MAC-7 | Macro file (JSON, in the user config folder): [{name, shortcut?, steps: [{type: "command", id} | {type: "text", value} | {type: "find", mode, find, replace?, options, action: "next"|"replace"|"replace_all"}]}]. Unknown step types make that macro unavailable with a message, others still load. |
hand-edit a bad step → that macro disabled, others work |
| MAC-8 | Playback with multiple carets or a rectangular selection applies each step at every caret as typing would (EDT-2). | 3 carets, play End, ; → 3 lines end in ; |
Out of scope
- A scripting language; editing macro steps inside the app (the file can be edited).
Decisions
- One undo step per playback: replay is a single user intention; the source undoes step by step.
Search
Status: Draft · Prefix: FND
Purpose
Find and replace in the current document, in all open documents, or across a folder tree, as plain text, with escape sequences, or with regular expressions.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| FND-1 | Find Mod+F, replace Mod+H (Mod+Alt+F on macOS), find in files Mod+Shift+F; next F3/Enter, previous Shift+F3. The search field starts with the selection (if on one line) or the word at the caret. The last 10 search and replace strings are remembered across restarts. |
select foo, Mod+F → field shows foo |
| FND-2 | Modes: normal (literal), extended, regex. Options: match_case (default off), whole_word (not in regex), wrap (default on), direction up/down, in_selection, and in regex dot_matches_newline (default off). |
whole word cat in concat cat → second only |
| FND-3 | Extended mode escapes in both fields: \n \r \t \0 \\, \xHH, \uHHHH, \oOOO (octal), \dDDD (decimal), \bBBBBBBBB (binary byte). An invalid escape is taken literally. |
find \t extended → matches a tab |
| FND-4 | Regex flavour: Perl-compatible on Unicode text: classes, \d \w \s \b, Unicode properties \p{L}, lazy and possessive quantifiers, numbered and named groups, backreferences, lookahead and lookbehind, ^/$ at line starts and ends, \A \z document bounds, \R any line break; matches may span lines. Invalid pattern → message with the error position; nothing replaced. |
(?<y>\d{4})-\d\d finds 2024-05 |
| FND-5 | Regex replacement: $0/$&, $1..$99, ${name}, \1..\9; case changes \U \L (until \E), \u \l (next char); \n \t escapes; $$ for a literal $. |
(\w+) → \U$1 on ab → AB |
| FND-6 | An empty match moves the search one character on so it never loops; "replace all" with an empty-matching pattern inserts once per position. | regex ^ replace all with > → every line prefixed once |
| FND-7 | Replace all in the document (or selection) is one undo step and reports the count; "replace all in all open documents" reports per document and is one undo step per document. | replace all 12 → status "12 replaced"; undo → all back |
| FND-8 | Count reports the number of matches without moving the caret. Mark highlights every match with a mark style, optionally bookmarks their lines, optionally clearing previous marks first; "clear marks" removes them. | mark TODO with bookmark → each line bookmarked |
| FND-9 | Find all in the current document or all open documents → results panel: grouped by document, each hit as line: text with matches highlighted, totals per document and overall; activating a hit opens the document and selects the match. |
find all x in 2 docs → 2 groups with counts |
| FND-10 | Find in files: folder (default the active document's folder), filters (default *.*; space- or ;-separated globs; !glob excludes files; !\name excludes folders), include subfolders (default on), include hidden (default off). Files with a 0x00 byte in their first 8 KiB (not UTF-16) are skipped. |
filters *.c !test_*.c → test files not searched |
| FND-11 | Find in files runs in the background with progress (files done/total) and cancel; results stream into the results panel in folder order; each file is read with detection from FIL-3. | cancel after 1 s → partial results kept, marked cancelled |
| FND-12 | Replace in files asks to confirm with the folder and filters, then writes each changed file keeping its encoding, BOM and line endings; files open with unsaved changes are changed in their buffer instead and left modified; read-only or failing files are listed and skipped. Reports files changed and total replacements. | 3 files, 1 read-only → 2 changed, 1 listed |
| FND-13 | Results panel: one search per block, newest on top, collapsible; lines longer than 1024 characters show a window around the match; copy selected result lines; F4/Shift+F4 go to next/previous hit. |
F4 → next hit selected in its document |
| FND-14 | Occurrence highlight: selecting a whole word highlights its other occurrences in the visible area (setting, default on; options match case and whole word). | select count → other count highlighted |
| FND-15 | No match → message in the find window or status bar, no dialog; with wrap on, a search passing the document end continues from the start and says so. |
wrap, search past end → "wrapped" shown, next match selected |
Out of scope
- Search across documents' history; fuzzy search; multi-line find field editor beyond escapes and regex.
Decisions
- Binary files are skipped in find in files: the source searches them, which mostly produces noise and risks corrupting them on replace.
- One regex flavour for every OS: results must not depend on the platform's library.
Session
Status: Draft · Prefix: SES
Purpose
Quit and come back to exactly where you were, unsaved text included, even after a crash, and keep optional copies of files before they are overwritten.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| SES-1 | With remember_session (default on), quitting stores per view: open documents in tab order, the active one, and per document: path or untitled name, caret and selections, first visible line, folded lines, bookmarks, chosen language and encoding (when user-set), read-only flag and zoom. Starting restores it. |
5 tabs, caret line 40 in tab 3, quit, start → same tabs, caret line 40 |
| SES-2 | Files given on the command line open in addition to the session and become active; --no-session opens only them. Files that no longer exist are skipped, and a message lists them. |
delete one session file, start → message names it |
| SES-3 | Unsaved buffers (snapshot, default on): every snapshot_seconds (default 7, 1..3600) each modified or untitled document whose text changed since its last snapshot is written to the app's backup folder. Quitting does not ask to save; the next start restores those documents with their unsaved text, still marked modified, and untitled ones with their names. |
type in new 1, quit, start → text back, modified |
| SES-4 | After a crash or forced end, the next start restores from the last snapshots (SES-3), so at most snapshot_seconds of typing is lost. |
kill process 10 s after typing → text restored |
| SES-5 | A file changed on disk while its unsaved snapshot exists: on restore the snapshot text is shown, marked modified, and the external-change prompt (FIL-14) offers reloading the disk version. | edit file on disk between sessions → restored snapshot + prompt |
| SES-6 | A snapshot is deleted when its document is saved, closed with "don't save", or becomes unmodified through undo; the backup folder holds nothing else. Snapshots are plain files readable only by the current user. | save → snapshot file gone |
| SES-7 | Turning snapshot off: quitting asks save / don't save / cancel for each modified document again (DOC-5). |
setting off, quit with edits → prompt |
| SES-8 | Backup on save, setting backup: none (default), simple (copy the old file to <name>.bak next to it, overwriting), timestamped (copy into the backup folder or a chosen folder as <name>.<YYYY-MM-DD_HHmmss>.bak). A failed backup aborts the save with a message. |
simple, save twice → one .bak with the version before the last save |
| SES-9 | Named sessions: save the current set of documents as a session file (JSON, same data as SES-1 without unsaved text) and load one later, replacing or adding to open documents (choice). | save session, close all, load → same tabs |
Out of scope
- Syncing sessions between computers; per-project sessions tied to folders.
Decisions
- Snapshot on by default: losing typed text is worse than a quit that does not ask.
- Snapshots are separate from backup-on-save: one protects unsaved text, the other protects the previous saved file.
View
Status: Draft · Prefix: VEW
Purpose
How text is shown: zoom, word wrap, invisible characters, margins, guides and the status bar.
Requirements
| ID | Requirement | Verified by |
|---|---|---|
| VEW-1 | Zoom per view: level −10..+20 added to the base font size in points (minimum shown size 2 pt); Mod+= / Mod+- one step, Mod+0 resets to 0, Mod+wheel steps; the level shows in the status bar while not 0. Optional sync of both views (default off). |
Mod+= 3 times → level +3 |
| VEW-2 | Word wrap toggle (Alt+Z, default off) wraps at the view's width on word boundaries (or any character when a word is wider); wrapped parts may keep the first part's indentation (setting wrap_indent, default on). Line numbers and Mod+G count file lines, not wrapped rows. |
200-char line, wrap on → several rows, one line number |
| VEW-3 | Show whitespace (spaces as dots, tabs as arrows), show line endings (CR, LF, CRLF markers), and show all; each a toggle, default off. Control and invalid bytes are always visible (FIL-4). |
show line endings in a mixed file → both marker kinds |
| VEW-4 | Margins: line numbers (default on, width grows with line count), bookmarks, folding. Indent guides (default on) at each tab stop within leading whitespace. Edge column (default off, column 80, setting 1..1000) as a line or background colour. Current line highlight (default on). | 10,000-line file → numbers fully visible |
| VEW-5 | Status bar: language name, document length in characters and line count, Ln, Col (column counts tabs to the next tab stop), Sel (characters and lines; with several selections, their totals and count), line ending type, encoding, insert or overwrite. Clicking the line ending or encoding opens the convert or reinterpret menu. |
select 2 lines of 5 chars → Sel: 11 | 2 with LF |
| VEW-6 | Full screen F11 hides menus and toolbars; distraction-free mode also hides tabs and margins and centres the text at 80 characters wide (setting 40..400). Esc or the same key leaves. |
F11 → only text and scrollbars |
| VEW-7 | Clickable links: URLs (http, https, ftp, file, mailto) are underlined and open with Mod+click (or double-click, setting) in the OS handler. Default on, off for large files. |
Mod+click https://example.org → OS browser opens |
| VEW-8 | Base font, size, line spacing and caret width and blink rate are settings; caret position and scroll are never changed by changing them. | change font size → caret on same character |
Out of scope
- Document map, minimap, function list panels; tab bar themes; toolbar customisation.
Decisions
- Column in the status bar counts visual columns with tab expansion, not characters: it matches what an edge column at 80 shows.
Alt+Zfor word wrap, matching another common editor; the source has no default key.
From Specfork/text-editor at 720f295, licence CC-BY-4.0.