Skip to content

A tour

Every screen Textchum has, in the order you would meet them, on both shells: the macOS app on the left and the GTK one on the right. The screenshots come from a small fictional project — Harbor, a port broker that exists only so these pictures have something honest to show — and each follows your own light or dark setting.

The two shells are the same editor over the same core, so the pictures mostly differ in what the platform contributes: window furniture, where a panel is drawn, and which font the system hands over.

The window

One document per window, tabs by default, and a navigator drawer with two halves: the open buffers grouped by project on top, that project's file tree below.

The editor window: sidebar with open buffers and the project tree, a Rust file with syntax highlighting and line numbers (macOS) The editor window: sidebar with open buffers and the project tree, a Rust file with syntax highlighting and line numbers (macOS)

macOS

The editor window: sidebar with open buffers and the project tree, a Rust file with syntax highlighting and line numbers (Linux) The editor window: sidebar with open buffers and the project tree, a Rust file with syntax highlighting and line numbers (Linux)

Linux

The title bar carries the facts about the document — encoding, size, language, and the problem count once a language server has an opinion. The tree follows along: switching tabs expands the path to the current file and highlights it, and a file saved, made or removed in a folder it shows — by the editor, a refactoring or a command in a terminal — appears or goes on its own.

Right-clicking in the text opens the editor's own menu: Jump to Definition, Find References, Rename Symbol, the diagnostics of the line, Blame Line, Format Document and File Properties, beside cut, copy, paste and the spelling suggestions. Those commands act on the character that was clicked rather than on the caret, which the click leaves where it was. What the document has no use for is not there — no server running means no Find References, no findings means no diagnostics rows.

Folding

