From c599265cedfe3afe1794cd3b2c7a85fe9d66768d Mon Sep 17 00:00:00 2001 From: Jaw <53564781+jaw52@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:25:48 +0800 Subject: [PATCH] fix(format): re-detect indents after Reformat Code (#957) After Reformat Code rewrote a file's indents (e.g. spaces to tabs per Code Style), the editor kept the indents detected when the file was opened: tabs rendered at the old width, the status bar showed the old indent, and Enter inserted the old indent, mixing tabs and spaces until the file was reopened. Whole-file format edits from language servers and contributed formatters are now tagged `format.reformat`, and after such a transaction the document runs the open-time indent detection again and reapplies its settings, so every split picks up the new indent. Refs #935. Co-authored-by: jaw52 <53564781+jaw52@users.noreply.github.com> --- src/editor/core/model/document.test.ts | 29 +++++++++++++++++++++++++- src/editor/core/model/document.ts | 15 ++++++++++++- src/editor/core/model/slot.ts | 4 ++++ src/editor/lsp/format.ts | 9 ++++++-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/editor/core/model/document.test.ts b/src/editor/core/model/document.test.ts index 79c35655..9ba5feeb 100644 --- a/src/editor/core/model/document.test.ts +++ b/src/editor/core/model/document.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { EditorState } from "@codemirror/state"; -import { language as languageFacet } from "@codemirror/language"; +import { indentUnit, language as languageFacet } from "@codemirror/language"; import { registerSaveParticipant, remoteChange } from "@/editor/api"; import { createMemoryBackend, type MemoryBackend } from "../backend"; import { _resetLanguages } from "../cm/languages"; @@ -56,6 +56,33 @@ describe("DocumentManager + slots", () => { expect(formatOptionsOf(slot.currentState())).toEqual({ tabSize: 1, insertSpaces: false }); }); + it("detects the indents again after Reformat Code rewrote them", async () => { + const settings = { + editor: { + codeStyle: { + languages: { java: { useTab: true, tabSize: 1, indent: 1 } }, + detectIndents: true, + editorconfig: false, + }, + }, + }; + const { manager } = setup({ "/p/A.java": "class A {\n int x;\n}\n" }, settings); + const model = await manager.acquire("/p/A.java"); + const first = new EditorSlot(model, "t1", host); + const split = new EditorSlot(model, "t2", host, { from: first }); + expect(first.currentState().tabSize).toBe(4); + // Typing keeps the indents detected when the file was opened. + first.dispatchLocal({ changes: { from: 10, to: 14, insert: "\t" }, userEvent: "input.type" }); + expect(first.currentState().tabSize).toBe(4); + first.dispatchLocal({ changes: { from: 10, to: 11, insert: " " }, userEvent: "undo" }); + first.dispatchLocal({ changes: { from: 10, to: 14, insert: "\t" }, userEvent: "format.reformat" }); + for (const slot of [first, split]) { + expect(slot.currentState().tabSize).toBe(1); + expect(slot.currentState().facet(indentUnit)).toBe("\t"); + } + expect(useDocMeta.getState()["/p/A.java"]?.indent).toEqual({ tabSize: 1, insertSpaces: false }); + }); + it("shares one document between split slots and syncs edits to the backend", async () => { const { backend, manager, frame } = setup({ "/p/a.txt": "hello\n" }); const model = await manager.acquire("/p/a.txt"); diff --git a/src/editor/core/model/document.ts b/src/editor/core/model/document.ts index e0e8355f..d8843183 100644 --- a/src/editor/core/model/document.ts +++ b/src/editor/core/model/document.ts @@ -392,7 +392,7 @@ export class DocModel { /** Per-file indentation chosen from the status bar; wins over settings until the file is closed. */ indentOverride: { tabSize: number; insertSpaces: boolean } | null = null; - /** Indentation detected in the file's text at open time. */ + /** Indentation detected in the file's text at open time and after Reformat Code. */ detectedIndent: { insertSpaces: boolean; size?: number } | null = null; /** Last tree-sitter highlighting, for slots opened before the next refresh. */ private highlights: { counter: number; value: SyntaxHighlights } | null = null; @@ -418,6 +418,19 @@ export class DocModel { return effectiveIndent(this.settings(), this.info, this.editorconfig, null); } + /** + * Detects the indents again after Reformat Code rewrote them: tabs written by + * the formatter then render at the Code Style tab size and new lines indent + * like them, instead of following the indents detected at open time. + */ + redetectIndent() { + const detected = detectIndent(this.sync.localText.sliceString(0, 200_000)); + const current = this.detectedIndent; + if (detected?.insertSpaces === current?.insertSpaces && detected?.size === current?.size) return; + this.detectedIndent = detected; + this.applySettings(); + } + /** Soft wraps toggled for this file (wins over settings until the file is closed). */ softWrapOverride: boolean | null = null; diff --git a/src/editor/core/model/slot.ts b/src/editor/core/model/slot.ts index 54221ce6..aa20315b 100644 --- a/src/editor/core/model/slot.ts +++ b/src/editor/core/model/slot.ts @@ -199,6 +199,7 @@ export class EditorSlot { private onTransactions(trs: readonly Transaction[], view: EditorView) { view.update(trs); let selection = false; + let reformatted = false; let navigatedFrom: { line: number; col: number } | undefined; for (const tr of trs) { if (!navigatedFrom && tr.selection && isNavigation(tr)) { @@ -217,8 +218,10 @@ export class EditorSlot { ) this.host.onUserEdit(this, tr); } + if (tr.docChanged && tr.isUserEvent("format.reformat")) reformatted = true; if (tr.selection || tr.docChanged) selection = true; } + if (reformatted) this.model.redetectIndent(); if (selection) this.host.onSelection(this, navigatedFrom); } @@ -231,6 +234,7 @@ export class EditorSlot { const tr = this.state.update(spec); this.state = tr.state; if (tr.docChanged) this.model.onLocal(tr.changes, this); + if (tr.docChanged && tr.isUserEvent("format.reformat")) this.model.redetectIndent(); } /** Applies changes that are already in the model (other slot / backend). */ diff --git a/src/editor/lsp/format.ts b/src/editor/lsp/format.ts index e3184a94..61f4a3b5 100644 --- a/src/editor/lsp/format.ts +++ b/src/editor/lsp/format.ts @@ -161,7 +161,7 @@ async function format( } view.dispatch({ changes, - userEvent: "format", + userEvent: "format.reformat", annotations: isolateHistory.of("full"), scrollIntoView: false, }); @@ -235,7 +235,12 @@ function applyContributedFormat( if (opts.explicit) showHint(view, "No lines changed: content is already properly formatted"); return NOTHING; } - view.dispatch({ changes, userEvent: "format", annotations: isolateHistory.of("full"), scrollIntoView: false }); + view.dispatch({ + changes, + userEvent: "format.reformat", + annotations: isolateHistory.of("full"), + scrollIntoView: false, + }); return { applied: true, changes: changes.length, lines: formattedLines(state.doc, changes) }; }