Skip to content

feat: add spell checking via aspell - #341

Open
tenox7 wants to merge 1 commit into
eugenioenko:mainfrom
tenox7:feat/spell-check
Open

feat: add spell checking via aspell#341
tenox7 wants to merge 1 commit into
eugenioenko:mainfrom
tenox7:feat/spell-check

Conversation

@tenox7

@tenox7 tenox7 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Adds native spell checking backed by aspell, modeled on micro's aspell plugin but integrated with ttt's existing diagnostics infrastructure.

What it does

  • Checks prose filetypes only — markdown, HTML, XML, TeX, reStructuredText, Org, plain text (files with no lexer, e.g. COMMIT_EDITMSG). Code files are skipped. Aspell filter modes (--mode=markdown etc.) keep code spans and URLs inside markdown out of the results.
  • Misspelled words get curly underlines via the same UlStyle cell channel as LSP diagnostics. Diagnostics take precedence when both hit a cell. New theme style editor.spellError (defaults to the warning color).
  • Right-click a misspelled word → up to 5 Correct to "…" items plus Add "…" to Dictionary prepended to the editor context menu.
  • Command palette: Spell: Suggest Corrections (autocomplete popup at the cursor), Spell: Add Word to Dictionary (aspell personal dictionary), Toggle Spell Check.
  • Options menu gets a Spell Check toggle with live apply (no restart). Off by default; requires aspell in PATH (warns when missing, inert otherwise).
  • Settings: 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.
  • Async flow mirrors the git gutter: ScheduleSpellCheck (debounced on change) → RequestSpellCheck (generation counter + buffer-line copy + goroutine) → SpellResult posted via EventInterrupt. Re-checks on edit, file open, and tab switch; results are stored per tab and restored on switch, like diagnostics.
  • Corrections apply as a single-undo 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.
  • Errors from aspell (e.g. missing dictionary for spell.lang) surface once in the status bar instead of spamming on every keystroke.

Also included

  • rclick X Y command in the --exec debug harness (needed to test the context-menu path; generally useful).
  • Debug state dump now includes the active editor's misspellings.
  • Regenerated reference config/settings.json (new spell section).

Testing

  • Unit (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).
  • E2E (tests/e2e/spellcheck_test.go): async check marks spans and sets UlStyle on 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.
  • Functional (tests/functional/spell-check.test.js): toggle → suggest → accept flow and the no-misspelling notice, gated on aspell presence.
  • Verified live via the debug harness: right-click menu with corrections, click-to-apply, settings persistence across runs.

Known limitations

  • Spell checking is whole-file per check (like micro's plugin); fine at typical prose sizes, guarded by a 15s timeout.
  • A spell underline and an LSP diagnostic on the same cell can't stack — diagnostics win.
  • Suggestions come from a completed check, so a word typed <debounce ago may not offer corrections yet.

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
@tenox7
tenox7 requested a review from eugenioenko as a code owner July 6, 2026 00:38
Copilot AI review requested due to automatic review settings July 6, 2026 00:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 aspell pipe-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.

Comment on lines +91 to +102
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)
}
}
Comment thread internal/app/menus.go
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) {

@eugenioenko eugenioenko left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants