Skip to content
Snippets Groups Projects
Select Git revision
  • 0e4da7b67f34443171012efca5935c106fea337b
  • main default
  • sidebar
3 results

extension.ts

Blame
  • 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