Skip to content

Commit db8e3cd

Browse files
committed
Tab/Shift-Tab indentTabStop/deindentTabStop
1 parent 6c2a4d1 commit db8e3cd

1 file changed

Lines changed: 132 additions & 5 deletions

File tree

src/ide/views/tabs.ts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,143 @@
1-
import { indentLess, insertTab } from "@codemirror/commands";
21
import { indentUnit } from "@codemirror/language";
3-
import { EditorState, Extension } from "@codemirror/state";
4-
import { keymap } from "@codemirror/view";
2+
import { EditorSelection, EditorState, Extension } from "@codemirror/state";
3+
import { EditorView, keymap } from "@codemirror/view";
4+
import { AsmTabStops, columnAt } from "../../common/tabdetect";
55
import { tabStopsFacet } from "../settings";
66

7+
export const MAX_COLS = 300;
8+
9+
export function tabStopsToColumns(asmTabStops: AsmTabStops, tabSize: number, useTabs?: boolean): number[] {
10+
if (!useTabs) {
11+
const asmCols = [asmTabStops.opcodes, asmTabStops.operands, asmTabStops.comments].filter((n): n is number => n > 0);
12+
if (asmCols.length > 0) return asmCols;
13+
}
14+
const cols: number[] = [];
15+
for (let col = tabSize; col < MAX_COLS; col += tabSize) {
16+
cols.push(col);
17+
}
18+
return cols;
19+
}
20+
21+
function nextTabStop(col: number, columns: number[]): number {
22+
for (const stop of columns) {
23+
if (stop > col) return stop;
24+
}
25+
return col;
26+
}
27+
28+
function prevTabStop(col: number, columns: number[]): number {
29+
for (let i = columns.length - 1; i >= 0; i--) {
30+
if (columns[i] < col) return columns[i];
31+
}
32+
return 0;
33+
}
34+
35+
function indentTabStop(view: EditorView): boolean {
36+
const useTabs = view.state.facet(indentUnit) === '\t';
37+
const asmTabStop = view.state.facet(tabStopsFacet);
38+
const tabSize = view.state.facet(EditorState.tabSize);
39+
const columns = tabStopsToColumns(asmTabStop, tabSize, useTabs);
40+
41+
// Indents lines for selected ranges.
42+
if (view.state.selection.ranges.some(r => !r.empty)) {
43+
const changes: { from: number; to: number; insert: string }[] = [];
44+
const seen = new Set<number>();
45+
for (const range of view.state.selection.ranges) {
46+
const fromLine = view.state.doc.lineAt(range.from).number;
47+
const toLine = view.state.doc.lineAt(range.to).number;
48+
for (let ln = fromLine; ln <= toLine; ln++) {
49+
if (seen.has(ln)) continue;
50+
seen.add(ln);
51+
const line = view.state.doc.line(ln);
52+
let wsEnd = 0, wsCol = 0;
53+
for (; wsEnd < line.text.length; wsEnd++) {
54+
if (line.text[wsEnd] === '\t') wsCol = wsCol + tabSize - (wsCol % tabSize);
55+
else if (line.text[wsEnd] === ' ') wsCol++;
56+
else break;
57+
}
58+
if (useTabs) {
59+
const insertAt = line.from + wsEnd;
60+
changes.push({ from: insertAt, to: insertAt, insert: '\t' });
61+
} else {
62+
const targetCol = nextTabStop(wsCol, columns);
63+
if (targetCol > wsCol) {
64+
changes.push({ from: line.from, to: line.from + wsEnd, insert: ' '.repeat(targetCol) });
65+
}
66+
}
67+
}
68+
}
69+
if (changes.length > 0) {
70+
view.dispatch({ changes });
71+
}
72+
return true;
73+
}
74+
75+
// Otherwise, inserts whitespace to next tab stop.
76+
view.dispatch(view.state.changeByRange(range => {
77+
const line = view.state.doc.lineAt(range.head);
78+
const col = columnAt(line.text, range.head - line.from, tabSize);
79+
const insert = useTabs ? '\t' : ' '.repeat(nextTabStop(col, columns) - col);
80+
return {
81+
changes: { from: range.head, insert },
82+
range: EditorSelection.cursor(range.head + insert.length)
83+
};
84+
}));
85+
return true;
86+
}
87+
88+
function deindentTabStop(view: EditorView): boolean {
89+
const useTabs = view.state.facet(indentUnit) === '\t';
90+
const asmTabStop = view.state.facet(tabStopsFacet);
91+
const tabSize = view.state.facet(EditorState.tabSize);
92+
const columns = tabStopsToColumns(asmTabStop, tabSize, useTabs);
93+
94+
// Dedents lines for all ranges, regardless of selection.
95+
const changes: { from: number; to: number; insert: string }[] = [];
96+
const seen = new Set<number>();
97+
for (const range of view.state.selection.ranges) {
98+
const fromLine = view.state.doc.lineAt(range.from).number;
99+
const toLine = view.state.doc.lineAt(range.to).number;
100+
for (let ln = fromLine; ln <= toLine; ln++) {
101+
if (seen.has(ln)) continue;
102+
seen.add(ln);
103+
const line = view.state.doc.line(ln);
104+
let wsEnd = 0, wsCol = 0;
105+
for (; wsEnd < line.text.length; wsEnd++) {
106+
if (line.text[wsEnd] === '\t') wsCol = wsCol + tabSize - (wsCol % tabSize);
107+
else if (line.text[wsEnd] === ' ') wsCol++;
108+
else break;
109+
}
110+
if (wsCol === 0) continue;
111+
const targetCol = prevTabStop(wsCol, columns);
112+
// Walk forward to find the offset where we reach or pass targetCol.
113+
let col = 0, prevCol = 0, off = 0;
114+
for (; off < wsEnd && col < targetCol; off++) {
115+
prevCol = col;
116+
if (line.text[off] === '\t') col = col + tabSize - (col % tabSize);
117+
else col++;
118+
}
119+
if (col > targetCol) {
120+
// A tab overshot targetCol — replace it with spaces to land exactly on targetCol.
121+
changes.push({ from: line.from + off - 1, to: line.from + wsEnd, insert: ' '.repeat(targetCol - prevCol) });
122+
} else {
123+
// Landed exactly on targetCol — just delete the rest.
124+
changes.push({ from: line.from + off, to: line.from + wsEnd, insert: '' });
125+
}
126+
}
127+
}
128+
if (changes.length > 0) {
129+
view.dispatch({ changes });
130+
}
131+
return true;
132+
}
133+
7134
export function tabExtension(tabSize: number, tabsToSpaces: boolean, asmTabStops: AsmTabStops): Extension {
8135
return [
9136
EditorState.tabSize.of(tabSize),
10137
indentUnit.of(tabsToSpaces ? " ".repeat(tabSize) : "\t"),
11138
keymap.of([
12-
{ key: "Tab", run: insertTab },
13-
{ key: "Shift-Tab", run: indentLess }
139+
{ key: "Tab", run: indentTabStop },
140+
{ key: "Shift-Tab", run: deindentTabStop }
14141
]),
15142
tabStopsFacet.of(asmTabStops),
16143
];

0 commit comments

Comments
 (0)