From f54fcc3abf4fcea6f8dbed2814ffd22563f9b096 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Mon, 12 Feb 2024 10:50:59 -0800 Subject: [PATCH 1/9] experimental support for running chapel code in terminal Signed-off-by: Jade Abraham --- src/extension.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 0d43ea9..c7da525 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -34,6 +34,21 @@ export function activate(context: vscode.ExtensionContext) { }) ); + let env = process.env; + const chplhome = getChplHome(); + env.CHPL_HOME = chplhome!; + env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; + + context.subscriptions.push( + vscode.commands.registerCommand('chapel.runFile', (filepath: string) => { + const terminal = vscode.window.createTerminal({env: env}); + terminal.sendText("cd $CHPL_HOME && source util/setchplenv.bash && cd -") + terminal.sendText("chpl " + filepath + " -o a.out && ./a.out"); + terminal.show(); + }) + ); + + // Start the language server once the user opens the first text document context.subscriptions.push( vscode.workspace.onDidOpenTextDocument(async () => { From e0571465f601a44fa2e76c51509afccfba7b6775 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Mon, 12 Feb 2024 10:58:21 -0800 Subject: [PATCH 2/9] fix tabs Signed-off-by: Jade Abraham --- src/extension.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index c7da525..d848eac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -39,13 +39,13 @@ export function activate(context: vscode.ExtensionContext) { env.CHPL_HOME = chplhome!; env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; - context.subscriptions.push( + context.subscriptions.push( vscode.commands.registerCommand('chapel.runFile', (filepath: string) => { - const terminal = vscode.window.createTerminal({env: env}); + const terminal = vscode.window.createTerminal({env: env}); terminal.sendText("cd $CHPL_HOME && source util/setchplenv.bash && cd -") - terminal.sendText("chpl " + filepath + " -o a.out && ./a.out"); + terminal.sendText("chpl " + filepath + " -o a.out && ./a.out"); terminal.show(); - }) + }) ); From bc379f29469c83f6cdaf388b0060c6d6f348f6f9 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Thu, 22 Feb 2024 10:37:19 -0800 Subject: [PATCH 3/9] add activation events Signed-off-by: Jade Abraham --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index b1479ee..c499f5b 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,10 @@ "category": "chapel" } ], + "activationEvents": [ + "onLanguage:chapel", + "onLanguage:chapel-ast" + ], "configuration": [ { "type": "object", From 26b9fb5a5bf1c20d4d75fa5751f1f51a6bc5626b Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Thu, 22 Feb 2024 10:37:53 -0800 Subject: [PATCH 4/9] enable semantic highlighting by default for chapel Signed-off-by: Jade Abraham --- package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c499f5b..f551e40 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,12 @@ } } } - ] + ], + "configurationDefaults": { + "[chapel]": { + "editor.semanticHighlighting.enabled": true + } + } }, "scripts": { "vscode:prepublish": "npm run compile", From 46e3343405c4cc104aabce854b7f5661101234e5 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Thu, 22 Feb 2024 10:38:22 -0800 Subject: [PATCH 5/9] fix bug in chapel ast Signed-off-by: Jade Abraham --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f551e40..c2a56ba 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ }, { "language": "chapel-ast", - "scopeName": "source.ast", + "scopeName": "source.chapelAst", "path": "./syntaxes/chapel-ast.tmLanguage.json" } ], From 7f5ddef57b461293cb62211f83b957ce203fc244 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Fri, 23 Feb 2024 09:42:51 -0800 Subject: [PATCH 6/9] rewrite extension Signed-off-by: Jade Abraham --- .vscode/tasks.json | 18 ++ package.json | 26 ++- src/ChapelLanguageClient.ts | 317 +++++++++++++++++++++++++++++ src/ChplPaths.ts | 128 ++++++++++++ src/configuration.ts | 51 +++++ src/extension.ts | 393 ++++++++++++------------------------ 6 files changed, 664 insertions(+), 269 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 src/ChapelLanguageClient.ts create mode 100644 src/ChplPaths.ts create mode 100644 src/configuration.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8a491bd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/package.json b/package.json index c2a56ba..eaa98e8 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,16 @@ } ], "commands": [ + { + "command": "chapel.findChpl", + "title": "Find Chapel", + "category": "chapel" + }, + { + "command": "chapel.buildTools", + "title": "Build chpl-language-server and chplcheck", + "category": "chapel" + }, { "command": "chplcheck.restart", "title": "Restart chplcheck", @@ -107,11 +117,6 @@ "default": false, "description": "CHPL_DEVELOPER" }, - "chapel.chplcheck.path": { - "scope": "window", - "type": "string", - "description": "path for chplcheck" - }, "chapel.chplcheck.enable": { "scope": "window", "type": "boolean", @@ -127,17 +132,18 @@ "default": [], "description": "Extra arguments to pass to chplcheck" }, - "chapel.chpl-language-server.path": { - "scope": "window", - "type": "string", - "description": "path for chpl-language-server" - }, "chapel.chpl-language-server.enable": { "scope": "window", "type": "boolean", "default": true, "description": "Enable chpl-language-server" }, + "chapel.chpl-language-server.resolver": { + "scope": "window", + "type": "boolean", + "default": false, + "description": "Enable the dyno resover" + }, "chapel.chpl-language-server.args": { "scope": "window", "type": "array", diff --git a/src/ChapelLanguageClient.ts b/src/ChapelLanguageClient.ts new file mode 100644 index 0000000..ac51636 --- /dev/null +++ b/src/ChapelLanguageClient.ts @@ -0,0 +1,317 @@ +import { ToolConfig, getChplDeveloper } from "./configuration"; +import * as fs from "fs"; +import * as vscode from "vscode"; +import * as vlc from "vscode-languageclient/node"; +import { checkToolPath, checkChplHome, cloneEnv } from "./ChplPaths"; +import * as path from "path"; + +export enum LanguageClientState { + DISABLED, + STOPPED, + STARTING, + RUNNING, + ERRORED, +} + +class ErrorHandlingClient extends vlc.LanguageClient { + errorHandler: (message: string, fatal: boolean, data?: any) => void; + constructor( + name: string, + serverOptions: vlc.ServerOptions, + clientOptions: vlc.LanguageClientOptions, + errorHandler: (message: string, fatal: boolean, data?: any) => void + ) { + super(name, serverOptions, clientOptions); + this.errorHandler = errorHandler; + } + + override info(message: string, data?: any): void { + this.errorHandler(message, false, data); + } + override warn(message: string, data?: any): void { + this.errorHandler(message, false, data); + } + override error(message: string, data?: any): void { + this.errorHandler(message, true, data); + } +} + +export abstract class ChapelLanguageClient { + chplhome: string; + protected config_: ToolConfig; + name: string; + state: LanguageClientState; + tool_path: string; + client: ErrorHandlingClient | undefined; + logger: vscode.LogOutputChannel; + + constructor( + chplhome: string, + config: ToolConfig, + name: string, + logger: vscode.LogOutputChannel + ) { + this.chplhome = chplhome; + this.config_ = config; + this.name = name; + this.state = this.config_.enable + ? LanguageClientState.STOPPED + : LanguageClientState.DISABLED; + this.tool_path = this.getToolPath(); + this.client = undefined; + this.logger = logger; + } + + protected abstract getToolPath(): string; + + get config(): ToolConfig { + return this.config_; + } + async resetConfig(chplhome: string, config: ToolConfig) { + await this.stop(); + this.chplhome = chplhome; + this.config_ = config; + this.state = this.config_.enable + ? LanguageClientState.STOPPED + : LanguageClientState.DISABLED; + this.tool_path = this.getToolPath(); + await this.start(); + } + + setErrorState() { + this.state = LanguageClientState.ERRORED; + } + + clearError(): void { + this.state = LanguageClientState.STOPPED; + } + + private errorFindTools() { + // if invalid chplhome, prompt user to set it + // if missing tool path, warn user that we can't find it, tell them to not override the path or upgrade their chapel version + // otherwise, its likely the tools arent built, so prompt the user to build them + + if (checkChplHome(this.chplhome) !== undefined) { + vscode.window + .showErrorMessage( + "CHPL_HOME is either missing or incorrect, make sure the path is correct", + "Find CHPL_HOME", + "Show Log", + "Ok" + ) + .then((value) => { + if (value === "Find CHPL_HOME") { + vscode.commands.executeCommand("chapel.findChpl"); + } else if (value === "Show Log") { + this.logger.show(); + } + }); + } else if (checkToolPath(this.tool_path) !== undefined) { + vscode.window + .showErrorMessage( + `${this.name} does not exist in the CHPL_HOME directory, make sure you are using the correct version of Chapel`, + "Show Log", + "Ok" + ) + .then((value) => { + if (value === "Show Log") { + this.logger.show(); + } + }); + } else { + vscode.window + .showErrorMessage( + `${this.name} encountered an error, this is likely because ${this.name} is not installed. Double check that ${this.name} is built.`, + "Build Tools", + "Show Log", + "Ok" + ) + .then((value) => { + if (value === "Build Tools") { + vscode.commands.executeCommand(`chapel.buildTools`, this.chplhome); + } else if (value === "Show Log") { + this.logger.show(); + } + }); + } + } + + protected abstract alwaysArguments(): Array; + + start(): Promise { + if (this.state !== LanguageClientState.STOPPED) { + return Promise.resolve(); + } + this.state = LanguageClientState.STARTING; + let toolPathError = checkToolPath(this.tool_path); + if (toolPathError !== undefined) { + this.logger.error(toolPathError); + this.errorFindTools(); + this.state = LanguageClientState.STOPPED; + return Promise.reject(); + } + + let env = cloneEnv(); + env.CHPL_HOME = this.chplhome; + env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; + + let args = this.alwaysArguments(); + args.push(...this.config.args); + + this.logger.info(`${this.name} path: '${this.tool_path}'`); + this.logger.info(`${this.name} args: '${args}'`); + + const serverOptions: vlc.ServerOptions = { + command: this.tool_path, + args: args, + options: { + cwd: this.chplhome, + env: env, + }, + }; + this.logger.debug( + `${this.name} server options ${JSON.stringify( + serverOptions, + undefined, + 2 + )}` + ); + + const errorLogger = (message: string) => { + this.logger.error(`${this.name}: ${message}`); + }; + const errorHandler = ( + message: string, + fatal: boolean = false, + data?: any + ) => { + if (data) { + errorLogger(`${message} - ${JSON.stringify(data, undefined, 2)}`); + } else { + errorLogger(message); + } + + if (data && "code" in data) { + const code = data.code; + if (code == -32097) { + this.errorFindTools(); + } + } + + if (fatal) { + this.stop().finally(() => { + this.state = LanguageClientState.ERRORED; + + vscode.window + .showErrorMessage( + `${this.name} encountered an unrecoverable error`, + "Restart", + "Show Log", + "Ok" + ) + .then((value) => { + if (value === "Restart") { + this.restart(); + } else if (value === "Show Log") { + this.logger.show(); + } + }); + }); + } + }; + + const clientOptions: vlc.LanguageClientOptions = { + documentSelector: [ + { + scheme: "file", + language: "chapel", + }, + ], + outputChannel: this.logger, + connectionOptions: { + maxRestartCount: 0, + }, + initializationFailedHandler: () => { + // always return false to trigger other error handlers + return false; + }, + errorHandler: { + error: (error) => { + errorHandler(error.message, true); + return { action: vlc.ErrorAction.Shutdown, handled: true }; + }, + closed: () => { + errorHandler("Server closed", true); + return { action: vlc.CloseAction.DoNotRestart, handled: true }; + }, + }, + }; + this.logger.debug( + `${this.name} server options ${JSON.stringify( + serverOptions, + undefined, + 2 + )}` + ); + + this.client = new ErrorHandlingClient( + this.name, + serverOptions, + clientOptions, + errorHandler + ); + + this.client.onDidChangeState((event) => { + if (event.newState === vlc.State.Stopped) { + this.state = LanguageClientState.STOPPED; + } else if (event.newState === vlc.State.Running) { + this.state = LanguageClientState.RUNNING; + } + }); + + return this.client.start(); + } + + stop(): Promise { + return new Promise((resolve, reject) => { + if (this.client && this.state === LanguageClientState.RUNNING) { + this.client.stop().catch(reject); + this.client.dispose(); + this.client = undefined; + } + resolve(); + }); + } + + restart(): Promise { + return new Promise((resolve, reject) => { + this.stop() + .then(() => { + this.clearError(); + this.start().then(resolve).catch(reject); + }) + .catch(reject); + }); + } +} + +export class ChplCheckClient extends ChapelLanguageClient { + getToolPath(): string { + return path.join(this.chplhome, "tools", "chplcheck", "chplcheck"); + } + alwaysArguments(): Array { + return ["--lsp"]; + } +} +export class CLSClient extends ChapelLanguageClient { + getToolPath(): string { + return path.join(this.chplhome, "tools", "chpl-language-server", "chpl-language-server"); + } + alwaysArguments(): Array { + let args = []; + if ("resolver" in this.config && this.config.resolver) { + args.push("--resolver"); + } + return args; + } +} diff --git a/src/ChplPaths.ts b/src/ChplPaths.ts new file mode 100644 index 0000000..03e21e3 --- /dev/null +++ b/src/ChplPaths.ts @@ -0,0 +1,128 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as vscode from "vscode"; + +export function checkChplHome( + chplhome: string | undefined +): string | undefined { + if (chplhome === undefined || chplhome === "") { + return "CHPL_HOME is not set"; + } + + if (!path.isAbsolute(chplhome)) { + return `CHPL_HOME (${chplhome}) is not an absolute path`; + } + + if (!fs.existsSync(chplhome) || !fs.statSync(chplhome).isDirectory()) { + return `CHPL_HOME (${chplhome}) does not exist`; + } + + const subdirs = [ + "util", + "compiler", + "frontend", + "runtime", + "modules", + "tools", + ]; + let isChplDir = true; + for (const subdir of subdirs) { + const subdir_path = path.join(chplhome, subdir); + if ( + !fs.existsSync(subdir_path) || + !fs.statSync(subdir_path).isDirectory() + ) { + isChplDir = false; + break; + } + } + if (!isChplDir) { + return `CHPL_HOME (${chplhome}) is not a Chapel directory`; + } + + return undefined; +} + +export function checkToolPath(tool_path: string): string | undefined { + if (!fs.existsSync(tool_path) || !fs.statSync(tool_path).isFile()) { + return `${tool_path} does not exist`; + } + return undefined; +} + +function searchDirectoryForChplHome(dir: string, depth: number = 1): string[] { + let chplhomes: string[] = []; + if (depth > 0) { + if (checkChplHome(dir) === undefined) { + chplhomes.push(dir); + } else if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) { + fs.readdirSync(dir).forEach((subdir) => { + const subdir_path = path.join(dir, subdir); + chplhomes.push(...searchDirectoryForChplHome(subdir_path, depth - 1)); + }); + } + } + return chplhomes; +} + +export function findPossibleChplHomes(): string[] { + let possibleChplHomes: string[] = []; + const chplhomeFromEnv = process.env["CHPL_HOME"]; + if (checkChplHome(chplhomeFromEnv) === undefined) { + possibleChplHomes.push(chplhomeFromEnv as string); + } + + // TODO: it would be nice to walk PATH and look for chpl + // then we could execute `chpl --print-chpl-home` to get the chplhome + // but we cannot execute shell commands and get their result with the vscode api + + // as a best effort, we find chpl in the PATH and check if chplhome is the parent directory + const PATH = process.env["PATH"]; + const paths_to_check = PATH?.split(":") ?? []; + for (const p of paths_to_check) { + const chpl_path = path.join(p, "chpl"); + if (fs.existsSync(chpl_path) && fs.statSync(chpl_path).isFile()) { + const chplhome = path.dirname(path.dirname(chpl_path)); + if ( + checkChplHome(chplhome) === undefined && + possibleChplHomes.indexOf(chplhome) === -1 + ) { + possibleChplHomes.push(chplhome); + } + } + } + // as a best effort, we can also check `/opt` and `/opt/homebrew/Cellar/chapel` for chplhome, searching to a depth of 3 + const dirs_to_check = ["/opt", "/opt/homebrew/Cellar/chapel/"]; + for (const dir of dirs_to_check) { + possibleChplHomes.push(...new Set(searchDirectoryForChplHome(dir, 3))); + } + + return possibleChplHomes; +} + +export function cloneEnv() { + interface Dict { + [k: string]: T; + } + let env: Dict = {}; + for (const e in process.env) env[e] = process.env[e] ?? ""; + return env; +} + +export function buildTools(chplhome: string) { + let env = cloneEnv(); + const term = vscode.window.createTerminal({ cwd: chplhome, env: env }); + term.sendText(`make chpl-language-server || exit 1 && make chplcheck || exit 1 && exit 0`); + term.show(); + vscode.window.onDidChangeTerminalState((e) => { + if (e === term && e.exitStatus !== undefined) { + if (e.exitStatus.code === 0) { + vscode.window.showInformationMessage("Build complete"); + vscode.commands.executeCommand("chplcheck.restart"); + vscode.commands.executeCommand("chpl-language-server.restart"); + } else { + vscode.window.showWarningMessage(`Build failed, try running 'export CHPL_HOME=${chplhome} && make chpl-language-server && make chplcheck' in the CHPL_HOME directory to see the error message.`); + } + } + }) +} diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 0000000..a98e3cf --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,51 @@ + +import * as vscode from "vscode"; + +export interface ToolConfig { + args: Array; + enable: boolean; +} +const ToolConfigDefault: ToolConfig = {args: [], enable: false}; + +export type ChplCheckConfig = ToolConfig; +const ChplCheckConfigDefault: ChplCheckConfig = {...ToolConfigDefault }; + +export interface CLSConfig extends ToolConfig { + resolver: boolean; +} +const CLSConfigDefault: CLSConfig = {...ToolConfigDefault, resolver: false}; + +const configScope = "chapel"; + +export function getChplHome(): string { + const config = vscode.workspace.getConfiguration(configScope); + const chplhome = config.get("CHPL_HOME"); + return chplhome ?? ""; +} +export function setChplHome(chplhome: string) { + const config = vscode.workspace.getConfiguration(configScope); + + // when updating CHPL_HOME, we should update the workspace config if its been overriden, otherwise set it globally for all users + if(config.inspect("CHPL_HOME")?.workspaceValue !== undefined) { + config.update("CHPL_HOME", chplhome, vscode.ConfigurationTarget.Workspace); + } else { + config.update("CHPL_HOME", chplhome, vscode.ConfigurationTarget.Global); + } +} +export function getChplDeveloper(): boolean { + const config = vscode.workspace.getConfiguration("chapel"); + const devel = config.get("CHPL_DEVELOPER"); + return devel ?? false; +} + +export function getChplCheckConfig(): ChplCheckConfig { + const config = vscode.workspace.getConfiguration(configScope); + const chplcheck = config.get("chplcheck") ?? ChplCheckConfigDefault; + return chplcheck; +} + +export function getCLSConfig(): CLSConfig { + const config = vscode.workspace.getConfiguration(configScope); + const cls = config.get("chpl-language-server") ?? CLSConfigDefault; + return cls; +} diff --git a/src/extension.ts b/src/extension.ts index d848eac..0851771 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,287 +1,162 @@ -import * as path from "path"; import * as vscode from "vscode"; - -let chplcheckClient: LanguageClient | undefined; -let chplcheckClientStarting = false; -let clsClient: LanguageClient | undefined; -let clsClientStarting = false; +import { + getChplHome, + getCLSConfig, + getChplCheckConfig, + setChplHome, +} from "./configuration"; +import { ChplCheckClient, CLSClient } from "./ChapelLanguageClient"; +import { buildTools, checkChplHome, findPossibleChplHomes } from "./ChplPaths"; + +let chplcheckClient: ChplCheckClient; +let clsClient: CLSClient; let logger: vscode.LogOutputChannel; -import { - LanguageClient, - LanguageClientOptions, - ServerOptions, - State, -} from "vscode-languageclient/node"; +function showChplHomeMissingError(errorString?: string) { + if (errorString) { + logger.error(errorString); + } + vscode.window + .showErrorMessage( + "CHPL_HOME is either missing or incorrect, make sure the path is correct", + "Find CHPL_HOME", + "Show Log", + "Ok" + ) + .then((value) => { + if (value === "Find CHPL_HOME") { + vscode.commands.executeCommand("chapel.findChpl"); + } else if (value === "Show Log") { + logger.show(); + } + }); +} + +function pickMyOwnChplHome() { + vscode.window + .showInputBox({ + placeHolder: "Enter the path to CHPL_HOME", + }) + .then((selection) => { + if (selection !== undefined) { + setChplHome(selection); + } + }); +} +function selectChplHome(choices: string[]) { + const pickMyOwn = "None of these - let me pick my own"; + choices.push(pickMyOwn); + + vscode.window + .showQuickPick(choices, { + placeHolder: "Select a CHPL_HOME", + }) + .then((selection) => { + if (selection === pickMyOwn) { + pickMyOwnChplHome(); + } else if (selection !== undefined) { + setChplHome(selection); + } + }); +} -// This method is called when your extension is activated -// Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { logger = vscode.window.createOutputChannel("chapel", { log: true }); - logger.info("Extension activated."); + logger.info("Chapel extension activated"); - // Restart language server command context.subscriptions.push( - vscode.commands.registerCommand("chplcheck.restart", async () => { - logger.info("restarting chplcheck server"); - await startChplCheck(); + vscode.commands.registerCommand("chapel.findChpl", async () => { + // show a selection to let the user select the proper CHPL_HOME + let choices = findPossibleChplHomes(); + if (choices.length === 0) { + pickMyOwnChplHome(); + } else { + selectChplHome(choices); + } }) ); context.subscriptions.push( - vscode.commands.registerCommand("chpl-language-server.restart", async () => { - logger.info("restarting chpl-language-server server"); - await startCLS(); - }) + vscode.commands.registerCommand( + "chapel.buildTools", + async (chplhome?: string) => { + if (chplhome === undefined) { + chplhome = getChplHome(); + } + if (checkChplHome(chplhome) === undefined) { + buildTools(chplhome); + } else { + vscode.window.showWarningMessage( + `Unable to build automatically, please build manually` + ); + } + } + ) ); - let env = process.env; const chplhome = getChplHome(); - env.CHPL_HOME = chplhome!; - env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; + const chplHomeError = checkChplHome(chplhome); + if (chplHomeError !== undefined) { + showChplHomeMissingError(chplHomeError); + } - context.subscriptions.push( - vscode.commands.registerCommand('chapel.runFile', (filepath: string) => { - const terminal = vscode.window.createTerminal({env: env}); - terminal.sendText("cd $CHPL_HOME && source util/setchplenv.bash && cd -") - terminal.sendText("chpl " + filepath + " -o a.out && ./a.out"); - terminal.show(); - }) + chplcheckClient = new ChplCheckClient( + chplhome, + getChplCheckConfig(), + "chplcheck", + logger + ); + clsClient = new CLSClient( + chplhome, + getCLSConfig(), + "chpl-language-server", + logger ); + if (chplHomeError !== undefined) { + chplcheckClient.setErrorState(); + clsClient.setErrorState(); + } - // Start the language server once the user opens the first text document + // Restart language server command context.subscriptions.push( - vscode.workspace.onDidOpenTextDocument(async () => { - if (!chplcheckClient) { - await startChplCheck(); + vscode.commands.registerCommand("chplcheck.restart", async () => { + logger.info("restarting chplcheck server"); + chplcheckClient.restart(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + "chpl-language-server.restart", + async () => { + logger.info("restarting chpl-language-server server"); + clsClient.restart(); } - if (!clsClient) { - await startCLS(); + ) + ); + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration("chapel")) { + const chplhome = getChplHome(); + const chplHomeError = checkChplHome(chplhome); + if (chplHomeError !== undefined) { + showChplHomeMissingError(chplHomeError); + } + Promise.all([ + chplcheckClient.resetConfig(chplhome, getChplCheckConfig()), + clsClient.resetConfig(chplhome, getCLSConfig()), + ]); } }) ); - // formatter.addFormatter(); + // Start the language server once the user opens the first text document + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(async () => { + Promise.all([chplcheckClient.start(), clsClient.start()]); + }) + ); } -// This method is called when your extension is deactivated export function deactivate() { - return Promise.all([ - stopLangServer(chplcheckClient), - stopLangServer(clsClient), - ]); -} - -function getChplHome(): string | undefined { - const config = vscode.workspace.getConfiguration("chapel"); - const chplhome = config.get("CHPL_HOME"); - - return chplhome; -} - -function getChplDeveloper(): boolean { - const config = vscode.workspace.getConfiguration("chapel"); - const devel = config.get("CHPL_DEVELOPER"); - - return devel ?? false; -} - -interface ToolConfig { - args?: Array; - path?: string; - enable?: boolean; -} - -function getChplCheckArgs(): Array { - const config = vscode.workspace.getConfiguration("chapel"); - const chplcheck = config.get("chplcheck") ?? {}; - return chplcheck.args ?? []; -} - -function getCLSArgs(): Array { - const config = vscode.workspace.getConfiguration("chapel"); - const cls = config.get("chpl-language-server") ?? {}; - return cls.args ?? []; -} -function getChplCheckEnabled(): boolean { - const config = vscode.workspace.getConfiguration("chapel"); - const chplcheck = config.get("chplcheck") ?? {}; - return chplcheck.enable ?? true; -} -function getCLSEnabled(): boolean { - const config = vscode.workspace.getConfiguration("chapel"); - const cls = config.get("chpl-language-server") ?? {}; - return cls.enable ?? true; -} - -function getChplCheck(chplhome: string): string { - const config = vscode.workspace.getConfiguration("chapel"); - const chplcheck = config.get("chplcheck") ?? {}; - let p = chplcheck.path; - if (p) { - return p; - } - p = path.resolve(path.join(chplhome, "tools", "chplcheck", "chplcheck")); - return p; -} -function getCLS(chplhome: string): string { - const config = vscode.workspace.getConfiguration("chapel"); - const cls = config.get("chpl-language-server") ?? {}; - let p = cls.path; - if (p) { - return p; - } - p = path.resolve(path.join(chplhome, "tools", "chpl-language-server", "chpl-language-server")); - return p; -} - -async function startChplCheck() { - if (!getChplCheckEnabled()) { - return; - } - // Don't interfere if we are already in the process of launching the server. - if (chplcheckClientStarting) { - return; - } - - chplcheckClientStarting = true; - if (chplcheckClient) { - await stopLangServer(chplcheckClient); - chplcheckClientStarting = false; - } - - const chplhome = getChplHome(); - - if (!chplhome) { - logger.error(`Unable to start server, missing CHPL_HOME`); - await stopLangServer(chplcheckClient); - chplcheckClientStarting = false; - } - - const chplcheck = getChplCheck(chplhome!); - - var args = ["--lsp"]; - var userArgs = getChplCheckArgs(); - args.push(...userArgs); - - logger.info(`chplcheck path: '${chplcheck}'`); - logger.info(`chplcheck args: '${userArgs}'`); - - let env = process.env; - env.CHPL_HOME = chplhome!; - env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; - - const serverOptions: ServerOptions = { - command: chplcheck!, - args: args, - options: { - cwd: chplhome!, - env: env, - }, - }; - logger.debug("server options", serverOptions); - - try { - chplcheckClient = new LanguageClient( - "chplcheck", - serverOptions, - getClientOptions() - ); - await chplcheckClient.start(); - chplcheckClientStarting = false; - } catch (err) { - chplcheckClientStarting = false; - logger.error(`Unable to start server: ${err}`); - } -} - -async function startCLS() { - if (!getCLSEnabled()) { - return; - } - // Don't interfere if we are already in the process of launching the server. - if (clsClientStarting) { - return; - } - - clsClientStarting = true; - if (clsClient) { - await stopLangServer(clsClient); - clsClientStarting = false; - } - - const chplhome = getChplHome(); - - if (!chplhome) { - logger.error(`Unable to start server, missing CHPL_HOME`); - await stopLangServer(clsClient); - clsClientStarting = false; - } - - const cls = getCLS(chplhome!); - - var args = []; - var userArgs = getCLSArgs(); - args.push(...userArgs); - - logger.info(`chpl-language-server path: '${cls}'`); - logger.info(`chpl-language-server args: '${userArgs}'`); - - let env = process.env; - env.CHPL_HOME = chplhome!; - env.CHPL_DEVELOPER = getChplDeveloper() ? "1" : "0"; - - const serverOptions: ServerOptions = { - command: cls!, - args: args, - options: { - cwd: chplhome!, - env: env, - }, - }; - logger.debug("server options", serverOptions); - - try { - clsClient = new LanguageClient( - "chpl-language-server", - serverOptions, - getClientOptions() - ); - await clsClient.start(); - clsClientStarting = false; - } catch (err) { - clsClientStarting = false; - logger.error(`Unable to start server: ${err}`); - } -} - -async function stopLangServer( - client: LanguageClient | undefined -): Promise { - if (!client) { - return; - } - - if (client.state === State.Running) { - await client.stop(); - } - - client.dispose(); - client = undefined; -} -function getClientOptions() { - const options = { - documentSelector: [ - { - scheme: "file", - language: "chapel", - }, - ], - outputChannel: logger, - connectionOptions: { - maxRestartCount: 0, // don't restart on server failure. - }, - }; - logger.info(`client options: ${JSON.stringify(options, undefined, 2)}`); - return options; + return Promise.all([chplcheckClient.stop(), clsClient.stop()]); } From 1f7dc34f5b456413c2078f3ce587c446469dc7d8 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Fri, 23 Feb 2024 14:21:05 -0800 Subject: [PATCH 7/9] update readme Signed-off-by: Jade Abraham --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a2a71bc..7b31b87 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,15 @@ After installing the extension, follow these steps to make sure VSCode is setup ### From an existing Chapel build -These directions assume you already have Chapel built from source and know what your `CHPL_HOME` is: +The extension can auto-detect your `CHPL_HOME`, just open a Chapel file. The extension will prompt you to select an existing Chapel install to configure your editor. If you don't see your value of `CHPL_HOME` or don't know the right one, run `chpl --print-chpl-home` to get the right value. If the automatic installation fails, you can explicitly set your `CHPL_HOME` in your VSCode settings.json as `"chapel.CHPL_HOME": "/path/to/your/chapel/home"`. -1. Execute the following command to build the python tools `(cd $CHPL_HOME && make chapel-py-venv)` -2. Set your `CHPL_HOME` in your VSCode settings.json as `"chapel.CHPL_HOME": "/path/to/your/chapel/home"`. +The extension can also auto-build the Chapel language tools and will prompt you to do so if they are missing. If you prefer to build them manually, run the following: `(export CHPL_HOME=/path/to/your/chapel/home && cd $CHPL_HOME && make chpl-language-server && make chplcheck)` ### Without an existing Chapel build 1. Obtain a copy of latest Chapel source release from 2. After downloading the tar, extract the source tree with `tar xzf chapel-VERSION.tar.gz` -3. Build the python tools with ``(cd chapel-VERSION && CHPL_HOME=`pwd` make chapel-py-venv && echo "Your CHPL_HOME is '`pwd`'")`` -4. The above command will tell you what your `CHPL_HOME` is, add it to your VSCode settings.json as `"chapel.CHPL_HOME": "/path/to/your/chapel/home"`. +3. After unpacking the tar, you can treat this as your `CHPL_HOME` (`"/path/to/unpacked/source/chapel-VERSION"`) and follow the steps for an existing Chapel build. ## Linter options From f6651292b97a79884ca9dad6ca290eb2033ff46c Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Fri, 23 Feb 2024 14:23:14 -0800 Subject: [PATCH 8/9] improve placeholder Signed-off-by: Jade Abraham --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 0851771..4896173 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,7 +35,7 @@ function showChplHomeMissingError(errorString?: string) { function pickMyOwnChplHome() { vscode.window .showInputBox({ - placeHolder: "Enter the path to CHPL_HOME", + placeHolder: "Enter the path to CHPL_HOME (possible run `chpl --print-chpl-home` in the terminal to find it)", }) .then((selection) => { if (selection !== undefined) { From fe42a0175f7abb7f8dcc532a1b7b1f236e67d542 Mon Sep 17 00:00:00 2001 From: Jade Abraham Date: Fri, 23 Feb 2024 14:26:59 -0800 Subject: [PATCH 9/9] fix typo Signed-off-by: Jade Abraham --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 4896173..37d68f7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,7 +35,7 @@ function showChplHomeMissingError(errorString?: string) { function pickMyOwnChplHome() { vscode.window .showInputBox({ - placeHolder: "Enter the path to CHPL_HOME (possible run `chpl --print-chpl-home` in the terminal to find it)", + placeHolder: "Enter the path to CHPL_HOME (possibly run `chpl --print-chpl-home` in the terminal to find it)", }) .then((selection) => { if (selection !== undefined) {