Fold (⌘[, Ctrl+[ on Linux) closes the block that opens on the caret's line; Fold All (⌥⌘[, Ctrl+Alt+[) closes every block that is not inside one already closed, and Unfold All (⌘], Ctrl+]) opens them again. A closed block shows its opening line with an ellipsis after it.

The blocks come from the same tree the colouring uses, and the folds belong to the document: closing a function in one view closes it in every view of that file.

Columns

A window is a row of columns. A column shows one file at a time and holds one or more views of it, stacked.

New Column (⌘\, Ctrl+\ on Linux) puts a column beside this one, showing the same file until it is given another; Close Column (⇧⌘\, Ctrl+Shift+\) takes one away. Second View (⌥⌘\, Ctrl+Alt+\) stacks another view of the column's file under the first, and Close View (⇧⌥⌘\, Ctrl+Alt+Shift+\) removes it. Next Pane (⌥⌘, Ctrl+Alt+) moves the keyboard through them.

Each view scrolls on its own, which is the point: reading the top of a file while editing the bottom of it. A column owns the file it shows, so changing its tab moves every view in it to the new file.

Both sides are one document. There is one history and one save, so an edit on either side is the same edit, and neither view can be a stale copy of the other. Both toolkits are built for this — a text buffer that several views share — and what does not come free is the colouring, since on macOS that lives on the layout rather than the text; each view gets painted.

Language servers

Diagnostics arrive as tinted marks in the text and a count in the title bar. Nothing about the editor waits on the server: it attaches when it can, and says so when it cannot.

A warning from the language server marked in the text, counted in the title bar (macOS) A warning from the language server marked in the text, counted in the title bar (macOS)

macOS

A warning from the language server marked in the text, counted in the title bar (Linux) A warning from the language server marked in the text, counted in the title bar (Linux)

Linux

The gutter says the same: a line with a finding has its number in the severity's colour — red, orange, blue — with a disc beside it for an error or a warning and a ring for information and hints. The code in a server's documentation — a signature, a definition — is coloured the way the editor colours that language, and the editor's context menu shows the same keys the main menu does for every command.

Resting the pointer on a symbol shows the server's documentation, with the Markdown it sends rendered — code blocks monospaced, emphasis styled. ⌃⌘H asks for the symbol under the caret instead, which works with mouse hover switched off.

Hover documentation over a function, showing a rendered signature and prose (macOS) Hover documentation over a function, showing a rendered signature and prose (macOS)

macOS

Hover documentation over a function, showing a rendered signature and prose (Linux) Hover documentation over a function, showing a rendered signature and prose (Linux)

Linux

The same documentation can live in a panel instead of a bubble. View ▸ Info Panel (⌥⌘I) docks one under the editor, the way a second view stacks in the column; while it is up the bubble stays away, and the panel shows the documentation for the symbol under the pointer, and for the symbol at the caret as the caret moves. Its second face lists the document's diagnostics — line, kind, message — and a click goes to the line. The panel follows the focused file across tabs and columns.

Go ▸ Jump History… lists the trail Go Back would walk, most recent first, with the forward trail beneath when there is one, in the same list Find References uses; choosing a place goes there, as a jump of its own. Both lists show each line in its language's colours.

A folder opens as a project: File ▸ Open takes a directory, and so does chum path/to/folder. A path that does not exist yet — chum notes/new.go — opens a new document named after it, treated as the language its name says, and Save writes it there without asking. When a tab of that project is open somewhere, that tab comes to the front; otherwise a new window opens with the project in its tree and no file yet, and it stays open when its last tab closes. The status bar says the project's git branch, read when the file is shown and again when git rewrites HEAD — the editor watches that file rather than asking on a timer. After it come the breadcrumbs: the named constructs the caret is in, outermost first — the impl and the function, the class and the method — read from the syntax tree, so they need no language server. Each is a menu of what is beside it, the one it names marked; choosing one goes there, as a jump Go Back returns from. Text no grammar reads has none. Its right end is the notification area: the last thing the editor had to say — a language server it could not find, and what would install it; a grammar it could not load; a save that went ahead without its preprocessors — as much of it as fits, the whole of it as a tooltip. Nothing there interrupts: what used to be a dialog to dismiss is a line here. A click lists the session's notices, newest first with the time each was said, selectable, since what was said is often a command to run. Not there yet: the list does not outlive the session, and nothing clears it. Go ▸ Worktrees… lists the repository's working trees: choose one to switch this window to it — its files close and the ones the other tree also has reopen there, and the language servers start in the new root — or to open it as a project of its own. With modified files in the current tree, or unsaved ones in the window, only opening is offered; a tree that is open already just comes to the front. A file git hands the editor — a commit message, a rebase todo — starts at the top every time.

Find and Replace (⌥⌘F) docks a bar above the text with the pattern, the replacement and three switches: regular expression, match case, whole word. The expressions are Vim's — \( \) groups, \|, \+, \?, \{n,m}, \< \>, \v and \V, \c — and the replacement takes \1…\9, & or \0 for the match, \n, and \u \l \U \L \E for case. ⏎ in the pattern goes to the next match, ⇧⏎ to the previous, ⏎ in the replacement replaces the current one and moves on; All replaces every match as one undo step. ⎋, in the bar or in the text, puts the bar and its marks away and leaves the keyboard in the text.

Go to Line (⌘L, Ctrl+L on Linux) takes a number, or the whole src/main.rs:412:8 pasted straight out of a build log — the file name and the trailing noise are ignored, the line lands a third of the way down the view, and Go Back returns to where reading was interrupted.

A change bar runs down the left of the gutter, saying which lines differ from the file as it stands in git: a green stripe for a line that is new, blue for one that reads differently, and a red wedge on the boundary where lines were deleted — deleted lines occupy no height, so a stripe would have nothing to cover. It follows the buffer rather than the file on disk, so it is right before you save, and it is recomputed once typing settles. A file with no committed version, or one outside a repository, gets no marks rather than every line claimed as new.

The bar compares against the last commit unless told otherwise. Deep in a feature branch that answer goes quiet — everything committed on the branch counts as unchanged — so editor.git_marks: "branch" compares against the commit the branch grew from instead, and the bar shows everything the branch touches. When git does not name a default branch, editor.merge_base_branches lists the names to try, most likely first; both settings can be overridden per project. Go → Changed in Branch (⌃⌘T, Ctrl+Alt+G on Linux) lists the branch's files — the pull request's files, read from git alone — behind the same fuzzy filter as Open Quickly.

Blame Line (⌃⌘B, Ctrl+Alt+B on Linux) asks git who last touched the line under the caret: the commit, the author and when they wrote it, the subject and the message body — where the reasoning usually is — and the file's name at the time if it has been renamed since. The commit is one button away from the clipboard, which is most of what the answer is for. A line typed since the last commit says so rather than borrowing somebody else's.

It asks with the buffer's text, not the file on disk, so an unsaved edit above the caret cannot quietly shift the answer onto the neighbouring line.

In a line's leading whitespace, two keys mean something more than usual. Backspace deletes back to the previous tab stop rather than one space at a time, and Tab lines the line up with the nearest non-blank line above it — pressing it again, once already level, goes one level deeper. Anywhere else in the line both keys are themselves: it is the position that decides, not a mode, which is what keeps them from surprising anyone. A tab-indented line is left to its tab character, which is already one press per level.

With text selected, typing an opening bracket or quote — (, [, {, ', ", ` — wraps the selection in the pair instead of replacing it. What was wrapped stays selected, so pressing another one wraps that in turn: [, ( and { over hello give [({hello})]. Typing anything else replaces the selection as it always did.

With nothing selected, Close brackets and quotes as they are typed (Settings ▸ General, off by default) brings the closing half along — ( gives () with the caret between — and the ) typed afterwards steps over the one already there rather than adding a second; Backspace between an empty pair takes both. Brackets pair everywhere; quotes follow the language: ' is an apostrophe in prose and a lifetime in Rust, so it is left alone there, and a backtick pairs only where it means code. No pair opens into a word, and no quote opens right after one. A language the rule does not know, or one where it gets a quote wrong for you, takes its own table in the file: editor.pairs in configuration.

The thin bar under the editor answers what a look at the text cannot: where the caret is, whether the file indents with tabs or spaces and by how much, what language it is treated as, and its encoding. The indentation and the language are clickable and open File Properties, where those choices are made.

Scrolled deep into a long body, the first line of each enclosing construct stays pinned at the top of the view — the class line and the def line while a Python method scrolls — and clicking a pin goes to it. The pins cost rows, so editor.context_lines: false (or the switch in Settings) turns them off.

The caret's line carries a faint tint, so the caret can be found in a long file at a glance. Right-clicking in the text — or File → Copy Path — offers the caret's line in the shapes that get pasted: Path and Line (path:line, which a terminal opens straight to) and Forge URL for Line, the file's forge URL with the line fragment spelled the way that forge does.

Word movement (⌥→/⌥←, Ctrl+arrows on Linux, and their selecting and deleting forms) stops at every change of character class — identifier, symbol, blank — and at a line break, so a run like )} is one stop and the caret never skips across lines. A quote belongs to the word beside it, so "hello" is one stop, and ., , and ; are passed over like blanks — they separate things rather than being things — so a.b is two stops with the dot skipped. A closing bracket typed first on its line takes the indentation of the line that opened it.

Completions appear as you type after identifier characters and .; ↑/↓ choose, ⏎ or ⇥ accept, ⎋ dismisses. A snippet arrives with its first placeholder selected, so typing replaces it; ⇥ moves to the next placeholder and ⇧⇥ back, and one written twice mirrors as you type. The last ⇥ leaves the caret where the snippet asked for it and hands the keys back.

The completion popup listing members with their types (macOS) The completion popup listing members with their types (macOS)

macOS

The completion popup listing members with their types (Linux) The completion popup listing members with their types (Linux)

Linux

⇧⌘O lists the file's symbols, filterable from the keyboard.

The document outline panel, listing a struct and its methods (macOS) The document outline panel, listing a struct and its methods (macOS)

macOS

The document outline panel, listing a struct and its methods (Linux) The document outline panel, listing a struct and its methods (Linux)

Linux

View ▸ Language Server Status answers "is my server alive?" — what runs where, and the session's recent transitions, refreshed live.

The language server status panel listing one running instance and its status transitions (macOS) The language server status panel listing one running instance and its status transitions (macOS)

macOS

The language server status panel listing one running instance and its status transitions (Linux) The language server status panel listing one running instance and its status transitions (Linux)

Linux

Finding things

⌘T opens files by fuzzy name within the project. The scope is walked once and matched in memory, so typing stays instant; the status strip says how many of how many files matched, and which keys do what — ⏎ opens the selected row once the rows answer the query, and searches while one is still owed; ⌘⏎ opens regardless — so refining a query never opens a file by accident.

Open Quickly: a fuzzy query, one matching path, and the status strip naming the keys (macOS) Open Quickly: a fuzzy query, one matching path, and the status strip naming the keys (macOS)

macOS

Open Quickly: a fuzzy query, one matching path, and the status strip naming the keys (Linux) Open Quickly: a fuzzy query, one matching path, and the status strip naming the keys (Linux)

Linux

⇧⌘F searches contents for the text as typed — or for a regular expression, with the .* toggle beside the query — with stacked filters that refine the results by line text or file path. The status line always says what the search did — matches, files searched, or why nothing was read.

Find in Project: regex results with a file filter applied (macOS) Find in Project: regex results with a file filter applied (macOS)

macOS

Find in Project: regex results with a file filter applied (Linux) Find in Project: regex results with a file filter applied (Linux)

Linux

⇧⌘P is the command palette: every menu action, fuzzy-searchable, with its shortcut alongside.

The command palette listing menu actions and their shortcuts (macOS) The command palette listing menu actions and their shortcuts (macOS)

macOS

The command palette listing menu actions and their shortcuts (Linux) The command palette listing menu actions and their shortcuts (Linux)

Linux

Running commands

Run → Run Command… (⌃⌥R, Shift+F10 on Linux) runs a command line through the shell in the project of the file in front — cargo test, go build ./..., pytest -x — and shows its output in a panel under the editor as it comes. The line offered is the last one run in that project; Run Again (⌘R, Ctrl+F5 on Linux) runs the panel's command once more, and Stop (⌘F2, Ctrl+F2 on Linux) stops it and whatever it started. The output keeps its colours — a tool writing into a panel is asked for them, as it would be in a terminal — and a progress line redrawn in place stays one line.

Places the output names — src/main.rs:12:5, a Python traceback's File "app.py", line 3, ui.ts(4,7) — are underlined and open with a click; Next Output Place (F4) and Previous Output Place (⇧F4) walk them like findings. A command that has said nothing for a while while still running may be waiting for input: the panel says so, and a line typed in its field goes to the command's input.

What can be run in the file is marked in the gutter with a green triangle: in Rust, a #[test], a #[bench] and the main of a binary or an example; in Go, a TestX, a BenchmarkX and the main of a main package; in Python, a pytest test function or method and a module's if __name__ == "__main__":. A click on the mark runs just that one — cargo test -- tests::adds --exact, go test -v -run '^TestAdds$' ., python3 -m pytest -v tests/test_app.py::test_adds — in the panel, from the package's folder. Run → Run at Caret (⌃⇧R, Ctrl+Shift+F10 on Linux) runs the innermost one around the caret. Where the light bulb of a fix stands on the same line, the bulb is drawn and the click offers the fix.

When the output reports tests — cargo's test x ... ok, go test -v, pytest — the panel lists them beside it by suite, each marked passed, failed, skipped or still running, with the counts in its header. A test chosen in the list shows the output line that reported it, where its failure follows; Return or a double-click runs that test alone, and the field above the list filters it by name as you type. Rerun Failed in the panel, or Run → Rerun Failed Tests, runs only the ones that failed again, from the same folder: the command is the one that ran them, its own filters replaced by the failed names. Run Again also runs in the folder the run started in, so a test run from its mark runs again in its package.

Not there yet: the panel is not a terminal. A program that draws the whole screen — an editor, top, a REPL that edits its own line — does not belong in it. Marks are found for Rust, Go and Python only, and a test written under another runner's conventions is not marked. Tests are read from cargo, go test and pytest output only; without -v, go test and pytest name only what failed, so only that is listed. A command line with a pipe or && is not narrowed for Rerun Failed, and a test in the list does not open its source yet — the failure's own place in the output does.

Debugging

A click in the gutter's marks column, on a line with no other mark, puts a breakpoint there — a red disc — and a second click takes it away; Run → Toggle Breakpoint (⌘F8, Ctrl+F8 on Linux) does the same on the caret's line. Breakpoints move with the text edited above them. Run → Debug at Caret (⌃⇧D, Shift+F9 on Linux), or a ⌥-click (Alt-click on Linux) on a run mark, runs the test or the main under a debugger: debugpy for Python, Delve for Go, lldb-dap for Rust — after cargo has built the test or the program, its messages in the debug console.

The debug panel opens under the editor. When the program stops at a breakpoint, its line is tinted and the file opens there; the panel lists the stack of the thread that stopped, the variables of the frame chosen in it — a structured value opens to show what it holds — and the program's output, with a field that evaluates an expression in that frame. Continue (F9, F5 on Linux), Step Over (F8, F6 on Linux), Step Into (F7), Step Out (⇧F8, Shift+F7 on Linux) and Stop Debugging (⇧⌘F2, Ctrl+Shift+F2 on Linux) are in the Run menu and on the panel. An adapter that is not installed is named, with the command that installs it; a program that does not start says what usually holds it up — on macOS, the permission a debugger needs to take control of a program, which developer mode (DevToolsSecurity -enable) grants.

Not there yet: debugging starts only from a test or a main the run marks find, in Rust, Go and Python; the adapter for a language cannot be chosen; there is no attaching to a running program, no conditional breakpoint and no watch list; and breakpoints are not kept across restarts.

Markdown and prose

Markdown documents open with a live preview beside the text, and the prose spell checker — off until you pick a dictionary — marks misspellings in purple, distinct from diagnostics. In code it looks only at comments; identifiers are never flagged.

A Markdown document with its rendered preview beside it (macOS) A Markdown document with its rendered preview beside it (macOS)

macOS

A Markdown document with its rendered preview beside it (Linux) A Markdown document with its rendered preview beside it (Linux)

Linux

Misspellings marked in prose, with the rendered preview alongside (macOS) Misspellings marked in prose, with the rendered preview alongside (macOS)

macOS

Misspellings marked in prose, with the rendered preview alongside (Linux) Misspellings marked in prose, with the rendered preview alongside (Linux)

Linux

Settings

Settings are a plain JSON file that the window edits; the file is the escape hatch, and it is watched, so an edit in another editor applies at once.

Settings, General tab: appearance, theme, placement, font, and the editor toggles (macOS) Settings, General tab: appearance, theme, placement, font, and the editor toggles (macOS)

macOS

Settings, General tab: appearance, theme, placement, font, and the editor toggles (Linux) Settings, General tab: appearance, theme, placement, font, and the editor toggles (Linux)

Linux

Projects decides how project roots are found, what the tree hides, and which editor settings a root overrides.

Settings, Projects tab: detection toggles, hide patterns, and per-project overrides (macOS) Settings, Projects tab: detection toggles, hide patterns, and per-project overrides (macOS)

macOS

Settings, Projects tab: detection toggles, hide patterns, and per-project overrides (Linux) Settings, Projects tab: detection toggles, hide patterns, and per-project overrides (Linux)

Linux

Hidden names are glob patterns, edited one per line, with a menu that adds a named preset in one click.

The hide editor open as a popover, one pattern per line, with the Add preset menu (macOS) The hide editor open as a popover, one pattern per line, with the Add preset menu (macOS)

macOS

The hide editor open as a popover, one pattern per line, with the Add preset menu (Linux) The hide editor open as a popover, one pattern per line, with the Add preset menu (Linux)

Linux

Presets edits those named sets the same way. They start as built-ins; edit any of them and your list takes over, so a preset you delete stays deleted until you restore the built-ins. This screen and the next have no picture from the GTK shell because they are not screens there: presets sit inside Projects, and preprocessors inside Language Servers.

Settings, Presets tab: named glob sets, each editable one pattern per
line Settings, Presets tab: named glob sets, each editable one pattern per
line

Language Servers overrides which command serves a language, for every project or for one root.

Settings, Language Servers tab: default and per-project server commands (macOS) Settings, Language Servers tab: default and per-project server commands (macOS)

macOS

Settings, Language Servers tab: default and per-project server commands (Linux) Settings, Language Servers tab: default and per-project server commands (Linux)

Linux

Preprocessors runs formatters before every save: one command per line, each reading the document on standard input and writing it back on standard output.

Settings, Preprocessors tab: per-language command chains Settings, Preprocessors tab: per-language command chains

Small things

⇧⌘N starts a new document in a chosen language, filtered from the keyboard, so highlighting works before the first save.

The New with Format picker, filtering the language list (macOS) The New with Format picker, filtering the language list (macOS)

macOS

The New with Format picker, filtering the language list (Linux) The New with Format picker, filtering the language list (Linux)

Linux

And the About panel says which build you are running — a real version, even for a local build.

The About panel showing the build version, author, repository, and license (macOS) The About panel showing the build version, author, repository, and license (macOS)

macOS

The About panel showing the build version, author, repository, and license (Linux) The About panel showing the build version, author, repository, and license (Linux)

Linux