Editing
Multiple cursors
Every cursor edits at once; typing, deleting, moving and pasting apply to all of them.
- Alt+click adds a cursor; Alt+double-click adds the word under the pointer as a region; Alt+drag extends the last added region.
- Shift+Alt+drag makes a column selection: one region per line between the two corners of the box, clamped to shorter lines (a line that ends before the box's left edge is skipped, as in VS Code). Drag up or left and the regions run backwards, so typing goes where the pointer is. A zero-width box gives a caret on every line.
- Select next occurrence (
⌘D/Ctrl+D): a caret first becomes its word; each press adds the next occurrence of the last added region, wrapping around the document and never selecting the same text twice. - Select all occurrences (
⌘⇧L/Ctrl+Shift+L): every occurrence of the first region's text (the word under a caret). - Skip occurrence (
⌘K ⌘D/Ctrl+K Ctrl+D): moves the last added region to the next occurrence instead of adding it. - Undo selection takes back the last change of selection; Insert cursor above / below and cursor at end of each selected line are in the palette and the keymap.
editor.multicursor-case-sensitive makes the occurrence commands match
case regardless of the find bar's setting. The behaviour above is pinned
by the test suite in graphoos-app/src/editor/multicursor.rs.
User snippets
Your own snippets live in TOML files, one per language: Open User
Snippets in the palette opens (and creates, from a template) the file
of the active document's language under the config directory
(snippets/<language>.toml); a workspace adds its own in
.graphoos/snippets/<language>.toml, and global.toml in either place
applies to every language. Later files win on the same prefix.
[[snippets]]
prefix = "tfn"
description = "test function"
body = """
#[test]
fn ${1:name}() {
${2:assert!(true)};$0
}
"""
Type the prefix and pick the snippet in the completion list (it shows
with its description); Tab jumps between placeholders, $0 is where
the cursor ends. The body is the LSP snippet syntax the editor already
speaks: $1, ${1:default}, nested ${1:outer ${2:inner}}, choices
${1|a,b,c|} (the first is inserted). body can also be a list of lines.
Variables are expanded when the snippet is inserted: $TM_FILENAME,
$TM_FILENAME_BASE, $TM_DIRECTORY, $TM_FILEPATH, $TM_LINE_INDEX,
$TM_LINE_NUMBER, $TM_SELECTED_TEXT, $CLIPBOARD, $WORKSPACE_NAME,
$CURRENT_YEAR, $CURRENT_YEAR_SHORT, $CURRENT_MONTH, $CURRENT_DATE,
$CURRENT_HOUR, $CURRENT_MINUTE, $CURRENT_SECOND, $RANDOM,
$RANDOM_HEX, $UUID; ${VAR:default} when the variable is empty.
Transformations follow VS Code: ${TM_FILENAME/(.*)\..+$/$1/} strips
the extension, ${TM_FILENAME_BASE/(.*)/\U$1/} upper-cases, \u \l
\L change case, g and i are the flags. A transformation on a
placeholder (${1/…/…/}) is applied once, to its default text, when the
snippet is inserted.
Breadcrumbs and rulers
The bar over the editor shows the file's path and, after it, the chain of
symbols the cursor is inside (src › doc.rs › impl Doc › save), from the
language server. editor.show-bread-crumbs = false hides the bar.
editor.rulers = [80, 100] draws a vertical guide at those columns.
Refactors across files
Rename Symbol (F2) asks the language server for every place the
symbol is used. When the change touches more than the file you are in,
GraphoOS says how much it would change (12 changes in 5 files) and
offers three answers: Apply, See the changes — which opens a
Refactor preview.md tab with each file and its lines before and after —
or Cancel. A rename inside the file you are already looking at is
applied straight away.
Undo Last Refactor puts the last applied refactor back in every file it touched, in one step (each file's own undo, so it can be redone).
Organize Imports and Apply All Fixes ask the language server for
its source.organizeImports / source.fixAll action and run it through
the same preview; when the server offers nothing there, it says so
rather than doing nothing silently.
Merge conflicts
A file a merge left with <<<<<<< / ======= / >>>>>>> blocks is
resolved from the palette: Merge Conflict: Keep Current Change, Keep
Incoming Change, Keep Both Changes, and, when git wrote the common
ancestor too (merge.conflictStyle = diff3), Keep the Common
Ancestor. They act on the conflict the cursor is inside and say how
many are left in the file; Go to the Next One jumps to the next
block, wrapping. Each resolution is a normal edit, so undo puts the
block back.
Each side is painted in its own colour — this branch's in green, the incoming one in blue, the ancestor's in red — with its marker line a shade stronger, and the first line of every conflict has an icon in the margin. The icon, and a right-click anywhere inside a conflict, offer the same actions, plus:
- Compare the Two Sides: the two sides of that conflict in a diff
editor, as files named after the branches (
main (HEAD).rs,main (feature).rs). - Show What Each Side Changed: the three versions git keeps while a merge is unfinished — the common ancestor, this branch's and the incoming one, whole — opened as two diffs against the ancestor. The result is made in the file itself, with the actions above.
What the file tree tells you
Besides colouring changed files, each row shows at its right what is
going on with it: the git status letter (M modified, A added, D
deleted, R renamed) and the problems the language server found
(2 ⨯ · 1 ⚠). A folder carries the sum of what is under it, so a
collapsed tree still points at where the errors are.
Git: staging, branches, merges
The Git menu (and the palette) runs git itself, so each operation means what it means on the command line, hooks and configuration included; what it did, or what git answered, comes back as a message.
- One change at a time: right-click a changed line — Stage This Change, Unstage This Change, Discard This Change — or Stage This File / Unstage This File for the whole file. Files with something staged carry an S in the Source Control panel, and the panel says how many there are.
- Commit Staged commits what is staged, exactly as staged, with the message in the Source Control box (the Commit button still commits the ticked files whole). Amend the Last Commit redoes the last one with what is staged, and with the box's message when there is one.
- Fetch, and Sync: pull — merging when both sides moved — then push; a branch that follows nothing yet goes to the first remote and follows it from then on.
- Merge a Branch…, Rebase onto…, Cherry-Pick a Commit… pick what they need in the palette. When one stops on conflicts, the panel says it is in progress, the files in conflict are among the changes, and Continue (once they are resolved and staged) or Abort finish it.
- Rename This Branch…, Delete a Branch… (one with commits of its own is kept unless git is told to force it), Create a Tag… (a name, then a message for an annotated tag), Delete a Tag…, Push Tags, Add a Remote… (a name and a URL).
Putting changes aside
Git: Put the Changes Aside (Stash) takes everything the working tree has — including files git has never seen, which is how a stash loses a new file — and leaves the tree as it was committed. Git: What Is Put Aside lists what is there, newest first, with when it was made; picking one brings it back and takes it off the stack. Git: Bring Back the Last Thing Put Aside does the same without asking which.
Each of them says what it did. A command that empties your changes without a word is indistinguishable from one that lost them.
Git history of a file
Git: History of This File in the palette lists the commits that
touched the active file, newest first, each with its short id, author and
date; picking one opens that version as a diff against the file as it
is now. It reads the repository directly (no git process), and a file
that did not exist in that commit says so.
Local history
Every time a file is saved — and the first time it is opened — GraphoOS
keeps a copy of it outside the workspace (history/ in the data
directory). It works for files that are not in git, and for changes made
between commits.
Local History: Open a Saved Version in the palette lists the versions of the active file, newest first, each with its time, how long ago it was, how many lines it differs from what is on disk now, and its size; picking one opens it as a diff against the current file. Local History: Restore a Saved Version writes the chosen version back over the file (the current content is saved as a version first, so the restore itself can be undone), and Local History: Forget This File's Versions deletes them.
The newest 50 versions of each file are kept, and nothing older than 30
days; editor.local-history = false turns the whole thing off.
The changes as a patch
Git: Show the Changes of This File as a Patch opens what changed
against the version in git as a .diff tab: the git diff form, with
its @@ hunks and three lines of context, which can be read whole,
copied into a message or applied somewhere else. Side by side is better
for reading a change in place; a patch is better for seeing the whole of
it at once.
Diff: Show This Comparison as a Patch does the same for whatever comparison is open — against a branch, against a commit, or the two files of a folder diff — because it only needs the two sides, which the comparison already holds.
Git: Compare This File With… opens the file beside the version in any branch, tag or commit of the repository, which is how a change is read against something other than the last commit.
The file explorer
Each row carries what the file is: the git status letter (M, A, D,
R) and the number of errors and warnings in it, and a folder carries
the sum of what is under it, so the tree says where to look without
opening anything.
Dragging a file or a folder onto another folder moves it there; dropping it on a file puts it beside that file; and dropping a file on an editor group opens it there. The folder it would land in is marked while you drag, and a move that would lose the file — into itself, over something of the same name — is refused with the reason. Everything open follows the file to its new path.
Comparing two folders: Select for Compare on one folder and
Compare Folder with Selected on another lists the files that differ —
those only on one side and those whose content changed — and picking one
opens the two as a diff. What is the same is not listed, and .git,
node_modules and target are not walked into.
The right-click menu has New File, New Directory, Open, Open to the Side, Reveal in Finder (or the file manager of the system), Open in Integrated Terminal, Find in Folder…, Open Changes, Open on Remote (web), History of This File, Rename, Duplicate, Move File/Directory to Trash, Copy Path, Copy Relative Path, Select for Compare / Compare with Selected and Refresh.
Running the tests of what you clicked: Run the Tests, Debug the
Tests and Run the Tests with Coverage appear on a file or a folder
when the workspace says how — cargo test <name> (--lib for src,
--tests for tests), pytest <path>, go test ./dir/... or
npm test -- <path> —, and the run goes to the Run and Debug panel
like any other task. Coverage asks each tool in its own way
(pytest --cov, go test -cover, npm test -- --coverage,
cargo llvm-cov), so it needs that tool to be installed.
Cut, copy and paste a file or a folder with ⌘X / ⌘C / ⌘V or
from the menu: pasting puts it inside the selected folder, or beside the
selected file. A folder travels whole. Nothing is ever overwritten — a
name already taken becomes name copy, then name copy 2 — and a
folder pasted inside itself is refused instead of copying for ever.
The keyboard alone goes through the tree: the arrows move and open or
close a folder, Home and End go to the first and last row, the page
keys move ten at a time, typing a letter jumps to the next row whose
name starts with it, Enter renames what is selected, ⌘⌫ (Delete
elsewhere) moves it to the trash, ⇧F10 (or the menu key) opens the
menu of what is selected, and ⌘A selects the text of the panel so it
can be copied.
The other secondary menus
On the text: Go to Definition, Go to Type Definition, Call
Hierarchy, Find References, Go to Implementation, Rename Symbol,
Show Code Actions, Select All Current (every occurrence of what is
under the cursor), Format Document, Format Selection, Organize
Imports, Apply All Fixes, Ask the AI about the selection, Run in
Terminal, Reveal in Panel, Reveal in File Explorer, Open on
Remote, Cut / Copy / Paste, the palette, and Copy Path / Copy
Relative Path of what is being edited. On a run.toml the menu is the
short one plus Add Run/Debug Configuration.
Format Selection (⌘K ⌘F, Ctrl+K Ctrl+F) asks the language server to
format only what is selected; with nothing selected it formats the whole
document, and a server that does not offer range formatting says so
instead of leaving the editor waiting.
On an editor tab: Close, Close Other Tabs, Close All Tabs, Close Tabs to the Right / to the Left, Close Saved Tabs, Pin Tab (Unpin when it is pinned), Split Right, Split Down, Reveal in Finder — the file manager of the system — , Copy Path and Copy Relative Path.
A pinned tab shows a pin where its close button is, sits to the left of the ones that are not pinned, survives every «close the rest» there is, and is still pinned the next time the editor opens.
On a terminal tab: Copy (off when nothing is selected), Paste, Select All, Find…, Clear All, Stop / Restart when it is running a task, and Close.
Selecting and copying in the panels
The text in the panels is text: drag across a problem's message, a
search hit, a file's name or a plugin's description and ⌘C (Ctrl+C)
copies it, without having to type it out again.
⌘A in the Problems or Search panel selects every row it shows, and ⌘C then copies the lot — one line per row, each saying its file and its line, the way a compiler writes it. A panel becomes the focused one when you click in it or select text in it; clicking a problem or a match opens the file, so the focus goes with it to the editor.
The panels, with the mouse
Problems: the secondary click copies the message, or the message with its file and line the way a compiler writes it, goes to the problem, and leaves only that file's problems in the panel until you ask for all of them again.
Search: copy the line, copy it with its place, go to the match, Replace Here — which replaces that one match, undoably, and is off while the replacement box is empty — and Dismiss This Result, which takes a hit off the list without touching the file.
Source Control: open the changes as a diff, take the file into the commit or leave it out, discard its changes, reveal it in the system's file manager, and copy its path or its relative path.
Debug: on a variable, copy its name, its value or both, change what it holds, and add it to Watch.
Plugins: the row's secondary click opens the same menu as the gear — reload, enable or disable (here or for this workspace), uninstall — and Open the Plugin's Folder.
Text objects (modal mode)
With core.modal = true, i and a after an operator — or in visual
mode — mean «the thing the cursor is in»: ciw changes the word, di(
takes out what is inside the brackets, ya" copies the string with its
quotes, vip selects the paragraph. The objects are w and W (word
and WORD), ", ', `, the brackets ((, [, {, <, and b
and B), p (paragraph), s (the line) and t (the innermost
XML/HTML tag).
Marks (modal mode)
m and then a letter remembers where the cursor is; ' and the same
letter goes back to it. A lowercase letter belongs to the file it was set
in, so ma in one file and ma in another are two different places. An
uppercase letter belongs to the window: 'A opens the file it was set in,
from wherever you are.
A mark is a line and a column, not a position in the bytes, so it keeps pointing at the same line while text is typed above it. A mark into a file that is no longer there is forgotten the first time it is followed.
Repeat and replace (modal mode)
. types the last change again — whatever it was: an operator with its
motion, a text object, a line typed in insert mode. It is kept as the keys
that made it, from the last time the editor was sitting in normal mode
until it was back there with the text changed.
r and a character replaces the character under the cursor without
leaving normal mode.
Macros (modal mode)
q and a letter starts recording what you type; q again stops it, and
@ with the same letter plays it back. @@ plays the last one again. A
macro can play another macro, four deep, which is enough to build one out
of others and not enough to hang the editor with one that plays itself.
Registers (modal mode)
" and a letter says which register the next yank, delete or paste uses:
"ayy keeps the line in a, "ap pastes it back, and what is in a
stays there while yy and p go on using the unnamed one. An uppercase
letter adds to the register instead of replacing it, so "Ayy on several
lines collects them.
Emmet
In HTML, XML, JSX/TSX, Vue, Svelte and PHP, an abbreviation before the
cursor expands into the markup it stands for: Emmet: Expand
Abbreviation (Ctrl+Alt+E), and, when it has structure, as a proposal
in the completion list, so Enter or Tab takes it.
ul>li.item*3 a list of three items, each with its class
div#main>p+span an id, a child and its sibling
a[href=# target=_blank] attributes, quoted or not
li.row$*3{fila $} numbered: row1 … row3, «fila 1» … «fila 3»
(div>p)*2 groups repeat everything inside them
table>tr>td what a bare class means follows the context
Empty attributes and empty elements become the placeholders of a
snippet, so Tab walks them, and the cursor ends after the last one. A
void tag (img, br, input…) closes itself.
In CSS, SCSS and Less the abbreviations are Emmet's for declarations, offered first in the completion list and expanded by the same command:
m10-20 margin: 10px 20px;
w100p width: 100%; (p %, e em, x ex, r rem, or the unit written out)
fz1.5e font-size: 1.5em;
df+aic+g8 display: flex; align-items: center; gap: 8px;
c#f color: #fff;
posa position: absolute;
m10! margin: 10px !important;
bdrs border-radius: | (the cursor where the value goes)
Emmet: Wrap with Abbreviation… puts the selection — or the line, with
nothing selected — inside what an abbreviation stands for: div.card
wraps it in the div, section>div in the inner one, and a * with no
number makes one copy per line, so ul>li* turns lines into a list.
Tags that close themselves
In those same languages, typing > after an opening tag writes the
closing one and leaves the cursor between the two, and typing </
finishes it with whichever tag is still open. Void tags (br, img,
input…) and tags that close themselves are left alone.
editor.auto-closing-tags = false turns it off.
Large files
A file past 4 MB, or one with a line longer than 100 000 characters (a JSON written on a single line, a minified bundle), is opened without syntax highlighting, and past 16 MB no language server is told about it either. Parsing it would take seconds of a frozen window, and holding it in a server would take memory nobody asked for; what you usually want from a file that size is to look at it, which still works. The editor says what it turned off when it opens one.
Pictures
Opening a .png, .jpg, .gif, .webp, .svg, .ico or .avif —
from the explorer, from the palette or from a link — shows the picture in
a tab, with its size in pixels and what it weighs underneath, instead of
the bytes that make it. It is a tab like any other, so it can be split
beside the code that uses it.
Colours you can see
Every colour written in a file — #1e1e2e, #0f08, rgb(220, 30, 90),
rgba(0, 128, 255, .5) — gets a bar in its own colour under the text
that names it, over a thin outline so a dark colour on a dark theme is
still there. What only looks like one is left alone: the #[derive] of a
Rust file is not a colour. editor.color-swatches = false turns it off.
Colour: Pick a Colour… offers the colour under the cursor written the
other ways — #ff0000, rgb(255 0 0), hsl(0 100% 50%) — and the named
colours; Colour: Type a Colour… takes one in any of those forms. What
is chosen replaces the colour under the cursor (a #hex, an rgb(), an
hsl() or a name), or goes where the cursor is.
The minimap
The whole file is drawn small down the right-hand side: one bar per line
at its indentation, the lines that differ from git in the colour of the
gutter, the matches of the find bar picked out, and a box over the part
you are looking at. Clicking or dragging it moves through the file. A
file too long to fit slides under the box, so the map always shows where
you are. editor.minimap = false turns it off and editor.minimap-width
changes how wide it is.
Walking the changes
Go to Next Change (Alt+F5) and Go to Previous Change
(Shift+Alt+F5) move the cursor through the places where the file
differs from the version in git — the same changes the gutter marks.
A block of added lines is one stop, a block that was deleted stops where
the text carries on, and the last change leads back to the first.
Terminal links
Ctrl/Cmd+click on a terminal line opens what is under the pointer: a file
with the line and column its tool printed — src/main.rs:12:5,
src/a.ts(3,5), File "app.py", line 12, ./cmd/x.go:7:2, a plain path
or a file:// URL — or a web address, in your browser. Paths are
resolved against the terminal's working directory and then the workspace,
and only offered when the file exists, so a word that merely looks like a
path is left alone. With shell integration set up, the
terminal's directory is the one the shell is really in, so the links of a
program that changed directory still work.
See Terminal for finding in the scrollback and for running a recent command again.
Source: docs/editing.md