Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
extension.ts 13.91 KiB
import * as fs from "fs";
import { existsSync } from "fs";
import * as vscode from "vscode";
import {
    ExtensionContext
} from "vscode";
import {
    LanguageClient,
    LanguageClientOptions,
    ServerOptions,
    Trace,
    TransportKind
} from "vscode-languageclient/node";
import { AnalysisResultsProvider, Result } from "./analysisResults";
import { ModestCommands } from "./commands";
import { getDocumentVars, initializeTools, ModestSidebarProvider } from "./sidebar";

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 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)

    // 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()
    );
    return client;
}

export function activate(context: ExtensionContext) {
    client = createClient();
    if (client) {
        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.simCommand);
    context.subscriptions.push(ModestCommands.viewDot);
    extensionUri = context.extensionUri;

    //#region listeners
    vscode.workspace.onDidChangeConfiguration((a) => {
        if (a.affectsConfiguration("modest.executableLocation")) {
            if (client) {
                client.stop();
                client = undefined;
            }
            client = createClient();
            if (client) {
                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(analysisResultsProvider, res)
	);

	function highlight(prov: AnalysisResultsProvider, res: Result) {
		res.highlight();
		prov.refresh();
	}

    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`;
		const regex = new RegExp('(\\)|\\.),', 'gm');
		const 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 pathExists(p: string): boolean {
    try {
        fs.accessSync(p);
    } catch (err) {
        return false;
    }
    return true;
}
//#endregion