Skip to content
Snippets Groups Projects
extension.ts 22.4 KiB
Newer Older
import * as fs from "fs";
import { existsSync } from "fs";
import * as vscode from "vscode";
} from "vscode";
    LanguageClient,
    LanguageClientOptions,
} from "vscode-languageclient/node";
import { AnalysisResultsProvider, Result } from "./analysisResults";
import { getDocumentVars, initializeTools, ModestSidebarProvider } from "./sidebar";
Sytze de Witte's avatar
Sytze de Witte committed
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 disposalQueue: Array<vscode.Disposable> = [];
export let resultHandlers: Array<(notification: ResultNotification) => void> = [];
export let activePlotView: vscode.Webview;

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) => {
                    vscode.commands.executeCommand(
                        "workbench.action.openSettings",
                        "modest.executableLocation"
                    );
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) => {
                    vscode.commands.executeCommand(
                        "workbench.action.openSettings",
                        "modest.executableLocation"
                    );
    // 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: {
            transport: TransportKind.stdio,
            args: [serverExe.replace(".exe", "") + ".dll", "startlspserver"], // Hacky
    // 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.
        "ModestExtension",
        "Modest Extension",
        serverOptions,
    client.registerProposedFeatures();
    client.trace = Trace.Verbose;
        client.onNotification(new NotificationType<ResultNotification>("modest/result"), 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) {
                }
            }
        }, 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(DotViewer.viewDot);
    context.subscriptions.push(DotViewer.viewDotSameWindow);
    context.subscriptions.push(DotViewer.dotZoomIn);
    context.subscriptions.push(DotViewer.dotZoomOut);
    context.subscriptions.push(DotViewer.resetDotPosition);
    context.subscriptions.push(DotViewer.dotSaveSvg);
    context.subscriptions.push(DotViewer.dotCopySvg);
    context.subscriptions.push(DotViewer.dotSaveDot);
    context.subscriptions.push(DotViewer.dotCopyDot);
    vscode.workspace.onDidChangeConfiguration((a) => {
        if (a.affectsConfiguration("modest.executableLocation")) {
            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);
    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
        )
    );

    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.copyValue",
        (res: Result) => copyToClipboard(res.getValue())
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.copyItem",
        (res: Result) => copyItemToClipboard(res)
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.openInEditor",
        (res: Result) => openInEditor(analysisResultsProvider, res)
    ));
	context.subscriptions.push(vscode.commands.registerCommand(
		"analysisResults.highlight",
		(res: Result) => highlight(res)
	));
	context.subscriptions.push(vscode.commands.registerCommand(
Sytze de Witte's avatar
Sytze de Witte committed
		"analysisResults.plotValue",
Sytze de Witte's avatar
Sytze de Witte committed
		(res: Result) => plotValue(res, context)
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.load",
        () => loadResults(analysisResultsProvider)
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResultsCompView.load",
        () => loadResults(analysisResultsCompProvider)
    context.subscriptions.push(vscode.commands.registerCommand(
Sytze de Witte's avatar
Sytze de Witte committed
        "analysisResults.saveAsJSON",
        () => exportResults(analysisResultsProvider)
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
Sytze de Witte's avatar
Sytze de Witte committed
        "analysisResultsCompView.saveAsJSON",
        () => exportResults(analysisResultsCompProvider)
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.openFileInEditor",
        () => openFileInEditor(analysisResultsProvider)
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResultsCompView.openFileInEditor",
        () => openFileInEditor(analysisResultsCompProvider)
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.clear",
        () => clearView(analysisResultsProvider)
    ));
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResultsCompView.clearCompView",
        () => clearView(analysisResultsCompProvider)
    context.subscriptions.push(vscode.commands.registerCommand(
        "analysisResults.openInCompView",
        () => analysisResultsCompProvider.setJsonObject(analysisResultsProvider.getJsonObject())
    ));

	context.subscriptions.push(vscode.commands.registerCommand(
        "modest.savePlot",
		() => savePlot()
	));
export function deactivate(): Thenable<void> | undefined {
    if (!client) {
    }
    return client.stop();
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) {
s1995588's avatar
s1995588 committed
    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.joinPath(vscode.workspace.workspaceFolders[0].uri, `/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`;
Sytze de Witte's avatar
Sytze de Witte committed
		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);
        }
    }
Sytze de Witte's avatar
Sytze de Witte committed
function highlight(res: Result) {
	res.highlight();
	analysisResultsProvider.refresh();
	analysisResultsCompProvider.refresh();
}

function savePlot() {
	activePlotView.postMessage({ command: 'savePlot' });
}

Sytze de Witte's avatar
Sytze de Witte committed
function plotValue(res: Result, context: ExtensionContext) {
Sytze de Witte's avatar
Sytze de Witte committed
	const panel = vscode.window.createWebviewPanel(
        'plotGraph',
        `Plot of ${res.getLabel()} of ${res.getParentName()}`,
        vscode.ViewColumn.One,
		{
Sytze de Witte's avatar
Sytze de Witte committed
			enableScripts: true,
			localResourceRoots: [context.extensionUri]
Sytze de Witte's avatar
Sytze de Witte committed
		}
	);

	activePlotView = panel.webview;
	panel.onDidDispose(() => {
		vscode.commands.executeCommand('setContext', 'modest:plotViewFocused', false);
	});
	panel.onDidChangeViewState(
		e => {
			vscode.commands.executeCommand('setContext', 'modest:plotViewFocused', e.webviewPanel.active);
			if (e.webviewPanel.active) {
				activePlotView = e.webviewPanel.webview;
			}
		}
	);
	vscode.commands.executeCommand('setContext', 'modest:plotViewFocused', panel.active);

Sytze de Witte's avatar
Sytze de Witte committed
	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])});
	});

