adamantine v0.1.0

Keyboard-first terminal editor built alongside Adamas with first-class LSP support.

Adamantine

CI

A keyboard-first terminal editor built alongside the Adamas compiler.

Adamantine is written in Crystal and built on crystal_tui. It is intentionally small and hackable. Adamas is its home toolchain, while language intelligence comes through LSP, so the editor can also work with Crystal and other languages.

[!NOTE] Adamantine is an early preview. It is useful today, but configuration and key bindings may still evolve before the first stable release.

Highlights

  • Multiple files in tabs and two side-by-side editor groups, a project tree, mouse support, and configurable keys
  • Bounded quick file opening, per-file EditorConfig preferences, and project session restoration
  • Find in file, bounded project search, replace, marks, and jump history
  • Undo and redo across normal editor input
  • Background detection of external file changes with reload, keep, and guarded overwrite choices
  • Private periodic checkpoints of unsaved buffers, with explicit recovery as separate files after a crash
  • Built-in dark, light, and high-contrast themes, plus JSON theme files
  • LSP diagnostics, hover, signatures, definitions, references, semantic tokens, and code folding, plus plain-text completion insertion and code-action previews
  • PATH-based Adamas language-server discovery, with explicit CLI and environment overrides
  • DAP debugging with reviewed launch profiles, breakpoints, stepping, stack and bounded variable inspection
  • Full-width bottom Chat / Log panel with a bounded multiline draft, explicitly configured streaming models and reviewed selected context
  • Explicitly reviewed local Lua 5.4 macros, with bounded execution and manual inline edit proposals
  • Editor core written in Crystal

Quick start

You need Crystal and Shards. The current development baseline is Crystal 1.21.0.

git clone https://github.com/skuznetsov/adamantine.git
cd adamantine
shards install --frozen
make build
./bin/adamantine .

Build, run, and spec targets check the locked dependencies first. If that check fails, install the versions in shard.lock with shards install --frozen.

Run directly from source while developing:

make run ARGS="--theme vscode-dark /path/to/project"

See all CLI options with ./bin/adamantine --help.

Everyday controls

Action Default binding
Search actions / command input F1 or Ctrl+Shift+P / Esc Esc
Quick file opener Ctrl+P
Quick actions Shift+Enter
Save Ctrl+S
Review external file changes Ctrl+Shift+E
Copy / cut / paste in the editor Ctrl+C / Ctrl+X / Ctrl+V
Close tab Ctrl+W
Split right / focus next group / close split Ctrl+Alt+R / Ctrl+Alt+O / Ctrl+Alt+W
Undo / redo Ctrl+Z / Ctrl+Shift+Z
Indent / dedent Tab / Shift+Tab
Find in file Ctrl+F
Find in project Alt+F
Show/focus Files / editor F2 / F3
Reveal/focus Chat Ctrl+G
Switch bottom Chat / Log Ctrl+Shift+G or F1 palette
Chat send / newline Enter / Shift+Enter or Alt+Enter
Hide/show bottom panel / cancel chat / new conversation F1 palette
Run / cancel a local Lua script / inspect its log F1 palette
Go to definition F12
Hover / references / signature F6 / F7 / F8
Start / continue, stop F5 / Shift+F5
Toggle breakpoint / step over / into F9 / F10 / F11
Step out Shift+F11
LSP actions F1 command palette → LSP actions
Problems Ctrl+Shift+M
Next / previous diagnostic Alt+N / Alt+P
Help / Settings / Reload theme F1 command palette
Quit Ctrl+Q

Some terminals reserve particular key combinations. Every application action can be remapped in a JSON keymap; keymap.example.json is a complete starting point.

The debugger keys follow the VS Code convention. F1 opens command discovery; Help, Settings, Reload theme, and LSP actions remain available there. F6 Hover and Ctrl+S Save are unchanged. A debug session must be started explicitly from a reviewed launch profile.

For a first local session, open the source and press F9 to set a breakpoint, then press F5. Review the adapter argv, program path, working directory, and target argv before continuing; dirty buffers require an explicit Save, Start-without-saving, or Cancel choice. If lldb-dap is already installed it may be prefilled as a suggestion, but the editor never installs or launches it until you approve the reviewed profile. Otherwise enter the adapter's absolute path in Adapter argv JSON (for example ["/path/to/lldb-dap", "--no-lldbinit"]). The last setup row, Values, defaults to Adapter values. Left/Right or Space selects Stock Crystal (LLDB); it is a choice, not a command field. Review discloses that this enables Adamantine's bundled provider. Stock Crystal requires an executable named lldb-dap and --no-lldbinit immediately after that executable in argv. Existing profiles remain generic unless you opt in. Setup/detail require at least 10 terminal rows; smaller viewports disable hidden-field editing and show a resize notice when it fits. Stock Crystal review needs at least 77 columns and 14 rows so its executable-provider disclosure fits; Escape still cancels.

The far-left activity rail shows vertical Files/Debug labels, with white text on the active background and one shared separator. By default, a blank column on each side of the label gives a four-column rail; F1 → Settings → Activity rail padding toggles the spacing. Set ui.activity_rail_padding to false for the compact two-column rail. If enabling padding would collapse an open editor split, the setting stays unchanged and shows a resize hint. Click a label or its padding to switch inspectors without losing tabs or debugger state. Debug opens on the left by default; change it in F1 → Settings → Debug dock position (Left, Right, Bottom). Files always stays left. Drag the shared divider to resize an inspector. The bottom status area remains independent of the inspector and both editor groups.

