1
1
"use strict" ;
2
2
3
- import { ConfigurationChangeEvent , ExtensionContext , commands , window , workspace } from "vscode" ;
3
+ import { ExtensionContext , commands , window , workspace } from "vscode" ;
4
4
import { LanguageClient , ServerOptions } from "vscode-languageclient/node" ;
5
5
import { promisify } from "util" ;
6
6
import { exec } from "child_process" ;
@@ -10,49 +10,85 @@ import Visualize from "./Visualize";
10
10
11
11
const promiseExec = promisify ( exec ) ;
12
12
13
+ // This is the expected top-level export that is called by VSCode.
13
14
export function activate ( context : ExtensionContext ) {
15
+ // This output channel is going to contain all of our informational messages.
16
+ // It's not really meant for the end-user, it's more for debugging.
14
17
const outputChannel = window . createOutputChannel ( "Syntax Tree" ) ;
18
+
19
+ // These objects will get initialized once the language client is ready.
15
20
let languageClient : LanguageClient | null = null ;
16
21
let visualizer : Visualize | null = null ;
17
22
23
+ // This is the list of objects that implement the Disposable interface. They
24
+ // will all get cleaned up with this extension is deactivated. It's important
25
+ // to add them to this list so we don't leak memory.
18
26
context . subscriptions . push (
27
+ // The output channel itself is a disposable. When the extension is
28
+ // deactivated it will be removed from the list.
19
29
outputChannel ,
30
+
31
+ // Each of the commands that interacts with this extension is a disposable.
32
+ // It's important to register them here as opposed to whenever the client
33
+ // starts up because we don't want to register them again whenever the
34
+ // client restarts.
20
35
commands . registerCommand ( "syntaxTree.start" , startLanguageServer ) ,
21
36
commands . registerCommand ( "syntaxTree.stop" , stopLanguageServer ) ,
22
37
commands . registerCommand ( "syntaxTree.restart" , restartLanguageServer ) ,
23
38
commands . registerCommand ( "syntaxTree.visualize" , ( ) => visualizer ?. visualize ( ) ) ,
24
39
commands . registerCommand ( "syntaxTree.showOutputChannel" , ( ) => outputChannel . show ( ) ) ,
25
- workspace . onDidChangeConfiguration ( event =>
26
- event . affectsConfiguration ( "syntaxTree" ) &&
27
- restartLanguageServer ( ) )
40
+ workspace . onDidChangeConfiguration ( event => {
41
+ if ( event . affectsConfiguration ( "syntaxTree" ) ) {
42
+ restartLanguageServer ( ) ;
43
+ }
44
+ } )
28
45
) ;
29
46
47
+ // We're returning a Promise from this function that will start the Ruby
48
+ // subprocess.
30
49
return startLanguageServer ( ) ;
31
50
51
+ // This function is called when the extension is activated or when the
52
+ // language server is restarted.
32
53
async function startLanguageServer ( ) {
54
+ // The top-level configuration group is syntaxTree. All of the configuration
55
+ // for the extension is under that group.
33
56
const config = workspace . getConfiguration ( "syntaxTree" ) ;
34
- const addlPlugins = config . get < string [ ] > ( "additionalPlugins" ) || [ ] ;
35
- const singleQuotes = config . get < boolean > ( "singleQuotes" ) ;
36
- const trailingComma = config . get < boolean > ( "trailingComma" ) ;
37
57
58
+ // The args are going to be passed to the stree executable. It's important
59
+ // that it lines up with what the CLI expects.
38
60
const args = [ "lsp" ] ;
39
-
40
61
const plugins = new Set < string > ( ) ;
41
- if ( singleQuotes ) {
62
+
63
+ if ( config . get < boolean > ( "singleQuotes" ) ) {
42
64
plugins . add ( "plugin/single_quotes" ) ;
43
65
}
44
- if ( trailingComma ) {
66
+
67
+ if ( config . get < boolean > ( "trailingComma" ) ) {
45
68
plugins . add ( "plugin/trailing_comma" ) ;
46
69
}
47
- addlPlugins . forEach ( plugins . add ) ;
48
70
49
- if ( plugins . size ) {
71
+ const additionalPlugins = config . get < string [ ] > ( "additionalPlugins" ) ;
72
+ if ( additionalPlugins ) {
73
+ additionalPlugins . forEach ( plugin => plugins . add ( plugin ) ) ;
74
+ }
75
+
76
+ // If there are any plugins, then we'll pass the --plugins command line
77
+ // option to the stree lsp command.
78
+ if ( plugins . size > 0 ) {
50
79
args . push ( `--plugins=${ Array . from ( plugins ) . join ( "," ) } ` ) ;
51
80
}
52
81
53
82
outputChannel . appendLine ( `Starting language server with ${ plugins . size } plugin(s)...` ) ;
54
83
let run : ServerOptions = { command : "stree" , args } ;
55
84
85
+ // There's a bit of complexity here. Basically, if there's an open folder,
86
+ // then w're going to check if the syntax_tree gem is inside the bundle. If
87
+ // it is, then we'll run bundle exec stree. This is good, because it'll
88
+ // ensure that we get the correct version of the gem. If it's not in the
89
+ // bundle or there is no bundle, then we'll just run the global stree. This
90
+ // might be correct in the end if the right environment variables are set,
91
+ // but it's a bit of a prayer.
56
92
if ( workspace . workspaceFolders ) {
57
93
const cwd = workspace . workspaceFolders ! [ 0 ] . uri . fsPath ;
58
94
@@ -64,30 +100,42 @@ export function activate(context: ExtensionContext) {
64
100
}
65
101
}
66
102
103
+ // Here, we instantiate the language client. This is the object that is
104
+ // responsible for the communication and management of the Ruby subprocess.
67
105
languageClient = new LanguageClient ( "Syntax Tree" , { run, debug : run } , {
68
106
documentSelector : [
69
107
{ scheme : "file" , language : "ruby" } ,
70
108
] ,
71
109
outputChannel
72
110
} ) ;
73
111
112
+ // Here we're going to wait for the language server to start.
74
113
context . subscriptions . push ( languageClient . start ( ) ) ;
75
114
await languageClient . onReady ( ) ;
76
115
116
+ // Finally, now that the language server has been properly started, we can
117
+ // add the various features to the extension. Each of them in turn
118
+ // implements Disposable so that they clean up their own resources.
77
119
visualizer = new Visualize ( languageClient , outputChannel ) ;
78
120
context . subscriptions . push (
79
121
new InlayHints ( languageClient , outputChannel ) ,
80
122
visualizer
81
123
) ;
82
124
}
83
125
126
+ // This function is called as part of the shutdown or restart process. It's
127
+ // always user-initiated either through manually executing an action or
128
+ // changing some configuration.
84
129
async function stopLanguageServer ( ) {
85
130
if ( languageClient ) {
86
131
outputChannel . appendLine ( "Stopping language server..." ) ;
87
132
await languageClient . stop ( ) ;
88
133
}
89
134
}
90
135
136
+ // This function is called as part of the restart process. Like
137
+ // stopLanguageServer, it's always user-initiated either through manually
138
+ // executing an action or changing some configuration.
91
139
async function restartLanguageServer ( ) {
92
140
outputChannel . appendLine ( "Restarting language server..." ) ;
93
141
await stopLanguageServer ( ) ;
0 commit comments