From 715ebf19e2ce9caa30a385791b75a0f1cfc15c85 Mon Sep 17 00:00:00 2001 From: Muu Date: Sat, 7 Nov 2026 10:36:09 +0800 Subject: [PATCH] fix(spelling): keep dictionary saves on read-only documents (#1756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typo quick fixes vanished entirely in read-only documents (library sources, stubs such as pyrefly's bundled typeshed), although saving a word to the project or application dictionary only writes settings. Only the in-place Change to…/Rename to… fix now needs a writable document; Save 'word' to dictionary and its per-level submenu are offered everywhere. Co-authored-by: muutot <92625353+muutot@users.noreply.github.com> --- src/editor/lsp/spelling.test.ts | 21 +++++++++++++++++++++ src/editor/lsp/spelling.ts | 29 +++++++++++++++++++---------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/editor/lsp/spelling.test.ts b/src/editor/lsp/spelling.test.ts index 62550c5de..9017b82ad 100644 --- a/src/editor/lsp/spelling.test.ts +++ b/src/editor/lsp/spelling.test.ts @@ -119,6 +119,27 @@ describe("Typo inspection", () => { expect(spellingIntentions(view)[0]!.label).toBe("Typo: Rename to…"); }); + it("in a read-only document saves to dictionaries but changes nothing in place", async () => { + vi.useFakeTimers(); + useProject.setState({ project: { root: "/p" } as never, settings: {} as never }); + const view = new EditorView({ + state: EditorState.create({ + doc: "// mesage here", + extensions: [ + lspDoc.of({ doc: 7, path: "/p/a.rs", language: "rust", uiMode: null, readOnly: true }), + spellingExtension(), + ], + }), + }); + views.push(view); + await settle(); + expect(spellCheck).toHaveBeenCalled(); + view.dispatch({ selection: { anchor: 5 } }); + expect(spellingIntentions(view).map((i) => i.label)).toEqual(["Save 'mesage' to dictionary"]); + await spellingIntentions(view)[0]!.run(); + expect(settingsSet).toHaveBeenCalledWith("project", "spelling.projectWords", JSON.stringify(["mesage"])); + }); + it("checks only what the Typo options include, and re-checks when they change", async () => { vi.useFakeTimers(); const options = (o: Record) => diff --git a/src/editor/lsp/spelling.ts b/src/editor/lsp/spelling.ts index 1a10926cc..6220ee49b 100644 --- a/src/editor/lsp/spelling.ts +++ b/src/editor/lsp/spelling.ts @@ -134,11 +134,15 @@ async function changeTo(view: EditorView, typo: PlacedTypo) { }); } -/** Typo quick fixes at the caret (⌥↵), or none. */ +/** + * Typo quick fixes at the caret (⌥↵), or none. Dictionary saves write + * settings, so they are offered in read-only documents too (library sources, + * stubs); only the in-place correction needs a writable file. + */ export function spellingIntentions(view: EditorView): ListPopupItem[] { const doc = docOf(view); const sel = view.state.selection.main; - if (!doc || doc.readOnly || !sel.empty || !spellingEnabled()) return []; + if (!doc || !sel.empty || !spellingEnabled()) return []; const typo = typoAt(view.state, sel.head); if (!typo) return []; const hasProject = !!useProject.getState().project; @@ -162,15 +166,20 @@ export function spellingIntentions(view: EditorView): ListPopupItem[] { { kind: "separator" }, ...intentionControls({ id: SPELLING_INSPECTION, name: "Typo" }), ]; + const change: ListPopupItem[] = doc.readOnly + ? [] + : [ + { + id: "spelling.change", + label: typo.context === "code" ? "Typo: Rename to…" : "Typo: Change to…", + icon: "bulb-fix", + emphasis: true, + run: () => changeTo(view, typo), + submenu: saveSubmenu, + }, + ]; return [ - { - id: "spelling.change", - label: typo.context === "code" ? "Typo: Rename to…" : "Typo: Change to…", - icon: "bulb-fix", - emphasis: true, - run: () => changeTo(view, typo), - submenu: saveSubmenu, - }, + ...change, { id: "spelling.save", label: tx`Save '${typo.word}' to dictionary`,