The full-width bottom panel has Chat and Log tabs. Ctrl+G reveals Chat and focuses its multiline composer; F3 returns to the editor. Pasted code keeps its newlines. Shift+Enter inserts a newline, with Alt+Enter as a fallback for terminals that cannot distinguish Shift+Enter. Escape cancels an active turn, or returns to the editor when idle. F1 exposes Focus Chat, Switch Chat / Log, Toggle bottom panel, Cancel chat request and New conversation. Hiding the panel returns its space to the editor and preserves its draft and dragged divider. Tiny viewports yield the panel's space and focus to the editor.

Escape handling depends on the terminal: the pinned TUI recognizes completed Escape sequences but can wait indefinitely on a bare single-byte Escape. Use F3 to return to the editor and F1 for cancellation/actions if this occurs.

Chat starts disabled. Open F1 → Chat settings to configure an exact OpenAI-compatible Chat Completions endpoint, model name, key environment-variable name, per-request output-token limit and optional reasoning effort. Leave reasoning effort blank to preserve the provider default; set it explicitly when your model requires it. It is sent unchanged on every request and shown in the Send review. Supply the key in that environment variable before launching Adamantine; never put the key itself in the settings. In the settings dialog, Space toggles Enabled and Ctrl+S saves. HTTPS is required except for literal loopback HTTP (127.0.0.1 or [::1]), where a blank key-variable name is allowed. There is no automatic model selection, credential discovery, retry or redirect.

Enter first opens a Send review; Y approves and Enter/Esc cancels by default. The review discloses the destination, model, prompt/history bytes, one-shot context and tool scope. F1 → Chat context selects None, Selection, Selection proposals (read + propose), Active buffer, Buffer read tools or Whole-file proposals. None is the default; direct source context is capped at 16 KiB and comes from unsaved editor text. Every explicit file context includes the active file's project-relative path and its nonempty selection, if any. Coordinates use one-based lines, zero-based Unicode codepoint columns, and an exclusive end; they are not byte offsets or screen columns. The Send review shows this location before export. Tools can read only the approved buffer at its approved revision, not other files or debugger state. Context resets to None after the approved request starts, not after its reply completes. Buffer read tools and Whole-file proposals initially attach no source text, but permit later tool rounds to send bounded portions of the explicitly approved buffer (8 KiB of source text per read, 64 KiB per encoded result, 512 KiB aggregate session output).

For ordinary inline changes, choose Selection proposals: it requires a nonempty selection, attaches only that selected text, and confines all subsequent reads and proposed edits to the reviewed fragment. Changing or clearing the selection invalidates its permission. A batch with any outside edit is rejected as a whole; insertions at either selection boundary are allowed. There is no silent fallback to the whole file. Selection without proposals remains read-only.

Whole-file proposals is a separate explicit permission to read/propose across the entire active file, regardless of its current selection. Both proposal modes permit one bounded edit attempt per turn, even if the host rejects it. Proposals remain private until the reply completes successfully, then appear as colored changes inside the editor. Space toggles an edit group; A selects all and N selects none. Enter applies the selected changes in memory and returns focus to the reviewed editor; Ctrl+Z undoes the batch in one step. Escape rejects it. Nothing is applied or saved by the model. Editing the source, switching files/groups, cancellation or a new turn invalidates an older proposal. A conflicting modal is not replaced. Read-only context modes grant no proposal permission.

The draft is limited to 16 KiB. Each request includes at most 64 KiB / 16 previous visible conversation messages; an excessive history requires New conversation. Attachments and tool results are not automatically replayed, but an assistant's visible reply may quote them, so None does not erase prior conversation content. A provider or project change cancels the turn and clears exportable history while preserving the draft. Each turn has at most four HTTP requests; the output-token setting applies to each request, not the whole turn.

Chat does not persist conversations, apply edits automatically or control debugging. It can request one explicitly reviewed named Lua script as described below; ordinary Chat turns have no scripting permission. store: false is requested, but is not a provider-side retention guarantee. Provider replies are untrusted text, not automatically scrubbed for secrets; choose the destination before authorizing any source or credentials. Local fake-server tests do not establish live-model/TLS compatibility or cost. See the selection/context frontier, reviewed edits frontier, provider frontier and Chat UI frontier for limits and verification scope.

Lua scripts in Chat

F1 → Chat context → Selection proposals + Lua…, Buffer read tools + Lua… or Whole-file proposals + Lua… selects a context and opens the trusted local script picker. With a tool-enabled context already selected, F1 → Choose Lua script for next Chat turn… opens the same picker; Clear Chat Lua script removes the selection. Selecting a script does not execute it. The Send review shows its frozen source, SHA-256, requested/effective/excluded capabilities and limits. Up/Down and PageUp/PageDown scroll the review; Y authorizes that script for this turn only. The selection resets when the approved request starts.

