330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
import * as vscode from "vscode";
|
|
import { exec } from "child_process";
|
|
import path from "path";
|
|
import { ABBREV } from "./abbrev";
|
|
|
|
interface FC {
|
|
file: string;
|
|
line: number;
|
|
col: number;
|
|
}
|
|
|
|
interface TopEntry {
|
|
fc: FC;
|
|
name: string;
|
|
type: string;
|
|
}
|
|
interface TopData {
|
|
context: TopEntry[];
|
|
}
|
|
|
|
export function activate(context: vscode.ExtensionContext) {
|
|
let topData: undefined | TopData;
|
|
const diagnosticCollection =
|
|
vscode.languages.createDiagnosticCollection("newt");
|
|
|
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (!editor || event.document !== editor.document) return;
|
|
|
|
const changes = event.contentChanges;
|
|
if (changes.length === 0) return;
|
|
|
|
const lastChange = changes[changes.length - 1];
|
|
const text = lastChange.text;
|
|
// Check if the last change is a potential shortcut trigger
|
|
if (!text || !( " ')\\".includes(text) || text.startsWith('\n'))) return;
|
|
|
|
const document = editor.document;
|
|
const position = lastChange.range.end;
|
|
const lineText = document.lineAt(position.line).text;
|
|
const start = Math.max(0, position.character - 10);
|
|
const snippet = lineText.slice(start, position.character);
|
|
const m = snippet.match(/(\\[^ ]+)$/);
|
|
if (m) {
|
|
const cand = m[0];
|
|
console.log("cand", cand);
|
|
const replacement = ABBREV[cand];
|
|
console.log("repl", replacement);
|
|
if (replacement) {
|
|
const range = new vscode.Range(
|
|
position.line,
|
|
position.character - cand.length,
|
|
position.line,
|
|
position.character
|
|
);
|
|
editor.edit((editBuilder) => {
|
|
editBuilder.replace(range, replacement);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
function checkDocument(document: vscode.TextDocument) {
|
|
const fileName = document.fileName;
|
|
// Is there a better way to do this? It will get fussy with quoting and all plus it's not visible to the user.
|
|
const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
const cwd = workspaceFolder
|
|
? workspaceFolder.uri.fsPath
|
|
: path.dirname(fileName);
|
|
const config = vscode.workspace.getConfiguration("newt");
|
|
const cmd = config.get<string>("path", "node bootstrap/newt.js");
|
|
const command = `${cmd} --top ${fileName}`;
|
|
let st = +new Date();
|
|
exec(
|
|
command,
|
|
{ cwd, maxBuffer: 1024 * 1024 * 10 },
|
|
(err, stdout, _stderr) => {
|
|
console.log(`newt took ${+new Date() - st}`);
|
|
// I think I ignored 1 here because I wanted failure to launch
|
|
if (err && err.code !== 1)
|
|
vscode.window.showErrorMessage(`newt error: ${err}`);
|
|
|
|
// extract errors and messages from stdout
|
|
const lines = stdout.split("\n");
|
|
const diagnostics: vscode.Diagnostic[] = [];
|
|
|
|
if (err) {
|
|
let start = new vscode.Position(0, 0);
|
|
let end = new vscode.Position(0, 1);
|
|
let range =
|
|
document.getWordRangeAtPosition(start) ??
|
|
new vscode.Range(start, end);
|
|
const diag = new vscode.Diagnostic(
|
|
range,
|
|
"newt execution failed",
|
|
vscode.DiagnosticSeverity.Error
|
|
);
|
|
diagnostics.push(diag);
|
|
}
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line.startsWith("TOP:")) {
|
|
try {
|
|
topData = JSON.parse(line.slice(4));
|
|
} catch (e) {
|
|
console.error("Bad top data", e);
|
|
}
|
|
console.log("top data", topData);
|
|
}
|
|
const match = line.match(
|
|
/(INFO|WARN|ERROR) at (.*):(\d+):(\d+)--(\d+):(\d+):\s*(.*)/
|
|
);
|
|
if (match) {
|
|
let [_full, kind, file, line, column, eline, ecol, message] = match;
|
|
let lnum = +line - 1;
|
|
let cnum = +column - 1;
|
|
let elnum = +eline - 1;
|
|
let ecnum = +ecol - 1;
|
|
let start = new vscode.Position(lnum, cnum);
|
|
let end = new vscode.Position(elnum, ecnum);
|
|
let range = new vscode.Range(start, end);
|
|
if (file !== fileName) {
|
|
range = new vscode.Range(new vscode.Position(0,0), new vscode.Position(0,0));
|
|
}
|
|
// anything indented after the ERROR/INFO line are part of
|
|
// the message
|
|
while (lines[i + 1]?.match(/^( )/)) message += "\n" + lines[++i];
|
|
|
|
let severity;
|
|
|
|
if (kind === "ERROR") severity = vscode.DiagnosticSeverity.Error;
|
|
else if (kind === "WARN")
|
|
severity = vscode.DiagnosticSeverity.Warning;
|
|
else severity = vscode.DiagnosticSeverity.Information;
|
|
const diag = new vscode.Diagnostic(range, message, severity);
|
|
if (kind === "ERROR" || lnum > 0) diagnostics.push(diag);
|
|
}
|
|
}
|
|
diagnosticCollection.set(vscode.Uri.file(fileName), diagnostics);
|
|
}
|
|
);
|
|
}
|
|
|
|
const runNewt = vscode.commands.registerCommand("newt-vscode.check", () => {
|
|
const editor = vscode.window.activeTextEditor;
|
|
if (editor) {
|
|
const document = editor.document;
|
|
if (document.fileName.endsWith(".newt")) checkDocument(document);
|
|
}
|
|
});
|
|
|
|
context.subscriptions.push(
|
|
vscode.languages.registerDefinitionProvider(
|
|
{ language: "newt" },
|
|
{
|
|
provideDefinition(document, position, cancelToken) {
|
|
if (!topData) return null;
|
|
const wordRange = document.getWordRangeAtPosition(position);
|
|
if (!wordRange) return null;
|
|
const name = document.getText(wordRange);
|
|
let entry = topData.context.find((entry) => entry.name === name);
|
|
if (!entry) {
|
|
console.log(`entry ${name} not found`);
|
|
return null;
|
|
}
|
|
const root = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
if (!root) {
|
|
console.error("Workspace folder not found");
|
|
return null;
|
|
}
|
|
// let uri = vscode.Uri.file(path.join(root.uri.fsPath, entry.fc.file));
|
|
let fileName = entry.fc.file;
|
|
if (!fileName.startsWith('/')) fileName = path.join(root.uri.fsPath, fileName);
|
|
let uri = vscode.Uri.file(fileName);
|
|
console.log('root', root.uri.fsPath, 'file', entry.fc.file, 'uri', uri);
|
|
let start = new vscode.Position(entry.fc.line, entry.fc.col);
|
|
let range = new vscode.Range(start, start);
|
|
return new vscode.Location(uri, range);
|
|
},
|
|
}
|
|
)
|
|
);
|
|
context.subscriptions.push(
|
|
vscode.languages.registerCompletionItemProvider(
|
|
{ language: "newt" },
|
|
{
|
|
provideCompletionItems(document, position, token, context) {
|
|
if (!topData) return [];
|
|
const wordRange = document.getWordRangeAtPosition(position);
|
|
const prefix = wordRange ? document.getText(wordRange) : "";
|
|
const items: vscode.CompletionItem[] = [];
|
|
for (const entry of topData.context) {
|
|
if (entry.name.startsWith(prefix)) {
|
|
const item = new vscode.CompletionItem(entry.name, vscode.CompletionItemKind.Function);
|
|
item.detail = entry.type;
|
|
items.push(item);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
},
|
|
...'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'.split('')
|
|
)
|
|
);
|
|
context.subscriptions.push(
|
|
vscode.languages.registerHoverProvider(
|
|
{ language: "newt" },
|
|
{
|
|
provideHover(document, position, token) {
|
|
if (!topData) return null;
|
|
const wordRange = document.getWordRangeAtPosition(position);
|
|
if (!wordRange) return null;
|
|
const name = document.getText(wordRange);
|
|
let entry = topData.context.find((entry) => entry.name === name);
|
|
if (!entry) {
|
|
console.log(`entry ${name} not found`);
|
|
return null;
|
|
}
|
|
return new vscode.Hover(`${entry.name} : ${entry.type}`);
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
context.subscriptions.push(
|
|
vscode.languages.registerDocumentSymbolProvider(
|
|
{ language: "newt" },
|
|
{
|
|
provideDocumentSymbols(document, token) {
|
|
console.log("DSP!!");
|
|
const symbols: vscode.DocumentSymbol[] = [];
|
|
// we should ask the compiler
|
|
const RE_FUNC = /^(|data|record|class|instance)\s*([\d\w']+)\s*:/;
|
|
|
|
for (let i = 0; i < document.lineCount; i++) {
|
|
const line = document.lineAt(i);
|
|
const match = line.text.match(RE_FUNC);
|
|
if (match) {
|
|
const prefix = match[1];
|
|
const functionName = match[2];
|
|
const range = new vscode.Range(
|
|
new vscode.Position(i, 0),
|
|
new vscode.Position(i, line.text.length)
|
|
);
|
|
let type = vscode.SymbolKind.Function;
|
|
let details = "Function";
|
|
if (prefix === "class") type = vscode.SymbolKind.Interface;
|
|
if (prefix === "instance") type = vscode.SymbolKind.Class;
|
|
if (prefix === "record") type = vscode.SymbolKind.Struct;
|
|
if (prefix === "data") type = vscode.SymbolKind.Struct;
|
|
|
|
const symbol = new vscode.DocumentSymbol(
|
|
functionName,
|
|
details,
|
|
type,
|
|
range, // full range
|
|
range // selection
|
|
);
|
|
symbols.push(symbol);
|
|
}
|
|
}
|
|
console.log("SYMBOLS", symbols);
|
|
return symbols;
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
context.subscriptions.push(runNewt);
|
|
|
|
vscode.workspace.onDidSaveTextDocument((document) => {
|
|
if (document.fileName.endsWith(".newt"))
|
|
vscode.commands.executeCommand("newt-vscode.check");
|
|
});
|
|
vscode.workspace.onDidOpenTextDocument((document) => {
|
|
if (document.fileName.endsWith(".newt"))
|
|
vscode.commands.executeCommand("newt-vscode.check");
|
|
});
|
|
for (let document of vscode.workspace.textDocuments)
|
|
if (document.fileName.endsWith(".newt")) checkDocument(document);
|
|
|
|
context.subscriptions.push(diagnosticCollection);
|
|
context.subscriptions.push(
|
|
vscode.languages.registerCodeActionsProvider(
|
|
{ language: "newt" },
|
|
{
|
|
provideCodeActions(document, range, context, token) {
|
|
const actions: vscode.CodeAction[] = [];
|
|
for (const diagnostic of context.diagnostics) {
|
|
let {message,range} = diagnostic;
|
|
let m = message.match(/missing cases: (.*)/);
|
|
if (m) {
|
|
// A lot of this logic would also apply to case split.
|
|
let cons = m[1].split(', ');
|
|
const line = range.start.line;
|
|
const lineText = document.lineAt(line).text;
|
|
let m2 = lineText.match(/(.*=>?)/);
|
|
if (!m2) continue;
|
|
let s = range.start.character;
|
|
let e = range.end.character;
|
|
let a = lineText.slice(0,s);
|
|
let b = lineText.slice(e,m2[0].length);
|
|
let parens = a.endsWith('(') && b.startsWith(')');
|
|
let lines = cons.map(con =>
|
|
!parens && con.includes('_')
|
|
? `${a}(${con})${b} ?`
|
|
: `${a}${con}${b} ?`);
|
|
const fix = new vscode.CodeAction(
|
|
"Add missing cases",
|
|
vscode.CodeActionKind.QuickFix
|
|
);
|
|
fix.edit = new vscode.WorkspaceEdit();
|
|
// TODO - we should skip over subsequent lines that are indented more than the current.
|
|
const insertPos = new vscode.Position(line + 1, 0);
|
|
fix.edit.insert(document.uri, insertPos, lines.join('\n') + '\n');
|
|
fix.diagnostics = [diagnostic];
|
|
fix.isPreferred = true;
|
|
actions.push(fix);
|
|
}
|
|
}
|
|
return actions;
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
export function deactivate() {}
|