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>
This commit is contained in:
Jaw
2026-11-03 19:25:48 +08:00
committed by GitHub
parent 867774b88a
commit c599265ced
4 changed files with 53 additions and 4 deletions
+28 -1
View File
@@ -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");
+14 -1
View File
@@ -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;
+4
View File
@@ -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). */
+7 -2
View File
@@ -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) };
}