The model can request that one script once, with no script arguments. Both its direct tools and Lua child can read or propose changes only for the exact buffer and revision approved in the review. The Selection proposals mode also confines the Lua child's reads and edits to the same frozen selection; choosing a script cannot widen it. A local script's broader listing, navigation or debugger permissions are excluded from Chat. Script output is returned to the selected provider and shares the turn's tool/result budget. Any child tool refusal poisons the run, even if Lua catches it and returns normally. Proposals stay private until both the child exits successfully and the entire provider reply completes, then use the same manual inline review. F1 → Cancel chat request also retires the child; only one local-or-Chat Lua worker can run at a time. See the Chat-Lua frontier.

A small GPT-6 Luna + Lua pagination demo provides deliberately failing Crystal tests and a read-only inspector. The model must propose the fix itself; accepting and saving it remain manual.

Local Lua scripts

F1 → Run Lua script opens a named picker. Choose a script, then review its frozen source SHA-256, requested tool capabilities and execution limits. Enter approves that run only; Escape cancels. Nothing runs at startup or just because a project contains a script. F1 → Cancel Lua script stops an active run; Lua script log shows bounded output in the bottom Log panel.

The built-ins list buffers, open README.md, and propose removal of trailing spaces in the first 32 lines of the selected buffer. Partial reads are reported; unobserved line tails are not edited. Proposals appear only after successful worker exit. Enter manually applies selected changes, Escape rejects, and Ctrl+Z undoes the accepted batch in one step. Scripts cannot apply or save changes themselves. Editing or switching the reviewed source revokes its proposal.

Lua is optional: normal editor startup/build does not require its development headers or library. Script execution requires a compatible Lua 5.4 shared library; LuaJIT/5.1 and Lua 5.5 are not supported by this ABI. Start the editor with ADAMANTINE_LUA_LIBRARY=/absolute/path/to/liblua5.4.so (or the corresponding .dylib). This is trusted native installation configuration, not a script permission or a security sandbox. Missing/incompatible runtime is reported without affecting normal editing.

For a user script, create ~/.config/adamantine/lua/manifest.json and place its source directly under ~/.config/adamantine/lua/scripts/. Only explicitly listed files are offered; there is no workspace catalog, arbitrary path/eval command, hook or automatic execution. A minimal manifest is:

{
  "version": 1,
  "scripts": [
    {
      "id": "my-buffers",
      "name": "My buffer list",
      "file": "my-buffers.lua",
      "capabilities": ["read_editor"]
    }
  ]
}

scripts/my-buffers.lua:

local result = editor.call("editor.list_buffers", { offset = 0, limit = 32 })
if result.status ~= "ok" then
  print(result.message or result.status)
  return
end
for _, buffer in ipairs(result.data.buffers) do
  print(buffer.path)
end
if result.data.partial then print("Partial buffer list") end

Available capabilities are read_editor, read_debugger, navigate and propose_edits; they must be requested in the manifest and approved per run. Local ReadEditor permits bounded project/buffer inspection, unlike Chat's narrower selected-buffer export. ReadDebugger returns retained observations, not target evaluation/control. Navigation uses guarded normal editor actions. ProposeEdits is restricted to the selected buffer/revision captured in editor.context.selected_buffer and one proposal attempt. Tool names, arguments, coordinates and response status use the shared tool contract.

Each run uses a fresh child and Lua state, at most 64 KiB source, 8 MiB Lua-managed heap, five seconds wall time including cleanup, and 16 KiB / 64 print events. The heap limit is not a total process-memory limit. Only basic Lua computation plus math/string/table/utf8 helpers is available: no filesystem, shell, network, native modules or dynamic code loading. Cancellation/deadline requests worker termination independently of runaway code or finalizers. The editor confirms cancellation only after observed reaping; otherwise it reports cleanup pending and retains a supervisor until exit. A trusted native interpreter is assumed; this is not OS-level isolation. These are local-run permissions, not permission to export content to a model. Chat delegation has the separate narrower review above. See the local Lua frontier.

F2 shows/focuses Files; pressing F2 again while Files is already focused hides it and returns its space to the editor. Ctrl+O or selecting Files restores it. Switching away from Debug does not stop the session. A narrow terminal moves Debug below the editor when it fits; an unusable inspector yields its space to the editor and shows a resize hint instead of leaving hidden input active. Use F1 → Debug panel, then Tab/Shift+Tab to select Variables (Threads is between); Enter on the placeholder explicitly loads scopes for the stopped frame. Right/Enter expands a value, m loads another partial page, and d opens its bounded detail. Scope headings are separate from values; an empty reply, loading, missing summary, partial page and clipped display are distinguished. A scope-loading error shows a reason and Enter retries. Detail shows the variable name/type and wraps its value: arrows scroll, PageUp/PageDown page, Home/End jump, x selects adapter Hex, z interprets JSON, and s returns to Summary. Escape closes it. A changed stop/frame retires the detail rather than showing the previous frame's value as current. Watches are manual: F1 → Add debug watch adds an expression without evaluating it; choose it in Watch and press e to see a code-execution warning before explicit evaluation. Stack o navigates to a selected location; adapter-owned source is shown in a read-only view. Pause and other controls are also in F1 command discovery if the terminal does not deliver a modified function key.

The gutter keeps separate breakpoint and location cells: ● is an enabled adapter-verified breakpoint, while muted o is disabled or not currently verified (see Breakpoints for its status/reason). ▶ marks the stopped frame; ◇ marks a different selected caller. Location markers require a matching, unchanged local launch baseline. Explicit navigation can still open an unverified reported location with a warning; a successful breakpoint alone does not prove that the source matches the executable.

