Skip to content

Commit 3c020f8

Browse files
committed
Alt-Shift-f formats asm and C-like source files
- Formats lines within selection range(s) - Formats entire doc if nothing is selected - Shift-Alt-F reformats the entire file, or just the line covered by the currently selected ranges(s): - C-like source is reindented by `cpp()` - asm source is formatted according to the current {opcodes, operands, comments} column settings - Affected lines have trailing whitespace removed - When formatting the entire file, Excess blank lines at the end of the file
1 parent b31f07f commit 3c020f8

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

src/ide/format.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { indentRange, indentUnit } from "@codemirror/language";
2+
import { ChangeSet, EditorState } from "@codemirror/state";
3+
import { EditorView } from "@codemirror/view";
4+
import { formatAsmLine } from "../common/format-asm";
5+
import { tabStopsFacet } from "./settings";
6+
7+
export function formatDocument(view: EditorView, isAsm: boolean) {
8+
let state = view.state;
9+
const changeSets: ChangeSet[] = [];
10+
11+
function apply(cs: ChangeSet) {
12+
changeSets.push(cs);
13+
// Update state so subsequent passes have correct line positions.
14+
state = state.update({ changes: cs }).state;
15+
}
16+
17+
function selectedLines(): Set<number> {
18+
const lines = new Set<number>();
19+
for (const range of sel.ranges) {
20+
const fromLine = state.doc.lineAt(range.from).number;
21+
const toLine = state.doc.lineAt(range.to).number;
22+
for (let i = fromLine; i <= toLine; i++) {
23+
lines.add(i);
24+
}
25+
}
26+
return lines;
27+
}
28+
29+
function isTargetLine(lineNum: number): boolean {
30+
return targetLines === null || targetLines.has(lineNum);
31+
}
32+
33+
// Determine which lines to format based on selections.
34+
const sel = state.selection;
35+
const hasSelection = sel.ranges.some(r => !r.empty);
36+
const targetLines = hasSelection ? selectedLines() : null;
37+
38+
// If 'Tab key insert spaces' is set, convert tabs to spaces.
39+
const indent = state.facet(indentUnit);
40+
const tabSize = state.facet(EditorState.tabSize);
41+
if (indent !== '\t') {
42+
const specs: { from: number, to: number, insert: string }[] = [];
43+
for (let i = 1; i <= state.doc.lines; i++) {
44+
if (!isTargetLine(i)) continue;
45+
const line = state.doc.line(i);
46+
if (line.text.indexOf('\t') >= 0) {
47+
let col = 0;
48+
let newText = "";
49+
for (const char of line.text) {
50+
if (char === '\t') {
51+
const nextTab = col + tabSize - (col % tabSize);
52+
newText += ' '.repeat(nextTab - col);
53+
col = nextTab;
54+
} else {
55+
newText += char;
56+
col++;
57+
}
58+
}
59+
specs.push({ from: line.from, to: line.to, insert: newText });
60+
}
61+
}
62+
if (specs.length > 0) {
63+
apply(state.changes(specs));
64+
}
65+
}
66+
67+
if (isAsm) {
68+
// Format asm languages using custom tab stops.
69+
const stops = state.facet(tabStopsFacet);
70+
if (stops.opcodes > 0 || stops.operands > 0 || stops.comments > 0) {
71+
const specs: { from: number, to: number, insert: string }[] = [];
72+
for (let i = 1; i <= state.doc.lines; i++) {
73+
if (!isTargetLine(i)) continue;
74+
const line = state.doc.line(i);
75+
const formatted = formatAsmLine(line.text, i, indent, tabSize, stops);
76+
if (formatted !== line.text) {
77+
specs.push({ from: line.from, to: line.to, insert: formatted });
78+
}
79+
}
80+
if (specs.length > 0) {
81+
apply(state.changes(specs));
82+
}
83+
}
84+
} else {
85+
// Format non-asm languages using CodeMirror's built-in indentation.
86+
if (hasSelection) {
87+
for (const range of sel.ranges) {
88+
const fromLine = state.doc.lineAt(range.from);
89+
const toLine = state.doc.lineAt(range.to);
90+
const indentChanges = indentRange(state, fromLine.from, toLine.to);
91+
if (!indentChanges.empty) {
92+
apply(indentChanges);
93+
}
94+
}
95+
} else {
96+
const indentChanges = indentRange(state, 0, state.doc.length);
97+
if (!indentChanges.empty) {
98+
apply(indentChanges);
99+
}
100+
}
101+
}
102+
103+
// Strip trailing whitespace.
104+
const trimSpecs: { from: number, to: number, insert: string }[] = [];
105+
for (let i = 1; i <= state.doc.lines; i++) {
106+
if (!isTargetLine(i)) continue;
107+
const line = state.doc.line(i);
108+
const trimmed = line.text.replace(/\s+$/, "");
109+
if (trimmed !== line.text) {
110+
trimSpecs.push({ from: line.from + trimmed.length, to: line.to, insert: "" });
111+
}
112+
}
113+
if (trimSpecs.length > 0) {
114+
apply(state.changes(trimSpecs));
115+
}
116+
117+
// Remove excess blank lines at end of file when formatting entire file.
118+
if (!hasSelection) {
119+
let lastNonEmpty = state.doc.lines;
120+
while (lastNonEmpty > 1 && state.doc.line(lastNonEmpty).text === '') {
121+
lastNonEmpty--;
122+
}
123+
if (lastNonEmpty < state.doc.lines) {
124+
const keepFrom = state.doc.line(lastNonEmpty + 1).to;
125+
const deleteTo = state.doc.length;
126+
apply(state.changes({ from: keepFrom, to: deleteTo }));
127+
}
128+
}
129+
130+
// Compose and dispatch all changes; selections are mapped automatically.
131+
if (changeSets.length > 0) {
132+
view.dispatch({ changes: changeSets.reduce((a, b) => a.compose(b)) });
133+
}
134+
}

src/ide/views/editors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { cobalt } from "../../themes/cobalt";
2020
import { disassemblyTheme } from "../../themes/disassemblyTheme";
2121
import { editorTheme } from "../../themes/editorTheme";
2222
import { mbo } from "../../themes/mbo";
23+
import { formatDocument } from "../format";
2324
import { loadSettings, registerEditor, settingsExtensions } from "../settings";
2425
import { clearBreakpoint, current_project, isAsmMode, lastDebugState, platform, runToPC } from "../ui";
2526
import { createAssetHeaderPlugin } from "./assetdecorations";
@@ -135,6 +136,17 @@ export class SourceEditor implements ProjectView {
135136
doc: text,
136137
extensions: [
137138

139+
// Use domEventHandler instead of keymap.of with "Shift-Alt-f"
140+
// to prevent macOS intercepting and inserting `Ï`.
141+
EditorView.domEventHandlers({
142+
keydown(event, view) {
143+
if (event.shiftKey && event.altKey && event.code === 'KeyF') {
144+
event.preventDefault();
145+
formatDocument(view, isAsm);
146+
}
147+
}
148+
}),
149+
138150
isAsm ? keymap.of([{
139151
key: "Enter",
140152
run: (view) => {

0 commit comments

Comments
 (0)