Sytze de Witte's avatar
Sytze de Witte committed
	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();
Sytze de Witte's avatar
Sytze de Witte committed

	panel.webview.html = `
	<html>
Sytze de Witte's avatar
Sytze de Witte committed
	<head>
		<link href="${styleResetUri}" rel="stylesheet">
        <link href="${styleVSCodeUri}" rel="stylesheet">
	</head>
Sytze de Witte's avatar
Sytze de Witte committed
	<body>
Sytze de Witte's avatar
Sytze de Witte committed
	<canvas id="myChart"></canvas>
	<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: 'saveImage',
Sytze de Witte's avatar
Sytze de Witte committed
					url: image.toString()
				})
			}
		}

		window.addEventListener('message', event => {
            const message = event.data;

            switch (message.command) {
                case 'savePlot':
                    save()
                    break;
            }
        });
Sytze de Witte's avatar
Sytze de Witte committed
	</script>
Sytze de Witte's avatar
Sytze de Witte committed
	</body>
	</html>
	`;
Sytze de Witte's avatar
Sytze de Witte committed

	panel.webview.onDidReceiveMessage(
        message => {
          switch (message.command) {
            case 'saveImage':
Sytze de Witte's avatar
Sytze de Witte committed
				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.joinPath(vscode.workspace.workspaceFolders[0].uri, `/analysis/${res.getParentName()}-${res.getLabel()}.png`)
Sytze de Witte's avatar
Sytze de Witte committed
        : undefined;
	vscode.window.showSaveDialog({ defaultUri: defaultUri }).then(f => {
		if (f) {
			fs.writeFileSync(f.fsPath, url);
		}
	});
Sytze de Witte's avatar
Sytze de Witte committed
}

function pathExists(p: string): boolean {
s1995588's avatar
s1995588 committed
    try {
        fs.accessSync(p);
    } catch (err) {
        return false;
s1995588's avatar
s1995588 committed
    return true;
Sytze de Witte's avatar
Sytze de Witte committed
//#endregion