Change the DAP message limit in F1 → Settings → DAP message limit. Enter cycles the bounded presets; the saved limit applies at the next explicit Start, not to an already running session. It can also be set separately from LSP in the active config:

{
  "debug": { "max_message_mib": 16, "dock": "left" }
}

It accepts 1–64 MiB (default 16). It limits one incoming message, not total memory. If saving fails, Settings reports that the new value is only in memory. debug.dock accepts left, right, or bottom; unlike the message limit, changing the dock moves the existing inspector immediately without restarting the session. Unknown config sections and key bindings are preserved. Local stdio launch supports generic adapter values and an opt-in bundled Stock-Crystal provider. The provider validates DWARF layouts and reads memory without evaluating target expressions or calling target methods. Its String preview is capped at 1024 bytes; integer Array previews show at most 8 elements and expose at most 256 indexed children, with visible limit notices. Real native-value checks cover String and Array(Int32) with Crystal 1.21.0, LLDB 22.1.8 and arm64 macOS; other layouts are not inferred from type names. Adamas, Hash/union/complex-element formatting, attach and inferior terminal input remain separate gates. See the debugger frontier and value-inspection evidence.

Copy and cut retain a shared in-memory clipboard across editor tabs and the Chat composer. On macOS, Adamantine also uses pbcopy/pbpaste as a best-effort system clipboard bridge; other platforms currently use the internal clipboard. Helper failures are reported without discarding the internal copy. Ctrl+C is no longer an implicit quit shortcut; use Ctrl+Q to quit. Clipboard actions apply only to the focused editor or Chat composer, not to a document behind a dialog. Terminal bracketed paste remains available in both surfaces.

Clipboard data is limited to 16 MiB and system helpers time out after 250 ms. If a system copy fails, paste keeps using that internal copy until another copy successfully reaches the system clipboard. A delayed paste is discarded if you continue typing, move the cursor, switch tabs, or change the selection first. Chat additionally limits the complete draft to 16 KiB and rejects a delayed paste after its composer revision, turn or project lifetime changes.

When another process changes an open file, Adamantine marks its tab with ! without interrupting typing or opening a popup. Press Ctrl+Shift+E, choose Review external changes from Quick Actions, or run external in the F1 palette. Save on an unresolved conflict also opens review instead of writing.

The inline comparison labels - Editor and + Disk. Tab/Shift+Tab selects Later / Reload from disk / Overwrite disk, and Enter confirms; Later is selected initially and Escape also defers the decision. Arrows/PageUp/PageDown scroll the comparison. Reload remains undoable; Overwrite explicitly writes your editor text to disk. A newer editor or disk revision invalidates the decision and requires fresh review. Later leaves both versions unchanged and the conflict unresolved. Unavailable or non-text disk contents are identified explicitly, not shown as an empty file. Whole-file comparisons can be coarse, and very long displayed lines are visibly abbreviated.

Quick file opener

Press Ctrl+P, type a fuzzy filename/path query, select with Up/Down, and press Enter to open; Escape cancels. Existing unsaved tabs are reused. The opener indexes paths only, skips dependency/VCS directories and symlinks, and shows up to 100 ranked matches. Traversal is bounded to 10,000 entries and 16 levels; an incomplete scan is visibly marked partial, even with no matches. Close and reopen to refresh the index. Query input is limited to 256 characters. See workflow boundaries for limits.

Command palette

Press F1 and search by ordinary words, for example open settings or formatting. Up/Down select an action; Enter runs it; Tab prepares its command. Actions needing an argument (such as Rename or Open File) prepare the command for you to finish. Escape cancels. Shortcut hints follow your configured keymap. An action shown as unbound is not reachable by an old default shortcut. When a shared action is unavailable, the selected action shows its current reason; Enter and Tab leave it open instead of dispatching it.

For explicit commands, type : first, or use Esc Esc, which inserts it. In command mode Enter executes exactly what you typed. Alt+Up/Alt+Down recall command history; ordinary Up/Down also recall history in command mode. Force quit is deliberately absent from action search and requires explicit :q!.

:w                         save
:q                         close the active tab
:quit                      quit, reviewing unsaved files first
:q!                        force quit (keep available recovery checkpoints)
:wq                        save and quit
:open path/to/file.cr       open a file inside the project
:cd path/to/project         change the project root
:find pattern               find in the active file
:grep [-i] pattern          search the project
:s/old/new/gic              replace; g=all, i=ignore case, c=preview
:buf [number]               list or select open buffers
:splitright                 open a second editor group on the right
:focusnextgroup             switch between editor groups
:closesplit                 return to one group without closing files
:mark name                  create a mark
:jump name                  jump to a mark
:theme vscode-light         switch theme
:recover                   list recoverable drafts
:format                    preview LSP formatting of the active document
:external                  compare editor text with external disk changes
:rename new_name           preview a current-file symbol rename via LSP
:quickfix                  choose an LSP quick fix, then preview its edits
:git                       browse Git status, history and diff (read-only)
:template                  choose an editor template for this file
:template def              insert the named template directly

/pattern opens forward search directly. After closing the search panel, n and N repeat the search forward and backward.

Editor templates

