2
2
3
3
import { exec } from "child_process" ;
4
4
import * as fs from "fs" ;
5
+ import * as os from "os" ;
6
+ import * as path from "path" ;
5
7
import { promisify } from "util" ;
6
8
import { ExtensionContext , commands , window , workspace } from "vscode" ;
7
- import { LanguageClient , ServerOptions } from "vscode-languageclient/node" ;
9
+ import { LanguageClient , Executable } from "vscode-languageclient/node" ;
8
10
9
- import * as variables from "./variables" ;
10
11
import Visualize from "./Visualize" ;
11
12
12
13
const promiseExec = promisify ( exec ) ;
@@ -50,9 +51,96 @@ export async function activate(context: ExtensionContext) {
50
51
} )
51
52
) ;
52
53
54
+ // We're returning a Promise from this function that will start the Ruby
55
+ // subprocess.
56
+ await startLanguageServer ( ) ;
57
+
53
58
// If there's an open folder, use it as cwd when spawning commands
54
59
// 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
+ }
56
144
57
145
// This function is called when the extension is activated or when the
58
146
// language server is restarted.
@@ -64,8 +152,6 @@ export async function activate(context: ExtensionContext) {
64
152
// The top-level configuration group is syntaxTree. Broadly useful settings
65
153
// are under that group.
66
154
const config = workspace . getConfiguration ( "syntaxTree" ) ;
67
- // More obscure settings for power users live in a subgroup.
68
- const advancedConfig = workspace . getConfiguration ( "syntaxTree.advanced" ) ;
69
155
70
156
// The args are going to be passed to the stree executable. It's important
71
157
// that it lines up with what the CLI expects.
@@ -97,40 +183,7 @@ export async function activate(context: ExtensionContext) {
97
183
args . push ( `--print-width=${ printWidth } ` ) ;
98
184
}
99
185
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 ) ;
134
187
outputChannel . appendLine ( `Starting language server: ${ run . command } ${ run . args ?. join ( " " ) } ` ) ;
135
188
136
189
// Here, we instantiate the language client. This is the object that is
@@ -207,10 +260,6 @@ export async function activate(context: ExtensionContext) {
207
260
outputChannel . appendLine ( `Error installing gem: ${ error } ` ) ;
208
261
}
209
262
}
210
-
211
- // We're returning a Promise from this function that will start the Ruby
212
- // subprocess.
213
- await startLanguageServer ( ) ;
214
263
}
215
264
216
265
// This is the expected top-level export that is called by VSCode when the
0 commit comments