|
1 | | -import { indentLess, insertTab } from "@codemirror/commands"; |
2 | 1 | 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"; |
5 | 5 | import { tabStopsFacet } from "../settings"; |
6 | 6 |
|
| 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 | + |
7 | 134 | export function tabExtension(tabSize: number, tabsToSpaces: boolean, asmTabStops: AsmTabStops): Extension { |
8 | 135 | return [ |
9 | 136 | EditorState.tabSize.of(tabSize), |
10 | 137 | indentUnit.of(tabsToSpaces ? " ".repeat(tabSize) : "\t"), |
11 | 138 | keymap.of([ |
12 | | - { key: "Tab", run: insertTab }, |
13 | | - { key: "Shift-Tab", run: indentLess } |
| 139 | + { key: "Tab", run: indentTabStop }, |
| 140 | + { key: "Shift-Tab", run: deindentTabStop } |
14 | 141 | ]), |
15 | 142 | tabStopsFacet.of(asmTabStops), |
16 | 143 | ]; |
|
0 commit comments