Select Git revision
extension.ts
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
extension.ts 21.16 KiB
import * as fs from "fs";
import { existsSync } from "fs";
import * as vscode from "vscode";
import {
ExtensionContext
} from "vscode";
import {
LanguageClient,
LanguageClientOptions,
NotificationType,
ServerOptions,
State,
Trace,
TransportKind
} from "vscode-languageclient/node";
import { AnalysisResultsProvider, Result } from "./analysisResults";
import { ModestCommands } from "./commands";
import { getDocumentVars, initializeTools, ModestSidebarProvider } from "./sidebar";
import { ResultNotification, getNonce } from "./utils";
export let client: LanguageClient | undefined;
export let provider: ModestSidebarProvider;
export let analysisResultsProvider: AnalysisResultsProvider;
export let analysisResultsCompProvider: AnalysisResultsProvider;
export let treeView: vscode.TreeView<Result>;
export let extensionUri: vscode.Uri;
export let clientReady: boolean = false;
export let disposalQueue: Array<vscode.Disposable> = [];
export let resultHandlers: Array<(notification: ResultNotification) => void> = [];
export function modestExecutable(): string | undefined {
let executable: string | undefined = vscode.workspace
.getConfiguration("modest")
.get("executableLocation");
if (executable === undefined || executable === "") {
vscode.window
.showErrorMessage(
"It looks like you don't have the modest executable located yet.",
"Go to settings"
)
.then((result) => {
if (result !== undefined) {
vscode.commands.executeCommand(
"workbench.action.openSettings",
"modest.executableLocation"
);
}
});
return;
}
return executable;
}
function createClient(): LanguageClient | undefined {
// TODO: Check if path exists for a better user experience(so it doesn't spam the user with errors)
// Client (re)starts so cancel all running progress bars
while (disposalQueue.length > 0) {
disposalQueue.pop()?.dispose();
}
// The server is implemented in node
let serverExe = modestExecutable();
if (serverExe === undefined) {
return;
}
if (!existsSync(serverExe)) {
vscode.window
.showErrorMessage(
"The specified modest executable could not be found",
"Go to settings"
)
.then((result) => {
if (result !== undefined) {
vscode.commands.executeCommand(
"workbench.action.openSettings",
"modest.executableLocation"
);
}
});
return;
}
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
// run: { command: serverExe, args: ['-lsp', '-d'] },
run: {
command: serverExe,
args: ["startlspserver"],
transport: TransportKind.stdio,
},
// debug: { command: serverExe, args: ['-lsp', '-d'] }
debug: {
command: "dotnet",
transport: TransportKind.stdio,
args: [serverExe.replace(".exe", "") + ".dll", "startlspserver"], // Hacky
runtime: "",
},
};
// Options to control the language client
let clientOptions: LanguageClientOptions = {
// Register the server for plain text documents
documentSelector: [
{
pattern: "**/*.modest",
},
],
progressOnInitialization: true,
//synchronize: {
// Synchronize the setting section 'languageServerExample' to the server
// configurationSection: "languageServerExample",
// fileEvents: workspace.createFileSystemWatcher("**/*.modest"),
//},
};
// Create the language client and start the client.
const client = new LanguageClient(
"ModestExtension",
"Modest Extension",
serverOptions,
clientOptions
);
client.registerProposedFeatures();
client.trace = Trace.Verbose;
client.onReady().then(() => {
initializeTools();
clientReady = true;
client.onNotification(new NotificationType<ResultNotification>("modest/toolResult"), result => {
resultHandlers.forEach(handler => {
handler(result);
});
});
});
return client;
}
export function activate(context: ExtensionContext) {
client = createClient();
if (client) {
client.onDidChangeState(e => {
if (e.newState === State.Stopped) {
while (disposalQueue.length > 0) {
disposalQueue.pop()?.dispose();
}
}
}, null, context.subscriptions);
let langClient = client.start();
// Push the disposable to the context's subscriptions so that the
// client can be deactivated on extension deactivation
context.subscriptions.push(langClient);
}
context.subscriptions.push(ModestCommands.viewDot);
context.subscriptions.push(ModestCommands.viewDotSameWindow);
context.subscriptions.push(ModestCommands.dotZoomIn);
context.subscriptions.push(ModestCommands.dotZoomOut);
context.subscriptions.push(ModestCommands.resetDotPosition);
context.subscriptions.push(ModestCommands.dotSaveSvg);
context.subscriptions.push(ModestCommands.dotCopySvg);
context.subscriptions.push(ModestCommands.dotSaveDot);
context.subscriptions.push(ModestCommands.dotCopyDot);
extensionUri = context.extensionUri;
//#region listeners
vscode.workspace.onDidChangeConfiguration((a) => {
if (a.affectsConfiguration("modest.executableLocation")) {
clientReady = false;
if (client) {
client.stop();
client = undefined;
}
client = createClient();
if (client) {
client.onDidChangeState(e => {
if (e.newState === State.Stopped) {
while (disposalQueue.length > 0) {
disposalQueue.pop()?.dispose();
}
}
}, null, context.subscriptions);
let langClient = client.start();
context.subscriptions.push(langClient);
}
}
});
vscode.window.onDidChangeActiveTextEditor(textEditor => {
if (textEditor) {
getDocumentVars(textEditor.document);
}
});
vscode.workspace.onDidSaveTextDocument(textEditor => {
getDocumentVars(textEditor);
});
//#endregion
provider = new ModestSidebarProvider(context.extensionUri);
analysisResultsProvider = new AnalysisResultsProvider();
analysisResultsCompProvider = new AnalysisResultsProvider();
treeView = vscode.window.createTreeView("analysisResults", {
treeDataProvider: analysisResultsProvider
});
vscode.window.createTreeView("analysisResultsCompView", {
treeDataProvider: analysisResultsCompProvider,
});
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
ModestSidebarProvider.viewType,
provider
)
);
//#region register commands
vscode.commands.registerCommand(
"analysisResults.copyValue",
(res: Result) => copyToClipboard(res.getValue())
);
vscode.commands.registerCommand(
"analysisResults.copyItem",
(res: Result) => copyItemToClipboard(res)
);
vscode.commands.registerCommand(
"analysisResults.openInEditor",
(res: Result) => openInEditor(analysisResultsProvider, res)
);
vscode.commands.registerCommand(
"analysisResults.highlight",
(res: Result) => highlight(res)
);
vscode.commands.registerCommand(
"analysisResults.plotValue",
(res: Result) => plotValue(res, context)
);
vscode.commands.registerCommand(
"analysisResults.load",
() => loadResults(analysisResultsProvider)
);
vscode.commands.registerCommand(
"analysisResultsCompView.load",
() => loadResults(analysisResultsCompProvider)
);
vscode.commands.registerCommand(
"analysisResults.export",
() => exportResults(analysisResultsProvider)
);
vscode.commands.registerCommand(
"analysisResultsCompView.export",
() => exportResults(analysisResultsCompProvider)
);
vscode.commands.registerCommand(
"analysisResults.openFileInEditor",
() => openFileInEditor(analysisResultsProvider)
);
vscode.commands.registerCommand(
"analysisResultsCompView.openFileInEditor",
() => openFileInEditor(analysisResultsCompProvider)
);
vscode.commands.registerCommand(
"analysisResults.clear",
() => clearView(analysisResultsProvider)
);
vscode.commands.registerCommand(
"analysisResultsCompView.clearCompView",
() => clearView(analysisResultsCompProvider)
);
vscode.commands.registerCommand(
"analysisResults.openInCompView",
() => analysisResultsCompProvider.setJsonObject(analysisResultsProvider.getJsonObject())
);
//#endregion
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
}
return client.stop();
}
//#region analysis results functions
function copyToClipboard(text: string) {
vscode.env.clipboard.writeText(text);
vscode.window.showInformationMessage(`Copied ${text} to clipboard.`);
}
function copyItemToClipboard(res: Result) {
if (res.getValue() !== "") {
return copyToClipboard(`${res.getLabel()}: ${res.getValue()}`);
} else {
return copyToClipboard(res.getLabel());
}
}
function loadResults(provider: AnalysisResultsProvider) {
vscode.window.showOpenDialog().then(fileUri => {
if (fileUri) {
var fpath = fileUri[0].fsPath;
provider.setJsonPath(fpath);
vscode.window.showInformationMessage(`Opened ${fpath.split("/").pop()}.`);
}
});
}
function exportResults(prov: AnalysisResultsProvider) {
if (prov.getJsonObject()) {
const defaultUri = vscode.workspace.workspaceFolders
? vscode.Uri.parse(`${vscode.workspace.workspaceFolders[0].uri.fsPath}/analysis/${prov.getModestFile()}-results.json`)
: undefined;
vscode.window.showSaveDialog({ defaultUri: defaultUri }).then(f => {
if (f) {
fs.writeFileSync(f.fsPath, JSON.stringify(prov.getJsonObject()));
}
});
} else {
vscode.window.showErrorMessage("There are no analysis results to be exported.");
}
}
function openInEditor(prov: AnalysisResultsProvider, res: Result) {
if (vscode.workspace.workspaceFolders !== undefined) {
let wsPath = vscode.workspace.workspaceFolders[0].uri.fsPath;
const date = new Date().toISOString().split('.')[0];
const parent = res.getParentName() ? res.getParentName() + "-" : "";
const newPath = `${wsPath}/analysis/${prov.getModestFile()}-${parent}${res.getLabel()}.txt`;
var newText = res.getValue();
if (res.getIsPlottable()) {
const regex = new RegExp('(\\)|\\.),', 'gm');
newText = res.getValue().replace(regex, "$1,\n");
}
if (pathExists(newPath)) {
vscode.workspace.openTextDocument(newPath).then(async document => {
var overwrite = await vscode.window.showInformationMessage(`This value already has been saved as a text file.`,
"Open existing file", "Create new file", "Overwrite file");
if (overwrite === "Overwrite file") {
const edit = new vscode.WorkspaceEdit();
edit.replace(vscode.Uri.parse(newPath), new vscode.Range(
new vscode.Position(0, 0),
document.positionAt(document.getText().length - 1)),
newText
);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
}
} else if (overwrite === "Open existing file") {
vscode.window.showTextDocument(document);
} else if (overwrite === "Create new file") {
const newFile = vscode.Uri.parse(`untitled:${newPath.replace(".txt", "")}-${date}.txt`);
vscode.workspace.openTextDocument(newFile).then(async document => {
const edit = new vscode.WorkspaceEdit();
edit.insert(newFile, new vscode.Position(0, 0), newText);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
}
});
}
});
} else {
const newFile = vscode.Uri.parse("untitled:" + newPath);
vscode.workspace.openTextDocument(newFile).then(async document => {
const edit = new vscode.WorkspaceEdit();
edit.insert(newFile, new vscode.Position(0, 0), newText);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
}
});
}
}
}
function openFileInEditor(prov: AnalysisResultsProvider) {
if (prov.getJsonObject()) {
if (vscode.workspace.workspaceFolders !== undefined) {
let wsPath = vscode.workspace.workspaceFolders[0].uri.fsPath;
const date = new Date().toISOString().split('.')[0];
const newPath = `${wsPath}/analysis/${prov.getModestFile()}-results.json`;
const newText = JSON.stringify(prov.getJsonObject());
if (pathExists(newPath)) {
vscode.workspace.openTextDocument(newPath).then(async document => {
var overwrite = await vscode.window.showInformationMessage(`${newPath.split("/").pop()} already exists.`,
"Open existing file", "Create new file", "Overwrite file");
if (overwrite === "Overwrite file") {
const edit = new vscode.WorkspaceEdit();
edit.replace(vscode.Uri.parse(newPath), new vscode.Range(
new vscode.Position(0, 0),
document.positionAt(document.getText().length - 1)),
newText
);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
vscode.commands.executeCommand("editor.action.formatDocument", document.uri);
}
} else if (overwrite === "Open existing file") {
vscode.window.showTextDocument(document);
} else if (overwrite === "Create new file") {
const newFile = vscode.Uri.parse(`untitled:${newPath.replace(".json", "")}-${date}.json`);
vscode.workspace.openTextDocument(newFile).then(async document => {
const edit = new vscode.WorkspaceEdit();
edit.insert(newFile, new vscode.Position(0, 0), newText);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
vscode.commands.executeCommand("editor.action.formatDocument", document.uri);
}
});
}
});
} else {
const newFile = vscode.Uri.parse("untitled:" + newPath);
vscode.workspace.openTextDocument(newFile).then(async document => {
const edit = new vscode.WorkspaceEdit();
edit.insert(newFile, new vscode.Position(0, 0), newText);
const success = await vscode.workspace.applyEdit(edit);
if (success) {
vscode.window.showTextDocument(document);
vscode.commands.executeCommand("editor.action.formatDocument", document.uri);
}
});
}
}
} else {
vscode.window.showErrorMessage("There are no analysis results to be opened in an editor.");
}
}
async function clearView(prov: AnalysisResultsProvider) {
if (prov.getJsonObject()) {
const jsonObject = prov.getJsonObject();
prov.setJsonObject(null);
var choice = await vscode.window.showInformationMessage("Analysis results view has been cleared.", "Undo");
if (choice === "Undo") {
prov.setJsonObject(jsonObject);
}
}
}
function highlight(res: Result) {
res.highlight();
analysisResultsProvider.refresh();
analysisResultsCompProvider.refresh();
}
function plotValue(res: Result, context: ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'plotGraph',
`Plot of ${res.getLabel()} of ${res.getParentName()}`,
vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [context.extensionUri]
}
);
var dataPoints: {x: number, y: number}[] = [];
const value = res.getValue();
const regex = /\((\d+\.?d*),.(\d+\.?\d*)\)/gm;
let matches = (value.match(regex) || []).map(e => [e.replace(regex, '$1'), e.replace(regex, '$2')]);
matches.forEach((element) => {
dataPoints.push({x: Number(element[0]), y: Number(element[1])});
});
const chart = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'node_modules', 'chart.js', 'dist', 'Chart.js'));
const styleResetUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'media', 'reset.css'));
const styleVSCodeUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, 'media', 'vscode.css'));
const nonce = getNonce();
panel.webview.html = `
<html>
<head>
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
</head>
<body>
<canvas id="myChart"></canvas>
<button style="width: fit-content; padding: var(--input-margin-vertical) 0.8em;" onclick="save()">Save as Image</button>
<script nonce="${nonce}" src="${chart}"></script>
<script nonce="${nonce}">
var htmlStyle = document.documentElement.style;
var fgColor = htmlStyle.getPropertyValue("--vscode-editor-foreground");
var darkFgColor = htmlStyle.getPropertyValue("--vscode-sideBar-background");
var bgColor = htmlStyle.getPropertyValue("--vscode-editor-background");
Chart.defaults.global.defaultFontColor = fgColor;
Chart.defaults.scale.gridLines.color = fgColor;
Chart.plugins.register({
beforeDraw: function(chartInstance) {
var ctx = chartInstance.chart.ctx;
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, chartInstance.chart.width, chartInstance.chart.height);
}
});
var ctx = document.getElementById('myChart').getContext('2d');
var chart = new Chart(ctx, {
type: 'scatter',
data: {
datasets: [{
label: '${res.getParentName()}',
data: [${dataPoints.map(elem => `{x:${elem.x},y:${elem.y}}`)}],
backgroundColor: 'rgba(0, 152, 255, 1)',
pointRadius: 4,
borderColor: 'rgba(0, 114, 191, 1)',
borderWidth: 1,
fill: false,
showLine: true,
tension: 0
}]
},
options: {
scales: {
xAxes: [{
display: true,
gridLines: {
color: darkFgColor,
zeroLineColor: fgColor
}
}],
yAxes: [{
display: true,
gridLines: {
color: darkFgColor,
zeroLineColor: fgColor
}
}]
},
bezierCurve: false,
animation: {
onComplete: done
}
}
});
const vscode = acquireVsCodeApi();
var image;
function done(){
image = document.getElementById('myChart').toDataURL('image/png');
}
function save(){
if (image) {
vscode.postMessage({
command: 'save',
url: image.toString()
})
}
}
</script>
</body>
</html>
`;
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'save':
var data = message.url.replace(/^data:image\/\w+;base64,/, "");
var buf = new Buffer(data, 'base64');
saveImage(res, buf);
return;
}
},
undefined,
context.subscriptions
);
}
function saveImage(res: Result, url: Buffer) {
const defaultUri = vscode.workspace.workspaceFolders
? vscode.Uri.parse(`${vscode.workspace.workspaceFolders[0].uri.fsPath}/analysis/${res.getParentName()}-${res.getLabel()}.png`)
: undefined;
vscode.window.showSaveDialog({ defaultUri: defaultUri }).then(f => {
if (f) {
fs.writeFileSync(f.fsPath, url);
}
});
}
function pathExists(p: string): boolean {
try {
fs.accessSync(p);
} catch (err) {
return false;
}
return true;
}
//#endregion