feat: add spell checking via aspell - #341
Conversation
Spell checks prose files (markdown, HTML, XML, TeX, plain text) by piping the buffer through aspell's ispell pipe protocol. Misspelled words render as curly underlines through the same UlStyle channel as LSP diagnostics, with diagnostics taking precedence on overlap. - internal/spell: aspell pipe client with rune-offset parsing, filetype-to-filter-mode mapping, and personal dictionary support - async check flow mirrors the git gutter: debounced trigger, generation counter, EventInterrupt result posting - corrections via spell.suggest (autocomplete popup at cursor) and the editor right-click menu; applied as a single-undo BatchCommand after verifying the span is not stale - spell.addWord adds to the personal aspell dictionary - Options menu toggle (off by default); spell.enabled, spell.lang, spell.debounce in settings.json - rclick command added to the --exec debug harness - unit, e2e, and functional tests (skipped when aspell is missing) Claude-Session: https://claude.ai/code/session_01Q7qx6RDsctuobbwRN3nC8c
There was a problem hiding this comment.
Pull request overview
This PR adds native spell checking for prose-oriented buffers using aspell, integrating misspelling detection into the existing inline underline/diagnostics rendering and exposing corrections via command palette and context menus.
Changes:
- Introduces an
aspellpipe-protocol client (internal/spell) plus async spellcheck scheduling/results wiring in the app event loop. - Adds UI support for rendering spell underlines (
UlStyle) and for showing/applying suggestions (command + right-click menu) and managing per-tab misspelling state. - Adds settings/theme surface area (
spell.*,editor.spellError) and test coverage (unit + e2e + functional), plus small harness/docs updates.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/functional/spell-check.test.js | Adds functional coverage for toggle + suggest/accept + “no misspelling” flows (skips if aspell missing). |
| tests/e2e/spellcheck_test.go | Adds E2E coverage for spans, underline rendering, applying fixes (single undo), skipping code files, and toggle-off clearing. |
| README.md | Documents spell checking as an editor feature and notes the aspell dependency. |
| internal/ui/editor_widget.go | Renders spell underlines (lower priority than diagnostics) and adds misspelling lookup via per-line index. |
| internal/ui/editor_group.go | Introduces SpellSpan and per-tab misspelling storage + APIs to set/clear misspellings and sync into active editor. |
| internal/term/screen.go | Adds a new terminal style key for spell underlines. |
| internal/spell/aspell.go | Implements aspell availability check, mode mapping, pipe-protocol check, output parsing, and dictionary add. |
| internal/spell/aspell_test.go | Unit tests for parser, mode mapping, suggestion cap, and an optional real-aspell round trip. |
| internal/config/theme.go | Adds editor.spellError theme field and defaults it to warning foreground. |
| internal/config/settings.go | Adds spell settings (enabled, lang, debounce) with defaults and helper. |
| internal/app/theme.go | Maps editor.spellError into the runtime style map. |
| internal/app/spellcheck.go | Adds debounced scheduling, async request, result handling, suggestion popup, apply-fix, and add-to-dictionary commands. |
| internal/app/menus.go | Prepends misspelling corrections + dictionary action to the editor right-click context menu. |
| internal/app/exec_script.go | Extends exec harness with rclick X Y to test right-click flows. |
| internal/app/eventloop.go | Hooks spellcheck requests/results into the main event loop (tab switch + interrupts). |
| internal/app/debug_dump.go | Includes active editor misspellings in debug state dump. |
| internal/app/commands.go | Registers spell commands. |
| internal/app/commands_options.go | Adds Options-menu toggle + command for spellcheck with persistence and aspell presence warning. |
| internal/app/app.go | Schedules spell checking on editor changes (mirrors git gutter/autocomplete scheduling). |
| config/settings.json | Regenerates reference settings with the new spell.debounce default. |
| CLAUDE.md | Documents the new internal/spell layer and adds rclick to the debug harness docs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (a *App) HandleSpellResult(v *SpellResult) { | ||
| if v.Err != "" { | ||
| if !a.spellErrShown { | ||
| a.spellErrShown = true | ||
| a.StatusError("Spell check: " + v.Err) | ||
| } | ||
| return | ||
| } | ||
| if v.Gen == a.SpellGen { | ||
| a.EditorGroup.SetMisspellings(v.Path, v.Spans) | ||
| } | ||
| } |
| openContextMenuWith(app, items, mx, my, func(cmd string) { | ||
| switch { | ||
| case strings.HasPrefix(cmd, "spell.apply."): | ||
| if i, err := strconv.Atoi(strings.TrimPrefix(cmd, "spell.apply.")); err == nil && i < len(span.Suggestions) { |
There was a problem hiding this comment.
Hi @tenox7 , thank you for putting this feature together. It's looking awesome!
Spell checker is actually one of the few plugins I used to install in vscode. So this was genuinly missing feature.
On the implementation: this PR bakes the whole spell-check feature into the binary. Shelling out to an external tool like aspell is totally fine (there's precedent with ripgrep) but I'd really like to steer this class of feature toward the plugin system rather than core, so the editor stays lean and the community can own and iterate on it. I've just landed the missing pieces to make that possible: a diagnostics API (squiggles + the Diagnostics panel), an editor context-menu API, and a byte and rune column helper.
Would you be able to re-create this as a plugin please?
It can be published as its own repo. And then you can open a PR here to update community-plugins.json with the link to your spellcheck plugin.
Here some info for reference that might help sort out the dirrection:
Demo PR with the functionality of spell checking using the build in diagnostic system (squigly error highlight and context menu)
eugenioenko/ttt-plugins#3
A functioning docker plugin (example of how to interract with external binaries):
https://github.com/eugenioenko/ttt-plugins/blob/main/docker-manager/init.lua
Plugin authoring docs:
https://tttedit.dev/guides/plugin-authoring/
To quickly iterate on plugins, you can have a /plugins folder in the repo root and ttt will load them from there as well. BTW, plugins can be published on any repository so you can have a repo of your own with ttt-plugins and make them findeable in the binary by adding to community-plugins.json here
Once again, thank you so much for contributing to TTT and making it an amazing terminal IDE
Adds native spell checking backed by
aspell, modeled on micro's aspell plugin but integrated with ttt's existing diagnostics infrastructure.What it does
COMMIT_EDITMSG). Code files are skipped. Aspell filter modes (--mode=markdownetc.) keep code spans and URLs inside markdown out of the results.UlStylecell channel as LSP diagnostics. Diagnostics take precedence when both hit a cell. New theme styleeditor.spellError(defaults to the warning color).Correct to "…"items plusAdd "…" to Dictionaryprepended to the editor context menu.Spell: Suggest Corrections(autocomplete popup at the cursor),Spell: Add Word to Dictionary(aspell personal dictionary),Toggle Spell Check.aspellin PATH (warns when missing, inert otherwise).spell.enabled,spell.lang(aspell dictionary code, empty = locale),spell.debounce(default 500ms).How it works
internal/spell— small aspell client speaking the ispell pipe protocol: terse mode,^-prefixed data lines, parses&/#result lines. Offsets are rune positions (verified with multibyte text), matching the editor's rune-based column convention.ScheduleSpellCheck(debounced on change) →RequestSpellCheck(generation counter + buffer-line copy + goroutine) →SpellResultposted viaEventInterrupt. Re-checks on edit, file open, and tab switch; results are stored per tab and restored on switch, like diagnostics.BatchCommand(delete + paste), and the span is re-verified against the buffer first so a stale result can never mangle text — if the word moved, it just re-checks.spell.lang) surface once in the status bar instead of spamming on every keystroke.Also included
rclick X Ycommand in the--execdebug harness (needed to test the context-menu path; generally useful).config/settings.json(newspellsection).Testing
internal/spell): protocol parser (banner, blank-line tracking,&/#, suggestion cap, malformed lines), mode mapping, plus a real-aspell round-trip with multibyte offsets (skips when aspell is absent).tests/e2e/spellcheck_test.go): async check marks spans and setsUlStyleon the right cells (and not on correct words), suggest→Enter applies a correction and a single undo restores it, code files are skipped and stale spans cleared, toggle-off clears spans. All skip when aspell is missing.tests/functional/spell-check.test.js): toggle → suggest → accept flow and the no-misspelling notice, gated on aspell presence.Known limitations