From 24d54259dce06eeb27e6d12c3a8ffa6a50694ccb Mon Sep 17 00:00:00 2001 From: Tony Spataro Date: Thu, 21 Jul 2022 23:31:10 -0700 Subject: [PATCH] Add advanced setting --- package.json | 233 +++++++++++++++++++++++++---------------------- src/extension.ts | 61 ++++++++----- src/variables.ts | 25 +++++ 3 files changed, 188 insertions(+), 131 deletions(-) create mode 100644 src/variables.ts diff --git a/package.json b/package.json index 95ab989..22c87c5 100644 --- a/package.json +++ b/package.json @@ -1,111 +1,124 @@ { - "name": "vscode-syntax-tree", - "displayName": "Syntax Tree", - "description": "VSCode support for the syntax_tree gem", - "icon": "doc/logo.png", - "version": "0.3.1", - "publisher": "ruby-syntax-tree", - "repository": { - "type": "git", - "url": "https://github.com/ruby-syntax-tree/vscode-syntax-tree.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/ruby-syntax-tree/vscode-syntax-tree/issues" - }, - "engines": { - "vscode": "^1.68.0" - }, - "activationEvents": [ - "onLanguage:ruby", - "workspaceContains:Gemfile.lock", - "onCommand:syntaxTree.start", - "onCommand:syntaxTree.stop", - "onCommand:syntaxTree.restart", - "onCommand:syntaxTree.showOutputChannel", - "onCommand:syntaxTree.visualize" - ], - "main": "./out/extension", - "contributes": { - "commands": [ - { - "command": "syntaxTree.start", - "title": "Syntax Tree: Start" - }, - { - "command": "syntaxTree.stop", - "title": "Syntax Tree: Stop" - }, - { - "command": "syntaxTree.restart", - "title": "Syntax Tree: Restart" - }, - { - "command": "syntaxTree.showOutputChannel", - "title": "Syntax Tree: Show Output Channel" - }, - { - "command": "syntaxTree.visualize", - "title": "Syntax Tree: Visualize" - } - ], - "configuration": { - "type": "object", - "title": "Syntax Tree", - "properties": { - "syntaxTree.printWidth": { - "default": 80, - "markdownDescription": "The width to be used when formatting code.", - "type": "number" - }, - "syntaxTree.singleQuotes": { - "default": false, - "markdownDescription": "Uses single-quoted strings when possible.", - "type": "boolean" - }, - "syntaxTree.trailingComma": { - "default": false, - "markdownDescription": "Adds a trailing comma to multi-line array literals, hash literals, and method parameters.", - "type": "boolean" - }, - "syntaxTree.additionalPlugins": { - "default": [], - "markdownDescription": "Registers [extra behaviors](https://github.com/ruby-syntax-tree/syntax_tree#plugins) with the language server.", - "items": { - "type": "string" - }, - "type": "array" - } - } - }, - "colors": [] - }, - "scripts": { - "compile": "tsc -p ./", - "package": "vsce package --no-yarn --githubBranch main", - "publish": "vsce publish --no-yarn --githubBranch main", - "test": "node ./out/test/runTest.js", - "vscode:prepublish": "yarn compile", - "watch": "tsc --watch -p ./" - }, - "dependencies": { - "vscode-languageclient": "8.0.2" - }, - "devDependencies": { - "@types/glob": "^7.1.1", - "@types/mocha": "^9.1.1", - "@types/node": "^18.0.0", - "@types/vscode": "^1.68.0", - "@vscode/test-electron": "^1.6.2", - "glob": "^8.0.3", - "mocha": "^10.0.0", - "typescript": "^4.7.4", - "vsce": "^2.9.2" - }, - "__metadata": { - "id": "b46118f9-0f6f-4320-9e2e-75c96492b4cb", - "publisherDisplayName": "ruby-syntax-tree", - "publisherId": "63942dce-de09-44d8-b863-4a1dbd5508c6", - "isPreReleaseVersion": false - } -} \ No newline at end of file + "name": "vscode-syntax-tree", + "displayName": "Syntax Tree", + "description": "VSCode support for the syntax_tree gem", + "icon": "doc/logo.png", + "version": "0.3.1", + "publisher": "ruby-syntax-tree", + "repository": { + "type": "git", + "url": "https://github.com/ruby-syntax-tree/vscode-syntax-tree.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ruby-syntax-tree/vscode-syntax-tree/issues" + }, + "engines": { + "vscode": "^1.68.0" + }, + "activationEvents": [ + "onLanguage:ruby", + "workspaceContains:Gemfile.lock", + "onCommand:syntaxTree.start", + "onCommand:syntaxTree.stop", + "onCommand:syntaxTree.restart", + "onCommand:syntaxTree.showOutputChannel", + "onCommand:syntaxTree.visualize" + ], + "main": "./out/extension", + "contributes": { + "commands": [ + { + "command": "syntaxTree.start", + "title": "Syntax Tree: Start" + }, + { + "command": "syntaxTree.stop", + "title": "Syntax Tree: Stop" + }, + { + "command": "syntaxTree.restart", + "title": "Syntax Tree: Restart" + }, + { + "command": "syntaxTree.showOutputChannel", + "title": "Syntax Tree: Show Output Channel" + }, + { + "command": "syntaxTree.visualize", + "title": "Syntax Tree: Visualize" + } + ], + "configuration": [ + { + "type": "object", + "title": "Syntax Tree", + "properties": { + "syntaxTree.additionalPlugins": { + "default": [], + "markdownDescription": "Registers [extra behaviors](https://github.com/ruby-syntax-tree/syntax_tree#plugins) with the language server.", + "items": { + "type": "string" + }, + "type": "array" + }, + "syntaxTree.printWidth": { + "default": 80, + "markdownDescription": "The width to be used when formatting code.", + "type": "number" + }, + "syntaxTree.singleQuotes": { + "default": false, + "markdownDescription": "Uses single-quoted strings when possible.", + "type": "boolean" + }, + "syntaxTree.trailingComma": { + "default": false, + "markdownDescription": "Adds a trailing comma to multi-line array literals, hash literals, and method parameters.", + "type": "boolean" + } + } + }, + { + "type": "object", + "title": "Advanced", + "properties": { + "syntaxTree.advanced.commandPath": { + "default": "", + "markdownDescription": "Absolute path to stree executable. Overrides default search order.\n\nSupports variables `${userHome}`, `${pathSeparator}`, and `${cwd}`", + "type": "string" + } + } + } + ], + "colors": [] + }, + "scripts": { + "compile": "tsc -p ./", + "package": "vsce package --no-yarn --githubBranch main", + "publish": "vsce publish --no-yarn --githubBranch main", + "test": "node ./out/test/runTest.js", + "vscode:prepublish": "yarn compile", + "watch": "tsc --watch -p ./" + }, + "dependencies": { + "vscode-languageclient": "8.0.2" + }, + "devDependencies": { + "@types/glob": "^7.1.1", + "@types/mocha": "^9.1.1", + "@types/node": "^18.0.0", + "@types/vscode": "^1.68.0", + "@vscode/test-electron": "^1.6.2", + "glob": "^8.0.3", + "mocha": "^10.0.0", + "typescript": "^4.7.4", + "vsce": "^2.9.2" + }, + "__metadata": { + "id": "b46118f9-0f6f-4320-9e2e-75c96492b4cb", + "publisherDisplayName": "ruby-syntax-tree", + "publisherId": "63942dce-de09-44d8-b863-4a1dbd5508c6", + "isPreReleaseVersion": false + } +} diff --git a/src/extension.ts b/src/extension.ts index 4fd7746..0f7b3c6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,12 @@ "use strict"; +import { exec } from "child_process"; +import * as fs from "fs"; +import { promisify } from "util"; import { ExtensionContext, commands, window, workspace } from "vscode"; import { LanguageClient, ServerOptions } from "vscode-languageclient/node"; -import { promisify } from "util"; -import { exec } from "child_process"; +import * as variables from './variables'; import Visualize from "./Visualize"; const promiseExec = promisify(exec); @@ -59,9 +61,11 @@ export async function activate(context: ExtensionContext) { return; // preserve idempotency } - // The top-level configuration group is syntaxTree. All of the configuration - // for the extension is under that group. + // The top-level configuration group is syntaxTree. Broadly useful settings + // are under that group. const config = workspace.getConfiguration("syntaxTree"); + // More obscure settings for power users live in a subgroup. + const advancedConfig = workspace.getConfiguration("syntaxTree.advanced"); // The args are going to be passed to the stree executable. It's important // that it lines up with what the CLI expects. @@ -90,26 +94,41 @@ export async function activate(context: ExtensionContext) { // Configure print width. args.push(`--print-width=${config.get("printWidth")}`) - // There's a bit of complexity here. Basically, if there's an open folder, - // then we're going to check if the syntax_tree gem is inside the bundle. If - // it is, then we'll run bundle exec stree. This is good, because it'll - // ensure that we get the correct version of the gem. If it's not in the - // bundle or there is no bundle, then we'll just run the global stree. This - // might be correct in the end if the right environment variables are set, - // but it's a bit of a prayer. - const cwd = getCWD(); + // There's a bit of complexity here. Basically, we try to locate + // an stree executable in three places, in order of preference: + // 1. Explicit path from advanced settings, if provided + // 2. The bundle inside CWD, if syntax_tree is in the bundle + // 3. Anywhere in $PATH (i.e. system gem) + // + // None of these approaches is perfect. System gem might be correct if the + // right environment variables are set, but it's a bit of a prayer. Bundled + // gem is better, but we make the gross oversimplification that the + // workspace only has one root and that the bundle is at root of the + // workspace -- which is not true for large projects or monorepos. + // Explicit path varies between machines/users and is also victim to the + // oversimplification problem. let run: ServerOptions = { command: "stree", args }; - let where = 'global'; - - try { - await promiseExec("bundle show syntax_tree", { cwd }); - run = { command: "bundle", args: ["exec", "stree"].concat(args), options: { cwd } }; - where = 'bundled'; - } catch { - // No-op (just keep using the global stree) + let commandPath = advancedConfig.get('commandPath'); + if (commandPath) { + commandPath = variables.substitute(commandPath); + try { + if (fs.statSync(commandPath).isFile()) { + run = { command: commandPath, args }; + } + } catch (err) { + outputChannel.appendLine(`Ignoring bogus commandPath (${commandPath} does not exist); falling back to global.`); + } + } else { + try { + const cwd = getCWD(); + await promiseExec("bundle show syntax_tree", { cwd }); + run = { command: "bundle", args: ["exec", "stree"].concat(args), options: { cwd } }; + } catch { + // No-op (just keep using the global stree) + } } - outputChannel.appendLine(`Starting language server with ${where} stree and ${plugins.size} plugin(s)...`); + outputChannel.appendLine(`Starting language server: ${run.command} ${run.args?.join(' ')}`); // Here, we instantiate the language client. This is the object that is // responsible for the communication and management of the Ruby subprocess. diff --git a/src/variables.ts b/src/variables.ts new file mode 100644 index 0000000..1fb9620 --- /dev/null +++ b/src/variables.ts @@ -0,0 +1,25 @@ +import * as os from 'os'; +import * as path from 'path'; + +const substitution = new RegExp('\\$\\{([^}]*)\\}'); + +export function substitute(s: string) { + let match = substitution.exec(s); + while (match) { + const variable = match[1]; + switch (variable) { + case 'cwd': + s = s.replace(match[0], process.cwd()); + break; + case 'pathSeparator': + s = s.replace(match[0], path.sep); + break; + case 'userHome': + s = s.replace(match[0], os.homedir()); + break; + } + match = substitution.exec(s); + } + + return s; +}