diff --git a/src/editor/lsp/spelling.test.ts b/src/editor/lsp/spelling.test.ts index 62550c5d..9017b82a 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 1a10926c..6220ee49 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`,