editor: fixed tabs-insert-spaces

This commit is contained in:
Steven Hugg committed 2026-09-21 10:58:50 +02:00
1 parent 52d5f4a86c
commit a852b18ac0
4 files changed
+78 -7

No files matched your search

+2 -2
View File
@@ -38,8 +38,8 @@ For some platforms, the gutter shows additional information next to source lines
| Shortcut | Action |
| --- | --- |
| `Tab` | Insert a tab or indent the selected line(s) |
| `Shift+Tab` | Outdent the line(s) or selected range(s) |
| `Tab` | Insert spaces or a tab character (per settings) or indent selection |
| `Shift+Tab` | Unindent the current line or selection |
| `mod+Z` | Undo |
| `mod+Shift+Z` | Redo |
| `mod+F` | Find |
+2 -2
View File
@@ -5,7 +5,7 @@ import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlig
import { isProductionHost } from "../common/util";
import { setBarVisible } from "./shortcutbar";
import { debugHighlightTagsTooltip } from "./views/debug";
import { insertTabKeymap, smartIndentKeymap } from "./views/tabs";
import { insertSpacesKeymap, insertTabKeymap } from "./views/tabs";
declare var bootbox: BootboxStatic;
declare var $: JQueryStatic;
@@ -61,7 +61,7 @@ export function saveSettings(settings: EditorSettings) {
const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [
[tabSizeCompartment, s => [EditorState.tabSize.of(s.tabSize), indentUnit.of(" ".repeat(s.tabSize))]],
[tabsToSpacesCompartment, s => keymap.of(s.tabsToSpaces ? smartIndentKeymap : insertTabKeymap)],
[tabsToSpacesCompartment, s => keymap.of(s.tabsToSpaces ? insertSpacesKeymap : insertTabKeymap)],
[highlightSpecialCharsCompartment, s => s.highlightSpecialChars ? highlightSpecialChars() : []],
[highlightWhitespaceCompartment, s => s.highlightWhitespace ? highlightWhitespace() : []],
[highlightTrailingWhitespaceCompartment, s => s.highlightTrailingWhitespace ? highlightTrailingWhitespace() : []],
+30 -3
View File
@@ -1,9 +1,36 @@
import { indentLess, indentMore, insertTab } from "@codemirror/commands";
import { EditorSelection, EditorState } from "@codemirror/state";
import { getIndentUnit } from "@codemirror/language";
import { countColumn, EditorSelection, EditorState } from "@codemirror/state";
import { EditorView, KeyBinding } from "@codemirror/view";
export const smartIndentKeymap: KeyBinding[] = [
{ key: "Tab", run: indentMore },
// Spaces to insert when pressing Tab at document position 'head': up to the
// next multiple of the indent unit in leading whitespace, otherwise up to the
// next multiple of the tab size.
export function spacesToTabStop(state: EditorState, head: number): string {
const line = state.doc.lineAt(head);
const before = line.text.slice(0, head - line.from);
const col = countColumn(before, state.tabSize);
const stop = /^\s*$/.test(before) ? getIndentUnit(state) : state.tabSize;
return " ".repeat(stop - (col % stop));
}
// Insert spaces from the cursor up to the next tab stop.
// A non-empty selection indents the selected lines instead.
function insertSpacesToTabStop(view: EditorView): boolean {
const state = view.state;
if (state.selection.ranges.some(r => !r.empty)) return indentMore(view);
view.dispatch(state.changeByRange(range => {
const insert = spacesToTabStop(state, range.head);
return {
changes: { from: range.head, insert },
range: EditorSelection.cursor(range.head + insert.length),
};
}), { scrollIntoView: true, userEvent: "input" });
return true;
}
export const insertSpacesKeymap: KeyBinding[] = [
{ key: "Tab", run: insertSpacesToTabStop },
{ key: "Shift-Tab", run: indentLess },
];
+44
View File
@@ -0,0 +1,44 @@
import assert from "assert";
import { describe } from "mocha";
import { indentUnit } from "@codemirror/language";
import { EditorState } from "@codemirror/state";
import { spacesToTabStop } from "../../src/ide/views/tabs";
// Build a state from text with '|' marking the cursor; returns the spaces Tab would insert.
function tabAt(textWithCursor: string, tabSize: number, unit: number): string {
const head = textWithCursor.indexOf('|');
const state = EditorState.create({
doc: textWithCursor.replace('|', ''),
extensions: [EditorState.tabSize.of(tabSize), indentUnit.of(" ".repeat(unit))],
});
return spacesToTabStop(state, head);
}
describe('spacesToTabStop', () => {
it('should advance by indent unit in leading whitespace', () => {
assert.strictEqual(tabAt('|', 8, 2).length, 2);
assert.strictEqual(tabAt(' |', 8, 2).length, 2);
assert.strictEqual(tabAt(' |x', 8, 2).length, 2);
assert.strictEqual(tabAt(' |x', 8, 2).length, 1); // 3 -> 4
});
it('should advance to tab size stops after text', () => {
assert.strictEqual(tabAt('int x;|', 8, 2).length, 2); // 6 -> 8
assert.strictEqual(tabAt('int x; |', 8, 2).length, 8); // 8 -> 16
assert.strictEqual(tabAt('x|', 8, 2).length, 7);
});
it('should use the tab size stops when indent unit equals it', () => {
assert.strictEqual(tabAt('|', 8, 8).length, 8);
assert.strictEqual(tabAt(' |', 8, 8).length, 6);
});
it('should count literal tab characters as tab size columns', () => {
assert.strictEqual(tabAt('\t|', 8, 2).length, 2); // col 8 -> 10
assert.strictEqual(tabAt('\tx|', 8, 2).length, 7); // col 9 -> 16
});
it('should use the cursor line only', () => {
assert.strictEqual(tabAt(' foo\n|', 8, 2).length, 2);
assert.strictEqual(tabAt('a\n b|\nc', 4, 2).length, 1); // col 3 -> 4
});
it('should insert spaces only', () => {
assert.strictEqual(tabAt('a|', 4, 2), ' ');
});
});