Use F1 → Insert template or :template to choose a template explicitly. Crystal and Adamas files include def, class, and if. If the matching trigger is immediately before the cursor, accepting it replaces that trigger; otherwise the template is inserted at the cursor. Tab and Shift+Tab move between editable fields; Escape leaves field navigation and keeps the inserted text. The initial insertion is one Undo step. Templates work without an LSP.

Add optional templates in ~/.config/adamantine/templates.json or the project's .adamantine/templates.json (project definitions take precedence for the same trigger and language):

{
  "version": 1,
  "templates": [
    {
      "trigger": "test",
      "label": "Spec example",
      "languages": ["crystal"],
      "body": "it \"${1:does something}\" do\n  $0\nend"
    }
  ]
}

Omit languages to offer a template in every file. Configuration reloads when opening the picker or changing the project root; invalid entries are skipped with a status warning. Bodies support numbered $1 / ${1:default} fields and $0 as the final cursor position. They do not run commands or support linked fields, variables, or the full LSP snippet grammar. See the template boundary for details.

Split editor groups

Use F1 → Split right (or :splitright) to open a second editor group. Newly opened files go to the active group; clicking either group or using Focus next group (:focusnextgroup) switches the active target for Save, Close, search, and LSP actions. Opening a file that is already visible in the other group focuses its existing tab instead of creating another copy. Close split (:closesplit) moves all tabs back into one group, including unsaved buffers; it does not close their documents.

This version supports two groups with distinct files, not two independent views of the same file. Session version 2 preserves whether the split is open, each tab's group, each group's selected tab, and the active group; version-1 sessions still restore as a single group. If the terminal is too narrow at startup, the saved tabs are restored into one group with a warning. The next session save records that one-group fallback, so widening the terminal later does not reopen the split automatically; use :splitright to reopen it.

Quick Actions

Press Shift+Enter for contextual actions: search, LSP navigation, Format, Rename, Quick Fix and external-change review. The first four search entries keep their positions. Use Up/Down and Enter, or digits 1–9 for the numbered entries; longer menus scroll with the selection. Escape cancels. Hints marked global: describe shortcuts outside the menu, not extra menu selection keys.

Unavailable entries stay visible with a ! marker and the selected reason. Availability is checked again when you choose an action. Typing, paste and mouse input cannot edit the document behind the menu; F1 can replace it with action search. Format and refactoring still require explicit preview acceptance and never save automatically.

Formatting preview

:format requests formatting from the connected language server, using the active document's indentation settings. The server must advertise document formatting support. Proposed changes appear inside the editor pane: red - lines are removed and green + lines are added, with surrounding source context. Enter accepts the whole proposal; Escape rejects it. Up/Down and Page Up/Down scroll, Home/End reach the document ends, and Tab/Shift-Tab jump between changes. Review does not modify the live buffer. Acceptance is one Undo transaction and never saves automatically. Long or wide rows show explicit truncation; changed rows identify their line endings. Stale, malformed or overlapping edits are rejected as a whole. See inline review boundaries and safe edit boundaries.

Rename and Quick Fix

Place the cursor on a symbol and run :rename new_name. Use :quickfix to request fixes at the cursor; arrows choose an action and Enter opens its inline preview. As with formatting, Enter accepts all, Escape rejects and one Undo restores the previous document. Tab navigates changes; it does not apply. The connected server must advertise the requested capability and provide the actual edits.

This first slice accepts current-document edits only. If any edit targets another file, the whole operation is rejected, even when that file is already open. File creation/deletion/renaming, server commands, disabled actions and lazy action resolution are not supported. Nothing is saved automatically. The action list and preview are bounded with visible truncation. Quick Fix currently sends an empty diagnostic context; servers that depend on diagnostic code/data may return no actions. See refactoring boundaries.

Git browser

:git opens a read-only browser for the current project's repository. Tab switches status/history (s and l also select them); arrows select a row and Enter opens its diff. Escape returns from diff or closes the browser; r refreshes. The status shows both index and working-tree codes; file diffs separate staged and unstaged changes. This is disk/index state, not unsaved editor content. Untracked file content is not loaded as a diff.

The commit model and approximate branch-lane display are adapted from Crystal Ball's Git browser. Reads are bounded and cancellable, with visible errors/limits; no staging, checkout, merge, network operations or repository configuration writes are offered. See Git view boundaries.

For a saved file tracked at HEAD, the line-number gutter shows + for added lines, ~ for modified lines, and - beside the surviving line at a deletion. It combines staged and unstaged changes against local HEAD without changing the text viewport. Markers disappear while the editor buffer is unsaved or an external change is unresolved; saving refreshes them. Untracked, binary, and oversized diffs have no markers. See Git gutter boundaries.

In-file find reads the piece-tree buffer in bounded chunks. Files above 64 KiB are searched cooperatively after a short debounce; newer queries cancel stale work. The live list is capped at 200 matches and labeled partial at the cap, while n/N can reach later matches and wrap within a single line. These limits are currently fixed. Search preserves original Unicode character positions, including case-insensitive matches whose lowercase representation grows.

