Skip to content

Commit f929058

Browse files
authored
Merge pull request #6 from consistem/dot-syntax-auto-indent
feat: auto-indent dot syntax on enter for `.mac` / `.int` routines
2 parents 27f51cf + 3e63969 commit f929058

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

src/extension.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export const incLangId = "objectscript-macros";
2727
export const cspLangId = "objectscript-csp";
2828
export const outputLangId = "vscode-objectscript-output";
2929

30+
const dotPrefixRegex = /^(\s*(?:\.\s*)+)/;
31+
const dotIndentLanguages = new Set<string>([macLangId, intLangId]);
32+
const dotIndentSkipDocuments = new Set<string>();
33+
3034
import * as url from "url";
3135
import path = require("path");
3236
import {
@@ -1050,6 +1054,72 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
10501054
}
10511055
}
10521056

1057+
context.subscriptions.push(
1058+
vscode.workspace.onDidChangeTextDocument(async (event) => {
1059+
if (!dotIndentLanguages.has(event.document.languageId)) {
1060+
return;
1061+
}
1062+
1063+
const docUriString = event.document.uri.toString();
1064+
if (dotIndentSkipDocuments.has(docUriString)) {
1065+
return;
1066+
}
1067+
1068+
const editor = vscode.window.visibleTextEditors.find((e) => e.document === event.document);
1069+
if (!editor) {
1070+
return;
1071+
}
1072+
1073+
for (const change of event.contentChanges) {
1074+
if (!change.text.includes("\n")) {
1075+
continue;
1076+
}
1077+
1078+
const newLineNumber = change.range.start.line + 1;
1079+
if (newLineNumber >= event.document.lineCount || newLineNumber <= 0) {
1080+
continue;
1081+
}
1082+
1083+
const previousLine = event.document.lineAt(newLineNumber - 1).text;
1084+
const prefixMatch = previousLine.match(dotPrefixRegex);
1085+
if (!prefixMatch) {
1086+
continue;
1087+
}
1088+
1089+
let insertText = prefixMatch[1];
1090+
if (!insertText.endsWith(" ")) {
1091+
insertText += " ";
1092+
}
1093+
1094+
const remainder = previousLine.slice(prefixMatch[1].length);
1095+
if (remainder.startsWith(";")) {
1096+
insertText += ";";
1097+
}
1098+
1099+
const newLine = event.document.lineAt(newLineNumber);
1100+
if (newLine.text.startsWith(insertText)) {
1101+
continue;
1102+
}
1103+
1104+
const indentMatch = newLine.text.match(/^\s*/);
1105+
const indentLength = indentMatch ? indentMatch[0].length : 0;
1106+
const replaceRange = new vscode.Range(newLine.range.start, new vscode.Position(newLineNumber, indentLength));
1107+
1108+
dotIndentSkipDocuments.add(docUriString);
1109+
try {
1110+
await editor.edit(
1111+
(editBuilder) => {
1112+
editBuilder.replace(replaceRange, insertText);
1113+
},
1114+
{ undoStopBefore: false, undoStopAfter: false }
1115+
);
1116+
} finally {
1117+
dotIndentSkipDocuments.delete(docUriString);
1118+
}
1119+
}
1120+
})
1121+
);
1122+
10531123
openedClasses = workspaceState.get("openedClasses") ?? [];
10541124

10551125
/** The stringified URIs of all `isfs` documents that are currently open in a UI tab */

src/test/suite/extension.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ async function waitForIndexedDocument(documentName: string, workspaceFolderName:
2121
assert.fail(`Timed out waiting for '${documentName}' to be indexed in workspace folder '${workspaceFolderName}'.`);
2222
}
2323

24+
async function waitForCondition(predicate: () => boolean, timeoutMs = 1000, message?: string): Promise<void> {
25+
const start = Date.now();
26+
while (Date.now() - start < timeoutMs) {
27+
if (predicate()) {
28+
return;
29+
}
30+
await new Promise((resolve) => setTimeout(resolve, 10));
31+
}
32+
assert.fail(message ?? "Timed out waiting for condition");
33+
}
34+
2435
function getDefinitionTargets(definitions: (vscode.Location | vscode.DefinitionLink)[]): vscode.Uri[] {
2536
return definitions
2637
.map((definition) => ("targetUri" in definition ? definition.targetUri : definition.uri))
@@ -44,6 +55,42 @@ suite("Extension Test Suite", () => {
4455
assert.ok("All good");
4556
});
4657

58+
test("Dot-prefixed statements continue on newline", async () => {
59+
const document = await vscode.workspace.openTextDocument({
60+
language: "objectscript",
61+
content: " . Do ##class(Test).Run()",
62+
});
63+
const editor = await vscode.window.showTextDocument(document);
64+
try {
65+
await editor.edit((editBuilder) => {
66+
editBuilder.insert(document.lineAt(0).range.end, "\n");
67+
});
68+
await waitForCondition(() => document.lineCount > 1);
69+
await waitForCondition(() => document.lineAt(1).text.length > 0);
70+
assert.strictEqual(document.lineAt(1).text, " . ");
71+
} finally {
72+
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
73+
}
74+
});
75+
76+
test("Dot-prefixed semicolon comments continue on newline", async () => {
77+
const document = await vscode.workspace.openTextDocument({
78+
language: "objectscript",
79+
content: " . ; Comment",
80+
});
81+
const editor = await vscode.window.showTextDocument(document);
82+
try {
83+
await editor.edit((editBuilder) => {
84+
editBuilder.insert(document.lineAt(0).range.end, "\n");
85+
});
86+
await waitForCondition(() => document.lineCount > 1);
87+
await waitForCondition(() => document.lineAt(1).text.length > 0);
88+
assert.strictEqual(document.lineAt(1).text, " . ;");
89+
} finally {
90+
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
91+
}
92+
});
93+
4794
test("Go to Definition resolves to sibling workspace folder", async function () {
4895
this.timeout(10000);
4996
await waitForIndexedDocument("MultiRoot.Shared.cls", "shared");

0 commit comments

Comments
 (0)