Skip to content

Commit f244fc2

Browse files
authored
Merge pull request #76 from ruby-syntax-tree/search-in-path
Attempt to search in $PATH smartly
2 parents 6187005 + 281609a commit f244fc2

File tree

2 files changed

+92
-68
lines changed

2 files changed

+92
-68
lines changed

src/extension.ts

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import { exec } from "child_process";
44
import * as fs from "fs";
5+
import * as os from "os";
6+
import * as path from "path";
57
import { promisify } from "util";
68
import { ExtensionContext, commands, window, workspace } from "vscode";
7-
import { LanguageClient, ServerOptions } from "vscode-languageclient/node";
9+
import { LanguageClient, Executable } from "vscode-languageclient/node";
810

9-
import * as variables from "./variables";
1011
import Visualize from "./Visualize";
1112

1213
const promiseExec = promisify(exec);
@@ -50,9 +51,96 @@ export async function activate(context: ExtensionContext) {
5051
})
5152
);
5253

54+
// We're returning a Promise from this function that will start the Ruby
55+
// subprocess.
56+
await startLanguageServer();
57+
5358
// If there's an open folder, use it as cwd when spawning commands
5459
// to promote correct package & language versioning.
55-
const getCWD = () => workspace.workspaceFolders?.[0]?.uri?.fsPath || process.cwd();
60+
function getCWD() {
61+
return workspace.workspaceFolders?.[0]?.uri?.fsPath || process.cwd();
62+
}
63+
64+
// There's a bit of complexity here. Basically, we try to locate
65+
// an stree executable in three places, in order of preference:
66+
// 1. Explicit path from advanced settings, if provided
67+
// 2. The bundle inside CWD, if syntax_tree is in the bundle
68+
// 3. Somewhere in $PATH that contains "ruby" in the director
69+
// 4. Anywhere in $PATH (i.e. system gem)
70+
//
71+
// None of these approaches is perfect. System gem might be correct if the
72+
// right environment variables are set, but it's a bit of a prayer. Bundled
73+
// gem is better, but we make the gross oversimplification that the
74+
// workspace only has one root and that the bundle is at root of the
75+
// workspace -- which is not true for large projects or monorepos.
76+
// Explicit path varies between machines/users and is also victim to the
77+
// oversimplification problem.
78+
async function getServerOptions(args: string[]): Promise<Executable> {
79+
const advancedConfig = workspace.getConfiguration("syntaxTree.advanced");
80+
let value = advancedConfig.get<string>("commandPath");
81+
82+
// If a value is given on the command line, attempt to use it.
83+
if (value) {
84+
// First, substitute in any variables that may have been present in the
85+
// given value to the configuration.
86+
const substitution = new RegExp("\\$\\{([^}]*)\\}");
87+
88+
for (let match = substitution.exec(value); match; match = substitution.exec(value)) {
89+
switch (match[1]) {
90+
case "cwd":
91+
value = value.replace(match[0], process.cwd());
92+
break;
93+
case "pathSeparator":
94+
value = value.replace(match[0], path.sep);
95+
break;
96+
case "userHome":
97+
value = value.replace(match[0], os.homedir());
98+
break;
99+
}
100+
}
101+
102+
// Next, attempt to stat the executable path. If it's a file, we're good.
103+
try {
104+
if (fs.statSync(value).isFile()) {
105+
return { command: value, args };
106+
}
107+
} catch {
108+
outputChannel.appendLine(`Ignoring bogus commandPath (${value} does not exist).`);
109+
}
110+
}
111+
112+
// Otherwise, we're going to try using bundler to find the executable.
113+
try {
114+
const cwd = getCWD();
115+
await promiseExec("bundle show syntax_tree", { cwd });
116+
return { command: "bundle", args: ["exec", "stree"].concat(args), options: { cwd } };
117+
} catch {
118+
// Do nothing.
119+
}
120+
121+
// Otherwise, we're going to try parsing the PATH environment variable to
122+
// find the executable.
123+
const executablePaths = await Promise.all((process.env.PATH || "")
124+
.split(path.delimiter)
125+
.filter((directory) => directory.includes("ruby"))
126+
.map((directory) => {
127+
const executablePath = path.join(directory, "stree");
128+
129+
return fs.promises.stat(executablePath).then(
130+
(stat) => stat.isFile() ? executablePath : null,
131+
() => null
132+
);
133+
}));
134+
135+
for (const executablePath in executablePaths) {
136+
if (executablePath) {
137+
return { command: executablePath, args };
138+
}
139+
}
140+
141+
// Otherwise, fall back to the global stree lookup.
142+
return { command: "stree", args };
143+
}
56144

57145
// This function is called when the extension is activated or when the
58146
// language server is restarted.
@@ -64,8 +152,6 @@ export async function activate(context: ExtensionContext) {
64152
// The top-level configuration group is syntaxTree. Broadly useful settings
65153
// are under that group.
66154
const config = workspace.getConfiguration("syntaxTree");
67-
// More obscure settings for power users live in a subgroup.
68-
const advancedConfig = workspace.getConfiguration("syntaxTree.advanced");
69155

70156
// The args are going to be passed to the stree executable. It's important
71157
// that it lines up with what the CLI expects.
@@ -97,40 +183,7 @@ export async function activate(context: ExtensionContext) {
97183
args.push(`--print-width=${printWidth}`);
98184
}
99185

100-
// There's a bit of complexity here. Basically, we try to locate
101-
// an stree executable in three places, in order of preference:
102-
// 1. Explicit path from advanced settings, if provided
103-
// 2. The bundle inside CWD, if syntax_tree is in the bundle
104-
// 3. Anywhere in $PATH (i.e. system gem)
105-
//
106-
// None of these approaches is perfect. System gem might be correct if the
107-
// right environment variables are set, but it's a bit of a prayer. Bundled
108-
// gem is better, but we make the gross oversimplification that the
109-
// workspace only has one root and that the bundle is at root of the
110-
// workspace -- which is not true for large projects or monorepos.
111-
// Explicit path varies between machines/users and is also victim to the
112-
// oversimplification problem.
113-
let run: ServerOptions = { command: "stree", args };
114-
let commandPath = advancedConfig.get<string>("commandPath");
115-
if (commandPath) {
116-
commandPath = variables.substitute(commandPath);
117-
try {
118-
if (fs.statSync(commandPath).isFile()) {
119-
run = { command: commandPath, args };
120-
}
121-
} catch (err) {
122-
outputChannel.appendLine(`Ignoring bogus commandPath (${commandPath} does not exist); falling back to global.`);
123-
}
124-
} else {
125-
try {
126-
const cwd = getCWD();
127-
await promiseExec("bundle show syntax_tree", { cwd });
128-
run = { command: "bundle", args: ["exec", "stree"].concat(args), options: { cwd } };
129-
} catch {
130-
// No-op (just keep using the global stree)
131-
}
132-
}
133-
186+
const run = await getServerOptions(args);
134187
outputChannel.appendLine(`Starting language server: ${run.command} ${run.args?.join(" ")}`);
135188

136189
// Here, we instantiate the language client. This is the object that is
@@ -207,10 +260,6 @@ export async function activate(context: ExtensionContext) {
207260
outputChannel.appendLine(`Error installing gem: ${error}`);
208261
}
209262
}
210-
211-
// We're returning a Promise from this function that will start the Ruby
212-
// subprocess.
213-
await startLanguageServer();
214263
}
215264

216265
// This is the expected top-level export that is called by VSCode when the

src/variables.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)