Replace scans bounded chunks and prepares edits on a separate piece-tree root; one successful command is one Undo/Redo step. Preview shows at most five bounded samples labeled with original byte offsets. Replacement remains literal, with the existing regex case/backreference behavior for i (not Find's lowercase matching). Safety limits reject the entire operation without changing text or history: 16 KiB query, 1 MiB replacement argument, 100,000 matches, and output no larger than the greater of 16 MiB and the current buffer. Backreference expansion also has a conservative 16 MiB per-match bound. These limits are currently fixed. Replacement is synchronous; connected LSP servers still receive one full-text update after commit.

Project sessions

Closing a changed tab (Ctrl+W or :q) offers Save / Discard / Cancel for that file. Ordinary quit (Ctrl+Q or :quit) reviews each changed file before exiting. The dialog shows the path; Tab or arrows select, Enter confirms, and Escape cancels. Cancel is selected initially. Discard leaves the source file on disk unchanged; it is not a command to erase private recovery history.

Cancelling a multi-file quit leaves all tabs open, including files whose discard was already selected. Any saves explicitly requested earlier remain saved. Failed saves do not close the file. If an external change prevents Save, cancel the dialog and resolve that conflict first; Save never implicitly overwrites an external change. :q! remains an explicit force quit. Discard during quit retains the normal session tab list: those files reopen from disk at the next launch, not from their discarded editor text.

On normal editor startup, Adamantine restores the project's open tabs, active tab, cursor and viewport. A successful quit saves this UI metadata; a cancelled quit does not. Switching projects with :cd saves the old project's view and restores the new project's view without closing existing tabs or replacing unsaved edits. Files are reopened from their current disk contents: missing, binary or oversized files are skipped with a message, and positions are clamped.

Session metadata contains paths and positions, not document text or Undo history. Unsaved-text recovery remains separate. Set ADAMANTINE_SESSION=0 to disable session restoration and persistence. Use ADAMANTINE_STATE_HOME to choose the state directory; otherwise it uses $XDG_STATE_HOME/adamantine when absolute, or ~/.local/state/adamantine. Session files live in its private sessions subdirectory and are not encrypted.

Limits are 128 tabs, 1 MiB of metadata, 16 MiB per restored file and 64 MiB of source bytes per restoration. These are not a total memory limit, especially when a language server is running. Concurrent editor instances use the last successful snapshot; sessions are not merged. See the workflow boundaries.

Unsaved buffer recovery

While the editor is running, modified file-backed buffers are periodically checkpointed outside the project (a pass runs roughly every two seconds). State lives under $XDG_STATE_HOME/adamantine/recovery when XDG_STATE_HOME is absolute, otherwise under ~/.local/state/adamantine/recovery. Abandoned sessions are discovered at startup and with :recover; another running editor's snapshots are not offered. Each draft has three separate actions. Review draft (read-only) opens a detached, non-mutating comparison of the captured editor text, current disk contents and checkpoint wherever those sources are available. Use Tab and Shift-Tab to cycle pairwise views, arrows/Page Up/Page Down/Home/End to move, and Escape to close. A standalone checkpoint view remains available when the original file was deleted. Review never creates a copy, reloads or overwrites a file, edits a buffer, or deletes the checkpoint.

Open recovered copy creates an independent private copy, leaving both the original file and checkpoint untouched. This also works when the original was changed or deleted. Discard checkpoint is a separate explicit deletion; dismissing the menu keeps the checkpoint. Limits are 16 MiB per document and 256 MiB per session, not a global disk quota. Discovery is bounded; warnings indicate when accumulated state needs attention.

Recovery files are private to your OS account, but not encrypted. Set ADAMANTINE_RECOVERY=0 before launch to disable recovery for sensitive work. Checkpoints are not a keystroke journal or an undo-history backup: edits since the last successful checkpoint can be lost, and power-loss durability is not guaranteed. See the recovery scope.

LSP support

Without an explicit command, Adamantine detects the project language and looks for compatible server executables on PATH, preferring adamas_lsp. It never executes a server discovered inside the project tree; select one explicitly only when you trust it:

./bin/adamantine . --lsp crystalline
./bin/adamantine . --lsp crystal-tool-lsp --lsp-arg --stdio
EDITOR_LSP=/path/to/language-server ./bin/adamantine .
./bin/adamantine . --no-lsp

ADAMANTINE_LSP is the application-specific environment override; EDITOR_LSP is accepted as a generic alternative.

The header shows connection health. Press F1, search LSP status for details, or select Restart LSP to reconnect the configured server without reopening files. :lsp and :lsp restart provide the same command paths. After a failure, status shows a short reason and the recovery action; when no server is configured, it explains how to configure one before relaunch. Unexpected transport failures trigger at most three automatic retries (250, 500 and 1000 ms backoff); a successful handshake does not reset that budget. Manual restart or a project-root change starts a new budget. --no-lsp stays disabled: restart never discovers a new executable.

Recovery sends current unsaved text to a fresh server and leaves buffers and Undo intact. Old diagnostics and semantic/folding results are invalidated; lexical highlighting remains available. Initial startup is still synchronous, and existing pipe-write/stop timeout limits apply. See the recovery boundary and tests. Failure-status behavior is scoped in the LSP error UX frontier.

LSP capabilities depend on the selected server. The editor remains usable without one. Crystal-family buffers have a bounded, background lexical layer for keywords, identifiers, decimal numbers, hash comments and ordinary quoted strings. LSP semantic tokens take priority. This is not a full grammar: ambiguous percent literals, backticks and << conservatively leave the rest of the file plain until LSP supplies tokens. Very long or token-dense lines may also remain plain. See lexical limits and evidence.

In the completion list, Up/Down selects, Enter or Tab inserts, and Escape cancels. These physical modal controls remain safety/navigation guards even when application bindings are remapped; they are not promises that the same key is available to the editor widget. Plain-text completions and standard single-line source textEdit ranges are supported, including multiline replacement text, as one Undo/Redo operation. Stale results, active selections, snippets, insert/replace edits, additional edits, commands and list defaults are rejected explicitly. Lists and insertion payloads are bounded; see completion limits and verification. Rename and eager Quick Fix edits can be reviewed and accepted for the current document; multi-document workspace edits and server commands remain rejected.

Interactive LSP actions run in background fibers. Editing, cursor movement, tab switches and popup dismissal discard outdated results. The scheduler keeps one running action and only the latest queued action; a slow server can delay that next action until the current request finishes or times out. Server startup, transport writes and semantic highlighting retain their existing behavior. See the async runtime scope.

Configuration and themes

Pass a keymap or theme explicitly:

./bin/adamantine . --config ~/.config/adamantine/config.json
./bin/adamantine . --theme ~/.config/adamantine/theme.json

Built-in theme names include vscode-dark, vscode-light, and vscode-high-contrast. The editor also checks ~/.config/adamantine/config.json for its default keymap.

F1 opens action search; type : to switch to explicit command input. Esc Esc opens command input with : already entered; Escape closes either mode. Existing custom keymaps take precedence over defaults. If your config already defines app.command_palette, add "f1" to that action's binding list to use the new shortcut.

Keymap entries are sparse overrides. An omitted action inherits its built-in default; a non-empty string or array replaces it; and an explicit empty array unbinds it and remains unbound after restart. For example:

{
  "keymap": {
    "app.save": [],
    "app.undo": ["alt+u"]
  }
}

Empty strings, null, and arrays with no valid bindings are invalid and keep the built-in default (with a warning). In F1 → Settings, select a key binding and press physical Delete or Backspace to request an unbind; Enter/Y confirms and N/Escape cancels. Rebinding onto a key lists every same-context owner before confirmation. Settings, Help, action search and Quick Actions all show the effective map, including unbound and conflict status. Modal dialogs keep documented physical Escape/Enter/arrows/Tab recovery and navigation controls where applicable, and clipboard/editor Tab guards stay active even when their application actions are unbound.

Indentation

In F1 → Settings, change the default indentation width (1–8, default 2) or toggle auto-indent (on by default). Changes apply to open and future tabs and are saved in the active config:

{
  "editor": { "indent_width": 2, "auto_indent": true }
}

Tab inserts one indentation unit at the cursor or indents selected lines. Shift+Tab removes up to one unit of leading spaces, or one leading tab. A selection ending at column zero leaves that last line unchanged. Each command is one undoable edit. These actions can be remapped as app.indent and app.dedent; unbound Tab keys do not fall back to hardcoded editor indentation. Outside the editor, Tab remains focus navigation.

Text layout uses terminal display cells: existing tabs advance to tab stops, wide characters occupy two cells, and combining/emoji sequences are rendered as graphemes. Left/Right and Delete/Backspace operate on whole graphemes. Mouse hit testing and horizontal scrolling use the same layout. Internal cursor/search positions remain codepoint-based; LSP positions are converted at the UTF-16 boundary. Terminal/font width differences can still affect appearance. Visible-prefix rendering avoids whole-document copies; moving deep into a single enormous line still scans its prefix synchronously.

Enter copies the whitespace prefix before the cursor or start of the selected range; it does not infer nesting from language syntax. Existing tab prefixes are preserved. Without a per-file override, new indentation uses spaces. Automatic indent detection and a separate literal-tab insertion mode are not supported.

EditorConfig

Per-file .editorconfig settings override the Settings defaults. Closer files and later matching sections win; root = true stops ancestor lookup and unset removes an inherited property. Supported properties are indent_style, indent_size (1–8 or tab), tab_width (1–8) and end_of_line (lf, crlf, cr). Tab display width and indentation width are independent.

root = true
[*.cr]
indent_style = space
indent_size = 2
end_of_line = lf

Preferences affect newly inserted whitespace and line breaks, never an implicit whole-file conversion. Existing mixed line endings survive open, save and Undo. Opening a file, reapplying a theme, or changing Settings editing settings refreshes overrides; configuration files are not watched continuously. Unsupported values produce warnings and unknown properties are ignored. This is a bounded supported subset, not full EditorConfig conformance; see docs/WORKFLOW_FRONTIER.md for limits.

Problems navigation

Problems always lists diagnostics retained by every currently open file, ordered by severity, path and position. If the active language server also advertises LSP workspace diagnostics, the same action asynchronously adds its reports for unopened files and labels the view Server Workspace. This means "reported by the server", not proof that every project file was checked. Rows use project-relative paths where possible.

Use arrows and Enter to navigate, or Escape to close. Open tabs are reused without rereading their text. An unopened server result is opened only after Enter, and only when its canonical file is still unchanged, readable, regular, within the project root and below the file-size limit. Alt+N/Alt+P remain current-file actions: they visit that file's diagnostics in source order and wrap. These actions are remappable.

Edits, closed buffers, replacement publications and LSP replacement invalidate old rows. Versioned notifications must match the target buffer; servers omitting versions cannot guarantee freshness. Live open-buffer diagnostics remain authoritative over a workspace report for the same file, including symlink and hard-link aliases.

The view retains at most 1000 rows and is visibly partial when a source or hard bound was truncated. Workspace parsing examines at most 4096 document reports and 4096 diagnostics; individual push responses inspect at most 1000 items. Messages retain at most 4096 codepoints. A server without the exact static workspace-diagnostic capability keeps the existing Open Files view. No disk scan, automatic fix or source read occurs merely by opening Problems.

LSP response limit

Open F1 → Settings → LSP response limit and press Enter to cycle through 1, 4, 8, 16, 32, and 64 MiB. The default is 16 MiB. Changes are saved to the active config (--config, ADAMANTINE_CONFIG, or the default path above) and apply immediately. Increasing the limit requests highlighting again for the current file. The config accepts any integer from 1 through 64:

{
  "lsp": { "max_response_mib": 16 }
}

Merge this section into your existing config; settings and keymap saves preserve unrelated keys. This limits an incoming LSP response body, not source-file size or total process memory. Larger values permit more memory use during JSON parsing. An oversized framed response is discarded without parsing, with a warning in the status log showing its size, the limit, and where to change it. Pending requests may fail, but later small requests keep working. Truncated or stalled responses, or frames exceeding the hard 256 MiB discard cap, disconnect with a warning. See the response-limit boundary.

Existing preview installations remain compatible with CRYSTAL_EDITOR_CONFIG, CRYSTAL_EDITOR_THEME, CRYSTAL_EDITOR_LSP, and the old ~/.config/crystal_editor and ~/.crystal_editor directories. New installations should use the Adamantine names.

Development

shards install --frozen
task_lua_dir=$(mktemp -d /tmp/adamantine-test-lua.XXXXXX)
export ADAMANTINE_LUA_LIBRARY="$(bash scripts/build_test_lua.sh "$task_lua_dir")"
make build
export ADAMANTINE_LUA_WORKER="$PWD/bin/adamantine"
make check

make check verifies formatting, builds the binary, and runs the full spec suite. The Lua integration specs require the real editor worker and Lua 5.4; they do not silently skip a missing runtime. The helper verifies the pinned official Lua source archive and builds its shared library only in the supplied temporary directory, without installing anything globally. It also builds the incompatible-ABI fixtures required by the full suite. Production builds remain independent of Lua; normal script execution and the terminal smoke can instead use an explicitly configured Lua 5.4 installation.

The real-terminal local and Chat scripting gates are separate:

ruby scripts/smoke_lua_scripts.rb --editor-bin "$ADAMANTINE_LUA_WORKER" \
  --library-path "$ADAMANTINE_LUA_LIBRARY"
ruby scripts/smoke_chat_lua.rb --editor-bin "$ADAMANTINE_LUA_WORKER" \
  --library-path "$ADAMANTINE_LUA_LIBRARY"

The Chat gate uses a synthetic loopback provider, not a real key or paid call. It exercises frozen Send review, real child execution, cancellation/reaping and manual proposal acceptance/Undo; it does not certify live-provider behavior.

The LSP handshake is deliberately separate because it requires a local server executable and Ruby:

ADAMANTINE_LSP=/path/to/server make check-lsp

Large-file and stalled-LSP regression scenarios are available separately:

ruby scripts/verify_responsiveness.rb --list
ruby scripts/verify_responsiveness.rb --self-test
ruby scripts/verify_responsiveness.rb

The complete run builds release probes for multi-megabyte many-line and single-line search, highlighting, replacement and inline preview, then runs the asynchronous LSP transport scenarios. It emits one JSON report. Timings, allocation counters and best-effort peak RSS are diagnostic observations, not portable performance thresholds; behavioral checks, report validation and the hung-process watchdog determine pass/fail. The same runner executes weekly and can be started manually in GitHub Actions.

Run make help for the complete target list. Contributions and focused bug reports are welcome; please include your Crystal version, terminal, operating system, and the smallest reproduction you can provide.

See ARCHITECTURE.md for the component boundaries and event flow.

Project status

The editor is optimized for source-sized projects and interactive terminal use. Project search runs in a cancellable background fiber and intentionally caps traversal, file size, and result count to keep the UI responsive. Partial scans are labeled even when no matches are returned; a partial zero is not proof that the project contains no matches. The current limits are 1 MiB per file, 1,500 scanned text files, depth 16, and 40 results. Skipped unreadable files also make a scan partial. These limits are not yet configurable in Settings. The scheduled responsiveness runner now covers representative multi-megabyte and huge-single-line operations, but end-to-end input-to-render latency, filesystem watcher latency, cross-platform terminal quirks, packaged binaries, and compatibility across language servers remain active areas of work.

The ordered improvement plan and its verification boundaries are tracked in docs/EDITOR_ROADMAP.md.

License

MIT

Repository

adamantine

Owner
Statistic
  • 3
  • 0
  • 0
  • 0
  • 1
  • about 6 hours ago
  • September 2, 2026
License

MIT License

Links
Synced at

Thu, 15 Oct 2026 03:16:08 GMT

Languages