|
|
@@ -15,6 +15,45 @@ function isIdentifierCharacter(ch: string | undefined): boolean {
|
|
|
return !!ch && /[A-Za-z0-9_]/.test(ch);
|
|
|
}
|
|
|
|
|
|
+/** A separator that commits a keyword (`procedure `, `procedure(`, ...). */
|
|
|
+function isCommitTrigger(ch: string): boolean {
|
|
|
+ return !isIdentifierCharacter(ch);
|
|
|
+}
|
|
|
+
|
|
|
+/** Separators after which a fresh PROCEDURE skeleton is offered. */
|
|
|
+function isSnippetTrigger(ch: string): boolean {
|
|
|
+ return ch === ' ' || ch === '\t' || ch === '(';
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Decide what typing `inserted` at `insertOffset` (post-change coords)
|
|
|
+ * does to the keyword immediately before it.
|
|
|
+ *
|
|
|
+ * Returns the keyword range plus its uppercase form when a commit
|
|
|
+ * trigger (any non-identifier character) was just typed after a
|
|
|
+ * complete lowercase keyword. While a word is still being typed
|
|
|
+ * (`beginning`) the inserted text starts with an identifier character
|
|
|
+ * and nothing happens — this is what prevents `BEGINning`.
|
|
|
+ */
|
|
|
+export function upcaseTrigger(
|
|
|
+ text: string, insertOffset: number, inserted: string,
|
|
|
+): { start: number; end: number; upper: string } | null {
|
|
|
+ if (!inserted || !isCommitTrigger(inserted[0])) return null;
|
|
|
+ let start = insertOffset;
|
|
|
+ while (start > 0 && isIdentifierCharacter(text[start - 1])) start--;
|
|
|
+ if (start >= insertOffset) return null;
|
|
|
+ const token = text.slice(start, insertOffset);
|
|
|
+ const upper = token.toUpperCase();
|
|
|
+ if (!KEYWORDS.has(upper) || token === upper) return null;
|
|
|
+ return { start, end: insertOffset, upper };
|
|
|
+}
|
|
|
+
|
|
|
+/** True when offset `start` is preceded only by whitespace on its line. */
|
|
|
+export function isLineStart(text: string, start: number): boolean {
|
|
|
+ const lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
|
|
+ return /^\s*$/.test(text.slice(lineStart, start));
|
|
|
+}
|
|
|
+
|
|
|
export function activate(context: vscode.ExtensionContext): void {
|
|
|
createDiagnostics(context);
|
|
|
const serverModule = context.asAbsolutePath('out/server.js');
|
|
|
@@ -60,40 +99,35 @@ export function activate(context: vscode.ExtensionContext): void {
|
|
|
}
|
|
|
|
|
|
for (const change of event.contentChanges) {
|
|
|
- const position = change.range.start.translate(0, change.text.length);
|
|
|
- const line = event.document.lineAt(position.line).text;
|
|
|
- let start = position.character;
|
|
|
-
|
|
|
- while (start > 0 && isIdentifierCharacter(line[start - 1])) {
|
|
|
- start--;
|
|
|
- }
|
|
|
-
|
|
|
- let end = position.character;
|
|
|
- while (end < line.length && isIdentifierCharacter(line[end])) {
|
|
|
- end++;
|
|
|
- }
|
|
|
-
|
|
|
- // Do not modify a token while it is still being typed.
|
|
|
- // This is what prevents "beginning" from becoming "BEGINning".
|
|
|
- if (end >= line.length || isIdentifierCharacter(line[end])) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- const token = line.slice(start, end);
|
|
|
- const upper = token.toUpperCase();
|
|
|
-
|
|
|
- if (!KEYWORDS.has(upper) || token === upper) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
+ // Post-change coordinates: the insertion point sits right after
|
|
|
+ // the just-typed text.
|
|
|
+ const doc = event.document;
|
|
|
+ const text = doc.getText();
|
|
|
+ const end = doc.offsetAt(change.range.start);
|
|
|
+ const hit = upcaseTrigger(text, end, change.text);
|
|
|
+ if (!hit) continue;
|
|
|
+ const keywordStart = doc.positionAt(hit.start);
|
|
|
applyingEdit = true;
|
|
|
try {
|
|
|
await editor.edit(builder => {
|
|
|
builder.replace(
|
|
|
- new vscode.Range(position.line, start, position.line, end),
|
|
|
- upper
|
|
|
+ new vscode.Range(keywordStart, doc.positionAt(hit.end)),
|
|
|
+ hit.upper
|
|
|
);
|
|
|
});
|
|
|
+ // A freshly upcased PROCEDURE at line start offers its skeleton:
|
|
|
+ // `procedure ` becomes a snippet asking name and parameters.
|
|
|
+ // The trigger separator is swallowed so the snippet owns spacing.
|
|
|
+ if (hit.upper === 'PROCEDURE' && change.text.length === 1 &&
|
|
|
+ isSnippetTrigger(change.text[0]) && isLineStart(text, hit.start)) {
|
|
|
+ const snippet = new vscode.SnippetString(
|
|
|
+ 'PROCEDURE ${1:ProcedureName}(${2});\nBEGIN\n ${3}\nEND ${1:ProcedureName};'
|
|
|
+ );
|
|
|
+ await editor.insertSnippet(
|
|
|
+ snippet,
|
|
|
+ new vscode.Range(keywordStart, doc.positionAt(hit.end + 1))
|
|
|
+ );
|
|
|
+ }
|
|
|
} finally {
|
|
|
applyingEdit = false;
|
|
|
}
|