@@ -4,15 +4,23 @@ import * as fs from 'fs';
4
4
import * as path from 'path' ;
5
5
6
6
import * as cpp from 'child-process-promise' ;
7
+ import { ChildProcess } from "child_process" ;
7
8
8
9
import { ExtensionContext } from 'vscode' ;
9
10
import * as vscode from 'vscode' ;
10
11
import { LanguageClient , LanguageClientOptions , RevealOutputChannelOn ,
11
12
ServerOptions } from 'vscode-languageclient' ;
12
13
14
+ import * as rpc from 'vscode-jsonrpc'
15
+
16
+ import * as sbtserver from './sbt-server'
17
+
13
18
let extensionContext : ExtensionContext
14
19
let outputChannel : vscode . OutputChannel
15
20
21
+ /** The sbt process that may have been started by this extension */
22
+ let sbtProcess : ChildProcess
23
+
16
24
const sbtVersion = "1.2.3"
17
25
const sbtArtifact = `org.scala-sbt:sbt-launch:${ sbtVersion } `
18
26
const workspaceRoot = `${ vscode . workspace . rootPath } `
@@ -54,27 +62,79 @@ export function activate(context: ExtensionContext) {
54
62
} )
55
63
56
64
} else {
57
- // Check whether `.dotty-ide-artifact` exists. If it does, start the language server,
58
- // otherwise, try propose to start it if there's no build.sbt
59
- if ( fs . existsSync ( languageServerArtifactFile ) ) {
60
- runLanguageServer ( coursierPath , languageServerArtifactFile )
61
- } else if ( isUnconfiguredProject ( ) ) {
62
- vscode . window . showInformationMessage (
63
- "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?" ,
64
- "Yes" , "No"
65
+ let configuredProject : Thenable < void > = Promise . resolve ( )
66
+ if ( isUnconfiguredProject ( ) ) {
67
+ configuredProject = vscode . window . showInformationMessage (
68
+ "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?" ,
69
+ "Yes" , "No"
65
70
) . then ( choice => {
66
- if ( choice == "Yes" ) {
67
- fetchAndConfigure ( coursierPath , sbtArtifact , buildSbtFileSource , dottyPluginSbtFileSource ) . then ( ( ) => {
68
- runLanguageServer ( coursierPath , languageServerArtifactFile )
69
- } )
70
- } else {
71
+ if ( choice === "Yes" ) {
72
+ bootstrapSbtProject ( buildSbtFileSource , dottyPluginSbtFileSource )
73
+ return Promise . resolve ( )
74
+ } else if ( choice === "No" ) {
71
75
fs . appendFile ( disableDottyIDEFile , "" , _ => { } )
76
+ return Promise . reject ( )
72
77
}
73
78
} )
74
79
}
80
+
81
+ configuredProject
82
+ . then ( _ => withProgress ( "Configuring Dotty IDE..." , configureIDE ( coursierPath ) ) )
83
+ . then ( _ => runLanguageServer ( coursierPath , languageServerArtifactFile ) )
75
84
}
76
85
}
77
86
87
+ export function deactivate ( ) {
88
+ // If sbt was started by this extension, kill the process.
89
+ // FIXME: This will be a problem for other clients of this server.
90
+ if ( sbtProcess ) {
91
+ sbtProcess . kill ( )
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Display a progress bar with title `title` while `op` completes.
97
+ *
98
+ * @param title The title of the progress bar
99
+ * @param op The thenable that is monitored by the progress bar.
100
+ */
101
+ function withProgress < T > ( title : string , op : Thenable < T > ) : Thenable < T > {
102
+ return vscode . window . withProgress ( {
103
+ location : vscode . ProgressLocation . Window ,
104
+ title : title
105
+ } , _ => op )
106
+ }
107
+
108
+ /** Connect to an sbt server and run `configureIDE`. */
109
+ function configureIDE ( coursierPath : string ) : Thenable < sbtserver . ExecResult > {
110
+
111
+ function offeringToRetry ( client : rpc . MessageConnection , command : string ) : Thenable < sbtserver . ExecResult > {
112
+ return sbtserver . tellSbt ( outputChannel , client , command )
113
+ . then ( success => Promise . resolve ( success ) ,
114
+ _ => {
115
+ outputChannel . show ( )
116
+ return vscode . window . showErrorMessage ( "IDE configuration failed (see logs for details)" , "Retry?" )
117
+ . then ( retry => {
118
+ if ( retry ) return offeringToRetry ( client , command )
119
+ else return Promise . reject ( )
120
+ } )
121
+ } )
122
+ }
123
+
124
+ return withSbtInstance ( outputChannel , coursierPath )
125
+ . then ( client => {
126
+ // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
127
+ // until sbt/sbt#4370 is fixed.
128
+ // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
129
+ // in case of failure), and we're pretty sure configureIDE will pass if they passed.
130
+ return offeringToRetry ( client , "compile" ) . then ( _ => {
131
+ return offeringToRetry ( client , "test:compile" ) . then ( _ => {
132
+ return offeringToRetry ( client , "configureIDE" )
133
+ } )
134
+ } )
135
+ } )
136
+ }
137
+
78
138
function runLanguageServer ( coursierPath : string , languageServerArtifactFile : string ) {
79
139
fs . readFile ( languageServerArtifactFile , ( err , data ) => {
80
140
if ( err ) throw err
@@ -90,10 +150,28 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
90
150
} )
91
151
}
92
152
93
- function fetchAndConfigure ( coursierPath : string , sbtArtifact : string , buildSbtFileSource : string , dottyPluginSbtFileSource : string ) {
94
- return fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
95
- return configureIDE ( sbtClasspath , buildSbtFileSource , dottyPluginSbtFileSource )
153
+ /**
154
+ * Connects to an existing sbt server, or boots up one instance and connects to it.
155
+ */
156
+ function withSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) : Thenable < rpc . MessageConnection > {
157
+ const serverSocketInfo = path . join ( workspaceRoot , "project" , "target" , "active.json" )
158
+
159
+ if ( ! fs . existsSync ( serverSocketInfo ) ) {
160
+ fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
161
+ sbtProcess = cpp . spawn ( "java" , [
162
+ "-classpath" , sbtClasspath ,
163
+ "xsbt.boot.Boot"
164
+ ] ) . childProcess
165
+ sbtProcess . stdout . on ( 'data' , data => {
166
+ log . append ( data . toString ( ) )
167
+ } )
168
+ sbtProcess . stderr . on ( 'data' , data => {
169
+ log . append ( data . toString ( ) )
170
+ } )
96
171
} )
172
+ }
173
+
174
+ return sbtserver . connectToSbtServer ( log )
97
175
}
98
176
99
177
function fetchWithCoursier ( coursierPath : string , artifact : string , extra : string [ ] = [ ] ) {
@@ -127,40 +205,12 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string
127
205
} )
128
206
}
129
207
130
- function configureIDE ( sbtClasspath : string ,
131
- buildSbtFileSource : string ,
132
- dottyPluginSbtFileSource : string ) {
133
-
134
- return vscode . window . withProgress ( {
135
- location : vscode . ProgressLocation . Window ,
136
- title : 'Configuring the IDE for Dotty...'
137
- } , _ => {
138
-
139
- // Bootstrap an sbt build
208
+ function bootstrapSbtProject ( buildSbtFileSource : string ,
209
+ dottyPluginSbtFileSource : string ) {
140
210
fs . mkdirSync ( sbtProjectDir )
141
211
fs . appendFileSync ( sbtBuildPropertiesFile , `sbt.version=${ sbtVersion } ` )
142
212
fs . copyFileSync ( buildSbtFileSource , sbtBuildSbtFile )
143
213
fs . copyFileSync ( dottyPluginSbtFileSource , path . join ( sbtProjectDir , "plugins.sbt" ) )
144
-
145
- // Run sbt to configure the IDE.
146
- const sbtPromise =
147
- cpp . spawn ( "java" , [
148
- "-classpath" , sbtClasspath ,
149
- "xsbt.boot.Boot" ,
150
- "configureIDE"
151
- ] )
152
-
153
- const sbtProc = sbtPromise . childProcess
154
- sbtProc . on ( 'close' , ( code : number ) => {
155
- if ( code != 0 ) {
156
- const msg = "Configuring the IDE failed."
157
- outputChannel . append ( msg )
158
- throw new Error ( msg )
159
- }
160
- } )
161
-
162
- return sbtPromise
163
- } )
164
214
}
165
215
166
216
function run ( serverOptions : ServerOptions ) {
0 commit comments