@@ -6,6 +6,8 @@ import * as path from 'path';
66import * as cpp from 'child-process-promise' ;
77import * as compareVersions from 'compare-versions' ;
88
9+ import { ChildProcess } from "child_process" ;
10+
911import { ExtensionContext } from 'vscode' ;
1012import * as vscode from 'vscode' ;
1113import { LanguageClient , LanguageClientOptions , RevealOutputChannelOn ,
@@ -14,10 +16,16 @@ import { enableOldServerWorkaround } from './compat'
1416
1517import * as worksheet from './worksheet'
1618
19+ import * as rpc from 'vscode-jsonrpc'
20+ import * as sbtserver from './sbt-server'
21+
1722let extensionContext : ExtensionContext
1823let outputChannel : vscode . OutputChannel
1924export let client : LanguageClient
2025
26+ /** The sbt process that may have been started by this extension */
27+ let sbtProcess : ChildProcess
28+
2129const sbtVersion = "1.2.3"
2230const sbtArtifact = `org.scala-sbt:sbt-launch:${ sbtVersion } `
2331const workspaceRoot = `${ vscode . workspace . rootPath } `
@@ -71,27 +79,79 @@ export function activate(context: ExtensionContext) {
7179 } )
7280
7381 } else {
74- // Check whether `.dotty-ide-artifact` exists. If it does, start the language server,
75- // otherwise, try propose to start it if there's no build.sbt
76- if ( fs . existsSync ( languageServerArtifactFile ) ) {
77- runLanguageServer ( coursierPath , languageServerArtifactFile )
78- } else if ( isUnconfiguredProject ( ) ) {
79- vscode . window . showInformationMessage (
80- "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?" ,
81- "Yes" , "No"
82+ let configuredProject : Thenable < void > = Promise . resolve ( )
83+ if ( isUnconfiguredProject ( ) ) {
84+ configuredProject = vscode . window . showInformationMessage (
85+ "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?" ,
86+ "Yes" , "No"
8287 ) . then ( choice => {
83- if ( choice == "Yes" ) {
84- fetchAndConfigure ( coursierPath , sbtArtifact , buildSbtFileSource , dottyPluginSbtFileSource ) . then ( ( ) => {
85- runLanguageServer ( coursierPath , languageServerArtifactFile )
86- } )
87- } else {
88+ if ( choice === "Yes" ) {
89+ bootstrapSbtProject ( buildSbtFileSource , dottyPluginSbtFileSource )
90+ return Promise . resolve ( )
91+ } else if ( choice === "No" ) {
8892 fs . appendFile ( disableDottyIDEFile , "" , _ => { } )
93+ return Promise . reject ( )
8994 }
9095 } )
9196 }
97+
98+ configuredProject
99+ . then ( _ => withProgress ( "Configuring Dotty IDE..." , configureIDE ( coursierPath ) ) )
100+ . then ( _ => runLanguageServer ( coursierPath , languageServerArtifactFile ) )
92101 }
93102}
94103
104+ export function deactivate ( ) {
105+ // If sbt was started by this extension, kill the process.
106+ // FIXME: This will be a problem for other clients of this server.
107+ if ( sbtProcess ) {
108+ sbtProcess . kill ( )
109+ }
110+ }
111+
112+ /**
113+ * Display a progress bar with title `title` while `op` completes.
114+ *
115+ * @param title The title of the progress bar
116+ * @param op The thenable that is monitored by the progress bar.
117+ */
118+ function withProgress < T > ( title : string , op : Thenable < T > ) : Thenable < T > {
119+ return vscode . window . withProgress ( {
120+ location : vscode . ProgressLocation . Window ,
121+ title : title
122+ } , _ => op )
123+ }
124+
125+ /** Connect to an sbt server and run `configureIDE`. */
126+ function configureIDE ( coursierPath : string ) : Thenable < sbtserver . ExecResult > {
127+
128+ function offeringToRetry ( client : rpc . MessageConnection , command : string ) : Thenable < sbtserver . ExecResult > {
129+ return sbtserver . tellSbt ( outputChannel , client , command )
130+ . then ( success => Promise . resolve ( success ) ,
131+ _ => {
132+ outputChannel . show ( )
133+ return vscode . window . showErrorMessage ( "IDE configuration failed (see logs for details)" , "Retry?" )
134+ . then ( retry => {
135+ if ( retry ) return offeringToRetry ( client , command )
136+ else return Promise . reject ( )
137+ } )
138+ } )
139+ }
140+
141+ return withSbtInstance ( outputChannel , coursierPath )
142+ . then ( client => {
143+ // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything
144+ // until sbt/sbt#4370 is fixed.
145+ // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt
146+ // in case of failure), and we're pretty sure configureIDE will pass if they passed.
147+ return offeringToRetry ( client , "compile" ) . then ( _ => {
148+ return offeringToRetry ( client , "test:compile" ) . then ( _ => {
149+ return offeringToRetry ( client , "configureIDE" )
150+ } )
151+ } )
152+ } )
153+ }
154+
95155function runLanguageServer ( coursierPath : string , languageServerArtifactFile : string ) {
96156 fs . readFile ( languageServerArtifactFile , ( err , data ) => {
97157 if ( err ) throw err
@@ -109,10 +169,34 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str
109169 } )
110170}
111171
112- function fetchAndConfigure ( coursierPath : string , sbtArtifact : string , buildSbtFileSource : string , dottyPluginSbtFileSource : string ) {
113- return fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
114- return configureIDE ( sbtClasspath , buildSbtFileSource , dottyPluginSbtFileSource )
172+ /**
173+ * Connects to an existing sbt server, or boots up one instance and connects to it.
174+ */
175+ function withSbtInstance ( log : vscode . OutputChannel , coursierPath : string ) : Thenable < rpc . MessageConnection > {
176+ const serverSocketInfo = path . join ( workspaceRoot , "project" , "target" , "active.json" )
177+
178+ if ( ! fs . existsSync ( serverSocketInfo ) ) {
179+ fetchWithCoursier ( coursierPath , sbtArtifact ) . then ( ( sbtClasspath ) => {
180+ sbtProcess = cpp . spawn ( "java" , [
181+ "-Dsbt.log.noformat=true" ,
182+ "-classpath" , sbtClasspath ,
183+ "xsbt.boot.Boot"
184+ ] ) . childProcess
185+
186+ // Close stdin, otherwise in case of error sbt will block waiting for the
187+ // user input to reload or exit the build.
188+ sbtProcess . stdin . end ( )
189+
190+ sbtProcess . stdout . on ( 'data' , data => {
191+ log . appendLine ( data . toString ( ) )
192+ } )
193+ sbtProcess . stderr . on ( 'data' , data => {
194+ log . appendLine ( data . toString ( ) )
195+ } )
115196 } )
197+ }
198+
199+ return sbtserver . connectToSbtServer ( log )
116200}
117201
118202function fetchWithCoursier ( coursierPath : string , artifact : string , extra : string [ ] = [ ] ) {
@@ -150,54 +234,12 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string
150234 } )
151235}
152236
153- function configureIDE ( sbtClasspath : string ,
154- buildSbtFileSource : string ,
155- dottyPluginSbtFileSource : string ) {
156-
157- return vscode . window . withProgress ( {
158- location : vscode . ProgressLocation . Window ,
159- title : 'Configuring the IDE for Dotty...'
160- } , _ => {
161-
162- // Bootstrap an sbt build
237+ function bootstrapSbtProject ( buildSbtFileSource : string ,
238+ dottyPluginSbtFileSource : string ) {
163239 fs . mkdirSync ( sbtProjectDir )
164240 fs . appendFileSync ( sbtBuildPropertiesFile , `sbt.version=${ sbtVersion } ` )
165241 fs . copyFileSync ( buildSbtFileSource , sbtBuildSbtFile )
166242 fs . copyFileSync ( dottyPluginSbtFileSource , path . join ( sbtProjectDir , "plugins.sbt" ) )
167-
168- // Run sbt to configure the IDE.
169- const sbtPromise =
170- cpp . spawn ( "java" , [
171- "-Dsbt.log.noformat=true" ,
172- "-classpath" , sbtClasspath ,
173- "xsbt.boot.Boot" ,
174- "configureIDE"
175- ] )
176-
177- const sbtProc = sbtPromise . childProcess
178- // Close stdin, otherwise in case of error sbt will block waiting for the
179- // user input to reload or exit the build.
180- sbtProc . stdin . end ( )
181-
182- sbtProc . stdout . on ( 'data' , ( data : Buffer ) => {
183- let msg = data . toString ( ) . trim ( )
184- outputChannel . appendLine ( msg )
185- } )
186- sbtProc . stderr . on ( 'data' , ( data : Buffer ) => {
187- let msg = data . toString ( ) . trim ( )
188- outputChannel . appendLine ( msg )
189- } )
190-
191- sbtProc . on ( 'close' , ( code : number ) => {
192- if ( code != 0 ) {
193- const msg = "Configuring the IDE failed."
194- outputChannel . appendLine ( msg )
195- throw new Error ( msg )
196- }
197- } )
198-
199- return sbtPromise
200- } )
201243}
202244
203245function run ( serverOptions : ServerOptions , isOldServer : boolean ) {
0 commit comments