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 { DotViewer } from "./dotViewer"; 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 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) => { 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/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) { try { disposalQueue.pop()?.dispose(); } catch { } } } }, 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); 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 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( "analysisResults.plotValue", (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( "analysisResults.saveAsJSON", () => exportResults(analysisResultsProvider) )); context.subscriptions.push(vscode.commands.registerCommand( "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() )); //#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.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`; 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 savePlot() { activePlotView.postMessage({ command: 'savePlot' }); } 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] } ); 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); 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> <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', url: image.toString() }) } } window.addEventListener('message', event => { const message = event.data; switch (message.command) { case 'savePlot': save() break; } }); </script> </body> </html> `; panel.webview.onDidReceiveMessage( message => { switch (message.command) { case 'saveImage': 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`) : 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