From 5c34a3afa596872465b62f07ece3509194f5e178 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Tue, 18 Sep 2018 13:21:11 +0200 Subject: [PATCH 01/10] IDE: Initialize sbt project when there's none --- project/Build.scala | 7 ++-- vscode-dotty/src/extension.ts | 70 +++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index e5ddd91d0de4..af52b03a7cec 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -985,11 +985,12 @@ object Build { includeFilter in unmanagedSources := NothingFilter | "*.ts" | "**.json", watchSources in Global ++= (unmanagedSources in Compile).value, resourceGenerators in Compile += Def.task { - val defaultIDEConfig = baseDirectory.value / "out" / "default-dotty-ide-config" - IO.write(defaultIDEConfig, dottyVersion) + // Resources that will be copied when bootstrapping a new project + val buildSbtFile = baseDirectory.value / "out" / "build.sbt" + IO.write(buildSbtFile, s"""scalaVersion := "$dottyVersion"""") val dottyPluginSbtFile = baseDirectory.value / "out" / "dotty-plugin.sbt" IO.write(dottyPluginSbtFile, s"""addSbtPlugin("$dottyOrganization" % "$sbtDottyName" % "$sbtDottyVersion")""") - Seq(defaultIDEConfig, dottyPluginSbtFile) + Seq(buildSbtFile, dottyPluginSbtFile) }, compile in Compile := Def.task { val workingDir = baseDirectory.value diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index e9c25ad5ec2c..e8ca2ccc47fa 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -18,17 +18,31 @@ let extensionContext: ExtensionContext let outputChannel: vscode.OutputChannel export let client: LanguageClient +const sbtVersion = "1.2.3" +const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` +const workspaceRoot = `${vscode.workspace.rootPath}` +const disableDottyIDEFile = path.join(workspaceRoot, ".dotty-ide-disabled") +const sbtProjectDir = path.join(workspaceRoot, "project") +const sbtPluginFile = path.join(sbtProjectDir, "dotty-plugin.sbt") +const sbtBuildPropertiesFile = path.join(sbtProjectDir, "build.properties") +const sbtBuildSbtFile = path.join(workspaceRoot, "build.sbt") +const languageServerArtifactFile = path.join(workspaceRoot, ".dotty-ide-artifact") + +function isUnconfiguredProject() { + return !( fs.existsSync(disableDottyIDEFile) + || fs.existsSync(sbtPluginFile) + || fs.existsSync(sbtBuildPropertiesFile) + || fs.existsSync(sbtBuildSbtFile) + ) +} + export function activate(context: ExtensionContext) { extensionContext = context outputChannel = vscode.window.createOutputChannel("Dotty"); - const sbtArtifact = "org.scala-sbt:sbt-launch:1.2.3" - const buildSbtFile = `${vscode.workspace.rootPath}/build.sbt` - const dottyPluginSbtFile = path.join(extensionContext.extensionPath, './out/dotty-plugin.sbt') - const disableDottyIDEFile = `${vscode.workspace.rootPath}/.dotty-ide-disabled` - const languageServerArtifactFile = `${vscode.workspace.rootPath}/.dotty-ide-artifact` - const languageServerDefaultConfigFile = path.join(extensionContext.extensionPath, './out/default-dotty-ide-config') - const coursierPath = path.join(extensionContext.extensionPath, './out/coursier'); + const coursierPath = path.join(extensionContext.extensionPath, "out", "coursier"); + const dottyPluginSbtFileSource = path.join(extensionContext.extensionPath, "out", "dotty-plugin.sbt") + const buildSbtFileSource = path.join(extensionContext.extensionPath, "out", "build.sbt") vscode.workspace.onWillSaveTextDocument(worksheet.prepareWorksheet) vscode.workspace.onDidSaveTextDocument(document => { @@ -43,7 +57,7 @@ export function activate(context: ExtensionContext) { }) if (process.env['DLS_DEV_MODE']) { - const portFile = `${vscode.workspace.rootPath}/.dotty-ide-dev-port` + const portFile = path.join(workspaceRoot, ".dotty-ide-dev-port") fs.readFile(portFile, (err, port) => { if (err) { outputChannel.appendLine(`Unable to parse ${portFile}`) @@ -51,7 +65,7 @@ export function activate(context: ExtensionContext) { } run({ - module: context.asAbsolutePath('out/src/passthrough-server.js'), + module: context.asAbsolutePath(path.join("out", "src", "passthrough-server.js")), args: [ port.toString() ] }, false) }) @@ -61,20 +75,14 @@ export function activate(context: ExtensionContext) { // otherwise, try propose to start it if there's no build.sbt if (fs.existsSync(languageServerArtifactFile)) { runLanguageServer(coursierPath, languageServerArtifactFile) - } else if (!fs.existsSync(disableDottyIDEFile) && !fs.existsSync(buildSbtFile)) { + } else if (isUnconfiguredProject()) { vscode.window.showInformationMessage( "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?", "Yes", "No" ).then(choice => { if (choice == "Yes") { - fs.readFile(languageServerDefaultConfigFile, (err, data) => { - if (err) throw err - else { - const languageServerScalaVersion = data.toString().trim() - fetchAndConfigure(coursierPath, sbtArtifact, languageServerScalaVersion, dottyPluginSbtFile).then(() => { - runLanguageServer(coursierPath, languageServerArtifactFile) - }) - } + fetchAndConfigure(coursierPath, sbtArtifact, buildSbtFileSource, dottyPluginSbtFileSource).then(() => { + runLanguageServer(coursierPath, languageServerArtifactFile) }) } else { fs.appendFile(disableDottyIDEFile, "", _ => {}) @@ -101,9 +109,9 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str }) } -function fetchAndConfigure(coursierPath: string, sbtArtifact: string, languageServerScalaVersion: string, dottyPluginSbtFile: string) { +function fetchAndConfigure(coursierPath: string, sbtArtifact: string, buildSbtFileSource: string, dottyPluginSbtFileSource: string) { return fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { - return configureIDE(sbtClasspath, languageServerScalaVersion, dottyPluginSbtFile) + return configureIDE(sbtClasspath, buildSbtFileSource, dottyPluginSbtFileSource) }) } @@ -111,7 +119,7 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string return vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: `Fetching ${ artifact }` - }, (progress) => { + }, _ => { const args = [ "-jar", coursierPath, "fetch", @@ -142,21 +150,27 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string }) } -function configureIDE(sbtClasspath: string, languageServerScalaVersion: string, dottyPluginSbtFile: string) { +function configureIDE(sbtClasspath: string, + buildSbtFileSource: string, + dottyPluginSbtFileSource: string) { + return vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: 'Configuring the IDE for Dotty...' - }, (progress) => { + }, _ => { + + // Bootstrap an sbt build + fs.mkdirSync(sbtProjectDir) + fs.appendFileSync(sbtBuildPropertiesFile, `sbt.version=${sbtVersion}`) + fs.copyFileSync(buildSbtFileSource, sbtBuildSbtFile) + fs.copyFileSync(dottyPluginSbtFileSource, path.join(sbtProjectDir, "plugins.sbt")) - // Run sbt to configure the IDE. If the `DottyPlugin` is not present, dynamically load it and - // eventually run `configureIDE`. + // Run sbt to configure the IDE. const sbtPromise = cpp.spawn("java", [ "-Dsbt.log.noformat=true", "-classpath", sbtClasspath, "xsbt.boot.Boot", - `--addPluginSbtFile=${dottyPluginSbtFile}`, - `set every scalaVersion := "${languageServerScalaVersion}"`, "configureIDE" ]) @@ -182,7 +196,7 @@ function configureIDE(sbtClasspath: string, languageServerScalaVersion: string, } }) - return sbtPromise + return sbtPromise }) } From 4789db007821c187aec60085b2d92eb263f5fc42 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Fri, 21 Sep 2018 10:35:36 +0200 Subject: [PATCH 02/10] Run `configureIDE` with sbt server --- vscode-dotty/package.json | 3 +- vscode-dotty/src/extension.ts | 162 +++++++++++++++++++++------------ vscode-dotty/src/sbt-server.ts | 139 ++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 61 deletions(-) create mode 100644 vscode-dotty/src/sbt-server.ts diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json index 682335ff5bbe..3625bd737c42 100644 --- a/vscode-dotty/package.json +++ b/vscode-dotty/package.json @@ -74,7 +74,8 @@ "child-process-promise": "^2.2.1", "compare-versions": "^3.4.0", "vscode-languageclient": "^5.1.0", - "vscode-languageserver": "^5.1.0" + "vscode-languageserver": "^5.1.0", + "vscode-jsonrpc": "4.0.0" }, "devDependencies": { "@types/compare-versions": "^3.0.0", diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index e8ca2ccc47fa..e9ee03dcb8a0 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -6,6 +6,8 @@ import * as path from 'path'; import * as cpp from 'child-process-promise'; import * as compareVersions from 'compare-versions'; +import { ChildProcess } from "child_process"; + import { ExtensionContext } from 'vscode'; import * as vscode from 'vscode'; import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, @@ -14,10 +16,16 @@ import { enableOldServerWorkaround } from './compat' import * as worksheet from './worksheet' +import * as rpc from 'vscode-jsonrpc' +import * as sbtserver from './sbt-server' + let extensionContext: ExtensionContext let outputChannel: vscode.OutputChannel export let client: LanguageClient +/** The sbt process that may have been started by this extension */ +let sbtProcess: ChildProcess + const sbtVersion = "1.2.3" const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` const workspaceRoot = `${vscode.workspace.rootPath}` @@ -71,27 +79,79 @@ export function activate(context: ExtensionContext) { }) } else { - // Check whether `.dotty-ide-artifact` exists. If it does, start the language server, - // otherwise, try propose to start it if there's no build.sbt - if (fs.existsSync(languageServerArtifactFile)) { - runLanguageServer(coursierPath, languageServerArtifactFile) - } else if (isUnconfiguredProject()) { - vscode.window.showInformationMessage( - "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?", - "Yes", "No" + let configuredProject: Thenable = Promise.resolve() + if (isUnconfiguredProject()) { + configuredProject = vscode.window.showInformationMessage( + "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?", + "Yes", "No" ).then(choice => { - if (choice == "Yes") { - fetchAndConfigure(coursierPath, sbtArtifact, buildSbtFileSource, dottyPluginSbtFileSource).then(() => { - runLanguageServer(coursierPath, languageServerArtifactFile) - }) - } else { + if (choice === "Yes") { + bootstrapSbtProject(buildSbtFileSource, dottyPluginSbtFileSource) + return Promise.resolve() + } else if (choice === "No") { fs.appendFile(disableDottyIDEFile, "", _ => {}) + return Promise.reject() } }) } + + configuredProject + .then(_ => withProgress("Configuring Dotty IDE...", configureIDE(coursierPath))) + .then(_ => runLanguageServer(coursierPath, languageServerArtifactFile)) } } +export function deactivate() { + // If sbt was started by this extension, kill the process. + // FIXME: This will be a problem for other clients of this server. + if (sbtProcess) { + sbtProcess.kill() + } +} + +/** + * Display a progress bar with title `title` while `op` completes. + * + * @param title The title of the progress bar + * @param op The thenable that is monitored by the progress bar. + */ +function withProgress(title: string, op: Thenable): Thenable { + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: title + }, _ => op) +} + +/** Connect to an sbt server and run `configureIDE`. */ +function configureIDE(coursierPath: string): Thenable { + + function offeringToRetry(client: rpc.MessageConnection, command: string): Thenable { + return sbtserver.tellSbt(outputChannel, client, command) + .then(success => Promise.resolve(success), + _ => { + outputChannel.show() + return vscode.window.showErrorMessage("IDE configuration failed (see logs for details)", "Retry?") + .then(retry => { + if (retry) return offeringToRetry(client, command) + else return Promise.reject() + }) + }) + } + + return withSbtInstance(outputChannel, coursierPath) + .then(client => { + // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything + // until sbt/sbt#4370 is fixed. + // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt + // in case of failure), and we're pretty sure configureIDE will pass if they passed. + return offeringToRetry(client, "compile").then(_ => { + return offeringToRetry(client, "test:compile").then(_ => { + return offeringToRetry(client, "configureIDE") + }) + }) + }) +} + function runLanguageServer(coursierPath: string, languageServerArtifactFile: string) { fs.readFile(languageServerArtifactFile, (err, data) => { if (err) throw err @@ -109,10 +169,34 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str }) } -function fetchAndConfigure(coursierPath: string, sbtArtifact: string, buildSbtFileSource: string, dottyPluginSbtFileSource: string) { - return fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { - return configureIDE(sbtClasspath, buildSbtFileSource, dottyPluginSbtFileSource) +/** + * Connects to an existing sbt server, or boots up one instance and connects to it. + */ +function withSbtInstance(log: vscode.OutputChannel, coursierPath: string): Thenable { + const serverSocketInfo = path.join(workspaceRoot, "project", "target", "active.json") + + if (!fs.existsSync(serverSocketInfo)) { + fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { + sbtProcess = cpp.spawn("java", [ + "-Dsbt.log.noformat=true", + "-classpath", sbtClasspath, + "xsbt.boot.Boot" + ]).childProcess + + // Close stdin, otherwise in case of error sbt will block waiting for the + // user input to reload or exit the build. + sbtProcess.stdin.end() + + sbtProcess.stdout.on('data', data => { + log.appendLine(data.toString()) + }) + sbtProcess.stderr.on('data', data => { + log.appendLine(data.toString()) + }) }) + } + + return sbtserver.connectToSbtServer(log) } function fetchWithCoursier(coursierPath: string, artifact: string, extra: string[] = []) { @@ -150,54 +234,12 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string }) } -function configureIDE(sbtClasspath: string, - buildSbtFileSource: string, - dottyPluginSbtFileSource: string) { - - return vscode.window.withProgress({ - location: vscode.ProgressLocation.Window, - title: 'Configuring the IDE for Dotty...' - }, _ => { - - // Bootstrap an sbt build +function bootstrapSbtProject(buildSbtFileSource: string, + dottyPluginSbtFileSource: string) { fs.mkdirSync(sbtProjectDir) fs.appendFileSync(sbtBuildPropertiesFile, `sbt.version=${sbtVersion}`) fs.copyFileSync(buildSbtFileSource, sbtBuildSbtFile) fs.copyFileSync(dottyPluginSbtFileSource, path.join(sbtProjectDir, "plugins.sbt")) - - // Run sbt to configure the IDE. - const sbtPromise = - cpp.spawn("java", [ - "-Dsbt.log.noformat=true", - "-classpath", sbtClasspath, - "xsbt.boot.Boot", - "configureIDE" - ]) - - const sbtProc = sbtPromise.childProcess - // Close stdin, otherwise in case of error sbt will block waiting for the - // user input to reload or exit the build. - sbtProc.stdin.end() - - sbtProc.stdout.on('data', (data: Buffer) => { - let msg = data.toString().trim() - outputChannel.appendLine(msg) - }) - sbtProc.stderr.on('data', (data: Buffer) => { - let msg = data.toString().trim() - outputChannel.appendLine(msg) - }) - - sbtProc.on('close', (code: number) => { - if (code != 0) { - const msg = "Configuring the IDE failed." - outputChannel.appendLine(msg) - throw new Error(msg) - } - }) - - return sbtPromise - }) } function run(serverOptions: ServerOptions, isOldServer: boolean) { diff --git a/vscode-dotty/src/sbt-server.ts b/vscode-dotty/src/sbt-server.ts new file mode 100644 index 000000000000..017a161335e1 --- /dev/null +++ b/vscode-dotty/src/sbt-server.ts @@ -0,0 +1,139 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ +// Copy pasted from vscode-sbt-scala + +'use strict' + +import * as fs from 'fs' +import * as net from 'net' +import * as os from 'os' +import * as path from 'path' +import * as url from 'url' + +import * as rpc from 'vscode-jsonrpc' + +import * as vscode from 'vscode' + +/** The result of successful `sbt/exec` call. */ +export interface ExecResult { + status: string + channelName: string + execId: number + commandQueue: string[] + exitCode: number +} + +class CommandLine { + commandLine: string + constructor(commandLine: string) { + this.commandLine = commandLine + } +} + +/** + * Sends `command` to sbt with `sbt/exec`. + * + * @param log Where to log messages between this client and sbt server + * @param connection The connection to sbt server to use + * @param command The command to send to sbt + * + * @return The result of executing `command`. + */ +export function tellSbt(log: vscode.OutputChannel, + connection: rpc.MessageConnection, + command: string): Thenable { + log.appendLine(`>>> ${command}`) + let req = new rpc.RequestType("sbt/exec") + return connection.sendRequest(req, new CommandLine(command)) +} + +/** + * Attempts to connect to an sbt server running in this workspace. + * + * If connection fails, shows an error message and ask the user to retry. + * + * @param log Where to log messages between VSCode and sbt server. + */ +export function connectToSbtServer(log: vscode.OutputChannel): Promise { + return waitForServer().then(socket => { + if (socket) { + let connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(socket), + new rpc.StreamMessageWriter(socket)) + + connection.listen() + + connection.onNotification("window/logMessage", (params) => { + log.appendLine(`<<< [${messageTypeToString(params.type)}] ${params.message}`) + }) + + return connection + } else { + return vscode.window.showErrorMessage("Couldn't connect to sbt server.", "Retry?").then(answer => { + if (answer) { + return connectToSbtServer(log) + } else { + log.show() + return Promise.reject() + } + }) + } + }) +} + +function connectSocket(socket: net.Socket): net.Socket { + let u = discoverUrl(); + if (u.protocol == 'tcp:' && u.port) { + socket.connect(+u.port, '127.0.0.1'); + } else if (u.protocol == 'local:' && u.hostname && os.platform() == 'win32') { + let pipePath = '\\\\.\\pipe\\' + u.hostname; + socket.connect(pipePath); + } else if (u.protocol == 'local:' && u.path) { + socket.connect(u.path); + } else { + throw 'Unknown protocol ' + u.protocol; + } + return socket; +} + +// the port file is hardcoded to a particular location relative to the build. +function discoverUrl(): url.Url { + let pf = path.join(process.cwd(), 'project', 'target', 'active.json'); + let portfile = JSON.parse(fs.readFileSync(pf).toString()); + return url.parse(portfile.uri); +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function waitForServer(): Promise { + let socket: net.Socket | null = null + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: "Connecting to sbt server..." + }, async _ => { + let retries = 60; + while (!socket && retries > 0) { + try { socket = connectSocket(new net.Socket()) } + catch (e) { + retries--; + await delay(1000); + } + } + return socket + }).then(_ => socket) +} + +function messageTypeToString(messageType: number): string { + if (messageType == 1) return "error" + else if (messageType == 2) return "warn" + else if (messageType == 3) return "info" + else if (messageType == 4) return "log" + else return "???" +} + From 6c96109bbf0a5363110298052f0cc3a131222ca7 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 24 Sep 2018 14:01:46 +0200 Subject: [PATCH 03/10] Check sbt is alive and restart it if necessary This commis adds a status bar that shows the status of sbt server, and a timer that periodically checks that sbt server is still alive, starting a new instance if necessary. --- .../tools/sbtplugin/DottyIDEPlugin.scala | 5 +- vscode-dotty/src/extension.ts | 158 +++++++++++++----- vscode-dotty/src/sbt-server.ts | 56 +++---- 3 files changed, 141 insertions(+), 78 deletions(-) diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala index 4d5dd8348fd8..60f0735f8014 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -269,6 +269,9 @@ object DottyIDEPlugin extends AutoPlugin { Command.process("runCode", state1) } + /** An sbt command that does nothing. We use it to check that sbt server is still alive. */ + def nopCommand = Command.command("nop")(state => state) + private def projectConfigTask(config: Configuration): Initialize[Task[Option[ProjectConfig]]] = Def.taskDyn { val depClasspath = Attributed.data((dependencyClasspath in config).value) @@ -311,7 +314,7 @@ object DottyIDEPlugin extends AutoPlugin { ) override def buildSettings: Seq[Setting[_]] = Seq( - commands ++= Seq(configureIDE, compileForIDE, launchIDE), + commands ++= Seq(configureIDE, compileForIDE, launchIDE, nopCommand), excludeFromIDE := false, diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index e9ee03dcb8a0..d43926b6511d 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -24,7 +24,16 @@ let outputChannel: vscode.OutputChannel export let client: LanguageClient /** The sbt process that may have been started by this extension */ -let sbtProcess: ChildProcess +let sbtProcess: ChildProcess | undefined + +/** The status bar where the show the status of sbt server */ +let sbtStatusBar: vscode.StatusBarItem + +/** Interval in ms to check that sbt is alive */ +const sbtCheckIntervalMs = 10 * 1000 + +/** A command that we use to check that sbt is still alive. */ +export const nopCommand = "nop" const sbtVersion = "1.2.3" const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` @@ -96,11 +105,56 @@ export function activate(context: ExtensionContext) { } configuredProject - .then(_ => withProgress("Configuring Dotty IDE...", configureIDE(coursierPath))) + .then(_ => connectToSbt(coursierPath)) + .then(sbt => withProgress("Configuring Dotty IDE...", configureIDE(sbt))) .then(_ => runLanguageServer(coursierPath, languageServerArtifactFile)) } } +/** + * Connect to sbt server (possibly by starting a new instance) and keep verifying that the + * connection is still alive. If it dies, restart sbt server. + */ +function connectToSbt(coursierPath: string): Thenable { + if (!sbtStatusBar) sbtStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right) + sbtStatusBar.text = "sbt server: connecting $(sync)" + sbtStatusBar.show() + + return offeringToRetry(() => { + return withSbtInstance(outputChannel, coursierPath).then(connection => { + markSbtUp() + const interval = setInterval(() => checkSbt(interval, connection, coursierPath), sbtCheckIntervalMs) + return connection + }) + }, "Couldn't connect to sbt server (see log for details)") +} + +/** Mark sbt server as alive in the status bar */ +function markSbtUp(timeout?: NodeJS.Timer) { + sbtStatusBar.text = "sbt server: up $(check)" + if (timeout) clearTimeout(timeout) +} + +/** Mark sbt server as dead and try to reconnect */ +function markSbtDownAndReconnect(coursierPath: string) { + sbtStatusBar.text = "sbt server: down $(x)" + if (sbtProcess) { + sbtProcess.kill() + sbtProcess = undefined + } + connectToSbt(coursierPath) +} + +/** Check that sbt is alive, try to reconnect if it is dead. */ +function checkSbt(interval: NodeJS.Timer, connection: rpc.MessageConnection, coursierPath: string) { + sbtserver.tellSbt(outputChannel, connection, nopCommand) + .then(_ => markSbtUp(), + _ => { + clearInterval(interval) + markSbtDownAndReconnect(coursierPath) + }) +} + export function deactivate() { // If sbt was started by this extension, kill the process. // FIXME: This will be a problem for other clients of this server. @@ -123,33 +177,45 @@ function withProgress(title: string, op: Thenable): Thenable { } /** Connect to an sbt server and run `configureIDE`. */ -function configureIDE(coursierPath: string): Thenable { - - function offeringToRetry(client: rpc.MessageConnection, command: string): Thenable { - return sbtserver.tellSbt(outputChannel, client, command) - .then(success => Promise.resolve(success), - _ => { - outputChannel.show() - return vscode.window.showErrorMessage("IDE configuration failed (see logs for details)", "Retry?") - .then(retry => { - if (retry) return offeringToRetry(client, command) - else return Promise.reject() - }) - }) +function configureIDE(sbt: rpc.MessageConnection): Thenable { + + const tellSbt = (command: string) => { + return () => sbtserver.tellSbt(outputChannel, sbt, command) } - return withSbtInstance(outputChannel, coursierPath) - .then(client => { - // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything - // until sbt/sbt#4370 is fixed. - // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt - // in case of failure), and we're pretty sure configureIDE will pass if they passed. - return offeringToRetry(client, "compile").then(_ => { - return offeringToRetry(client, "test:compile").then(_ => { - return offeringToRetry(client, "configureIDE") - }) - }) + const failMessage = "`configureIDE` failed (see log for details)" + + // `configureIDE` is a command, which means that upon failure, sbt won't tell us anything + // until sbt/sbt#4370 is fixed. + // We run `compile` and `test:compile` first because they're tasks (so we get feedback from sbt + // in case of failure), and we're pretty sure configureIDE will pass if they passed. + return offeringToRetry(tellSbt("compile"), failMessage).then(_ => { + return offeringToRetry(tellSbt("test:compile"), failMessage).then(_ => { + return offeringToRetry(tellSbt("configureIDE"), failMessage) }) + }) +} + +/** + * Present the user with a dialog to retry `op` after a failure, returns its result in case of + * success. + * + * @param op The operation to perform + * @param failMessage The message to display in the dialog offering to retry `op`. + * @return A promise that will either resolve to the result of `op`, or a dialog that will let + * the user retry the operation. + */ +function offeringToRetry(op: () => Thenable, failMessage: string): Thenable { + return op() + .then(success => Promise.resolve(success), + _ => { + outputChannel.show() + return vscode.window.showErrorMessage(failMessage, "Retry?") + .then(retry => { + if (retry) return offeringToRetry(op, failMessage) + else return Promise.reject() + }) + }) } function runLanguageServer(coursierPath: string, languageServerArtifactFile: string) { @@ -169,6 +235,27 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str }) } +function startNewSbtInstance(log: vscode.OutputChannel, coursierPath: string) { + fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { + sbtProcess = cpp.spawn("java", [ + "-Dsbt.log.noformat=true", + "-classpath", sbtClasspath, + "xsbt.boot.Boot" + ]).childProcess + + // Close stdin, otherwise in case of error sbt will block waiting for the + // user input to reload or exit the build. + sbtProcess.stdin.end() + + sbtProcess.stdout.on('data', data => { + log.append(data.toString()) + }) + sbtProcess.stderr.on('data', data => { + log.append(data.toString()) + }) + }) +} + /** * Connects to an existing sbt server, or boots up one instance and connects to it. */ @@ -176,24 +263,7 @@ function withSbtInstance(log: vscode.OutputChannel, coursierPath: string): Thena const serverSocketInfo = path.join(workspaceRoot, "project", "target", "active.json") if (!fs.existsSync(serverSocketInfo)) { - fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { - sbtProcess = cpp.spawn("java", [ - "-Dsbt.log.noformat=true", - "-classpath", sbtClasspath, - "xsbt.boot.Boot" - ]).childProcess - - // Close stdin, otherwise in case of error sbt will block waiting for the - // user input to reload or exit the build. - sbtProcess.stdin.end() - - sbtProcess.stdout.on('data', data => { - log.appendLine(data.toString()) - }) - sbtProcess.stderr.on('data', data => { - log.appendLine(data.toString()) - }) - }) + startNewSbtInstance(log, coursierPath) } return sbtserver.connectToSbtServer(log) diff --git a/vscode-dotty/src/sbt-server.ts b/vscode-dotty/src/sbt-server.ts index 017a161335e1..b47a65a74bc2 100644 --- a/vscode-dotty/src/sbt-server.ts +++ b/vscode-dotty/src/sbt-server.ts @@ -18,6 +18,8 @@ import * as rpc from 'vscode-jsonrpc' import * as vscode from 'vscode' +import { nopCommand } from './extension' + /** The result of successful `sbt/exec` call. */ export interface ExecResult { status: string @@ -43,45 +45,32 @@ class CommandLine { * * @return The result of executing `command`. */ -export function tellSbt(log: vscode.OutputChannel, - connection: rpc.MessageConnection, - command: string): Thenable { +export async function tellSbt(log: vscode.OutputChannel, + connection: rpc.MessageConnection, + command: string): Promise { log.appendLine(`>>> ${command}`) - let req = new rpc.RequestType("sbt/exec") - return connection.sendRequest(req, new CommandLine(command)) + const req = new rpc.RequestType("sbt/exec") + return await connection.sendRequest(req, new CommandLine(command)) } /** * Attempts to connect to an sbt server running in this workspace. * - * If connection fails, shows an error message and ask the user to retry. - * * @param log Where to log messages between VSCode and sbt server. */ export function connectToSbtServer(log: vscode.OutputChannel): Promise { return waitForServer().then(socket => { - if (socket) { - let connection = rpc.createMessageConnection( - new rpc.StreamMessageReader(socket), - new rpc.StreamMessageWriter(socket)) - - connection.listen() - - connection.onNotification("window/logMessage", (params) => { - log.appendLine(`<<< [${messageTypeToString(params.type)}] ${params.message}`) - }) - - return connection - } else { - return vscode.window.showErrorMessage("Couldn't connect to sbt server.", "Retry?").then(answer => { - if (answer) { - return connectToSbtServer(log) - } else { - log.show() - return Promise.reject() - } - }) - } + let connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(socket), + new rpc.StreamMessageWriter(socket)) + + connection.listen() + + connection.onNotification("window/logMessage", (params) => { + log.appendLine(`<<< [${messageTypeToString(params.type)}] ${params.message}`) + }) + + return tellSbt(log, connection, nopCommand).then(_ => connection) }) } @@ -111,8 +100,8 @@ function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -async function waitForServer(): Promise { - let socket: net.Socket | null = null +async function waitForServer(): Promise { + let socket: net.Socket return vscode.window.withProgress({ location: vscode.ProgressLocation.Window, title: "Connecting to sbt server..." @@ -125,8 +114,9 @@ async function waitForServer(): Promise { await delay(1000); } } - return socket - }).then(_ => socket) + if (socket) return Promise.resolve(socket) + else return Promise.reject() + }) } function messageTypeToString(messageType: number): string { From 04a9d56e9322b382960a96cf4b1a5f914d5b9db2 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Tue, 25 Sep 2018 10:08:49 +0200 Subject: [PATCH 04/10] Don't pass output channel everywhere --- vscode-dotty/src/extension.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index d43926b6511d..54eeba918903 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -121,7 +121,7 @@ function connectToSbt(coursierPath: string): Thenable { sbtStatusBar.show() return offeringToRetry(() => { - return withSbtInstance(outputChannel, coursierPath).then(connection => { + return withSbtInstance(coursierPath).then(connection => { markSbtUp() const interval = setInterval(() => checkSbt(interval, connection, coursierPath), sbtCheckIntervalMs) return connection @@ -235,7 +235,7 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str }) } -function startNewSbtInstance(log: vscode.OutputChannel, coursierPath: string) { +function startNewSbtInstance(coursierPath: string) { fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { sbtProcess = cpp.spawn("java", [ "-Dsbt.log.noformat=true", @@ -248,10 +248,10 @@ function startNewSbtInstance(log: vscode.OutputChannel, coursierPath: string) { sbtProcess.stdin.end() sbtProcess.stdout.on('data', data => { - log.append(data.toString()) + outputChannel.append(data.toString()) }) sbtProcess.stderr.on('data', data => { - log.append(data.toString()) + outputChannel.append(data.toString()) }) }) } @@ -259,14 +259,14 @@ function startNewSbtInstance(log: vscode.OutputChannel, coursierPath: string) { /** * Connects to an existing sbt server, or boots up one instance and connects to it. */ -function withSbtInstance(log: vscode.OutputChannel, coursierPath: string): Thenable { +function withSbtInstance(coursierPath: string): Thenable { const serverSocketInfo = path.join(workspaceRoot, "project", "target", "active.json") if (!fs.existsSync(serverSocketInfo)) { - startNewSbtInstance(log, coursierPath) + startNewSbtInstance(coursierPath) } - return sbtserver.connectToSbtServer(log) + return sbtserver.connectToSbtServer(outputChannel) } function fetchWithCoursier(coursierPath: string, artifact: string, extra: string[] = []) { From 501fbef3dd1f00f86e02040e958e94bedf25dbe6 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Tue, 25 Sep 2018 10:32:16 +0200 Subject: [PATCH 05/10] Remove unused parameter --- vscode-dotty/src/extension.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 54eeba918903..06110d8c5d6a 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -130,9 +130,8 @@ function connectToSbt(coursierPath: string): Thenable { } /** Mark sbt server as alive in the status bar */ -function markSbtUp(timeout?: NodeJS.Timer) { +function markSbtUp() { sbtStatusBar.text = "sbt server: up $(check)" - if (timeout) clearTimeout(timeout) } /** Mark sbt server as dead and try to reconnect */ From c1f0e759622175ccba05888b3e3928a7c75f5cab Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 1 Oct 2018 14:50:19 +0200 Subject: [PATCH 06/10] Remove nop command We use `connection.onClose` to automatically reconnect when the connection with sbt is closed. --- .../dotty/tools/sbtplugin/DottyIDEPlugin.scala | 5 +---- vscode-dotty/src/extension.ts | 18 +----------------- vscode-dotty/src/sbt-server.ts | 4 +--- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala index 60f0735f8014..4d5dd8348fd8 100644 --- a/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala +++ b/sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala @@ -269,9 +269,6 @@ object DottyIDEPlugin extends AutoPlugin { Command.process("runCode", state1) } - /** An sbt command that does nothing. We use it to check that sbt server is still alive. */ - def nopCommand = Command.command("nop")(state => state) - private def projectConfigTask(config: Configuration): Initialize[Task[Option[ProjectConfig]]] = Def.taskDyn { val depClasspath = Attributed.data((dependencyClasspath in config).value) @@ -314,7 +311,7 @@ object DottyIDEPlugin extends AutoPlugin { ) override def buildSettings: Seq[Setting[_]] = Seq( - commands ++= Seq(configureIDE, compileForIDE, launchIDE, nopCommand), + commands ++= Seq(configureIDE, compileForIDE, launchIDE), excludeFromIDE := false, diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 06110d8c5d6a..32aa0af285d1 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -29,12 +29,6 @@ let sbtProcess: ChildProcess | undefined /** The status bar where the show the status of sbt server */ let sbtStatusBar: vscode.StatusBarItem -/** Interval in ms to check that sbt is alive */ -const sbtCheckIntervalMs = 10 * 1000 - -/** A command that we use to check that sbt is still alive. */ -export const nopCommand = "nop" - const sbtVersion = "1.2.3" const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` const workspaceRoot = `${vscode.workspace.rootPath}` @@ -122,8 +116,8 @@ function connectToSbt(coursierPath: string): Thenable { return offeringToRetry(() => { return withSbtInstance(coursierPath).then(connection => { + connection.onClose(() => markSbtDownAndReconnect(coursierPath)) markSbtUp() - const interval = setInterval(() => checkSbt(interval, connection, coursierPath), sbtCheckIntervalMs) return connection }) }, "Couldn't connect to sbt server (see log for details)") @@ -144,16 +138,6 @@ function markSbtDownAndReconnect(coursierPath: string) { connectToSbt(coursierPath) } -/** Check that sbt is alive, try to reconnect if it is dead. */ -function checkSbt(interval: NodeJS.Timer, connection: rpc.MessageConnection, coursierPath: string) { - sbtserver.tellSbt(outputChannel, connection, nopCommand) - .then(_ => markSbtUp(), - _ => { - clearInterval(interval) - markSbtDownAndReconnect(coursierPath) - }) -} - export function deactivate() { // If sbt was started by this extension, kill the process. // FIXME: This will be a problem for other clients of this server. diff --git a/vscode-dotty/src/sbt-server.ts b/vscode-dotty/src/sbt-server.ts index b47a65a74bc2..ee4f2e44e479 100644 --- a/vscode-dotty/src/sbt-server.ts +++ b/vscode-dotty/src/sbt-server.ts @@ -18,8 +18,6 @@ import * as rpc from 'vscode-jsonrpc' import * as vscode from 'vscode' -import { nopCommand } from './extension' - /** The result of successful `sbt/exec` call. */ export interface ExecResult { status: string @@ -70,7 +68,7 @@ export function connectToSbtServer(log: vscode.OutputChannel): Promise connection) + return connection }) } From 786788b97800dd70841472e999c5f4a286426486 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 10 Oct 2018 09:28:29 +0200 Subject: [PATCH 07/10] IDE: Set working directory when starting sbt --- vscode-dotty/package-lock.json | 74 +++++++--------------------------- vscode-dotty/package.json | 2 +- vscode-dotty/src/extension.ts | 11 ++--- 3 files changed, 21 insertions(+), 66 deletions(-) diff --git a/vscode-dotty/package-lock.json b/vscode-dotty/package-lock.json index f7f7b10e86cd..3c0d58df9fb6 100644 --- a/vscode-dotty/package-lock.json +++ b/vscode-dotty/package-lock.json @@ -210,16 +210,6 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "child-process-promise": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", - "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", - "requires": { - "cross-spawn": "^4.0.2", - "node-version": "^1.0.0", - "promise-polyfill": "^6.0.1" - } - }, "clone": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", @@ -293,15 +283,6 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -1070,11 +1051,6 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, "isobject": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", @@ -1163,15 +1139,6 @@ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", "dev": true }, - "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", @@ -1340,11 +1307,6 @@ "minimatch": "^3.0.0" } }, - "node-version": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", - "integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==" - }, "node.extend": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", @@ -1485,15 +1447,20 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, - "promise-polyfill": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", - "integrity": "sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "promisify-child-process": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/promisify-child-process/-/promisify-child-process-2.1.2.tgz", + "integrity": "sha512-j2BRwNaM7fUwrd67avtqSTRevQXZiqS+T4Ky3VVaQdvzkPpsTByBAv+ZyBxuXgV/eUrCe2qYrOZvPvd+sMryeg==", + "requires": { + "@types/node": "^10.11.3" + }, + "dependencies": { + "@types/node": { + "version": "10.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.11.6.tgz", + "integrity": "sha512-fnA7yvqg3oKQDb3skBif9w5RRKVKAaeKeNuLzZL37XcSiWL4IoSXQnnbchR3UnBu2EMLHBip7ZVEkqoIVBP8QQ==" + } + } }, "punycode": { "version": "1.4.1", @@ -2228,14 +2195,6 @@ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz", "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==" }, - "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "requires": { - "isexe": "^2.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2248,11 +2207,6 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/vscode-dotty/package.json b/vscode-dotty/package.json index 3625bd737c42..47afe1e6cc36 100644 --- a/vscode-dotty/package.json +++ b/vscode-dotty/package.json @@ -71,7 +71,7 @@ "scala-lang.scala" ], "dependencies": { - "child-process-promise": "^2.2.1", + "promisify-child-process": "^2.1.2", "compare-versions": "^3.4.0", "vscode-languageclient": "^5.1.0", "vscode-languageserver": "^5.1.0", diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 32aa0af285d1..f8c858347ae8 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as cpp from 'child-process-promise'; +import * as cpp from 'promisify-child-process'; import * as compareVersions from 'compare-versions'; import { ChildProcess } from "child_process"; @@ -224,7 +224,9 @@ function startNewSbtInstance(coursierPath: string) { "-Dsbt.log.noformat=true", "-classpath", sbtClasspath, "xsbt.boot.Boot" - ]).childProcess + ], { + cwd: workspaceRoot + }) // Close stdin, otherwise in case of error sbt will block waiting for the // user input to reload or exit the build. @@ -263,8 +265,7 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string "-p", artifact ].concat(extra) - const coursierPromise = cpp.spawn("java", args) - const coursierProc = coursierPromise.childProcess + const coursierProc = cpp.spawn("java", args) let classPath = "" @@ -283,7 +284,7 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string throw new Error(msg) } }) - return coursierPromise.then(() => { return classPath }) + return coursierProc.then(() => { return classPath }) }) } From cf2c36737661c8130a93ce386fb072f0b7b3b33c Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 10 Oct 2018 11:00:06 +0200 Subject: [PATCH 08/10] IDE: Don't rely on current working directory --- vscode-dotty/src/extension.ts | 2 +- vscode-dotty/src/sbt-server.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index f8c858347ae8..bbce7cb498f7 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -31,7 +31,7 @@ let sbtStatusBar: vscode.StatusBarItem const sbtVersion = "1.2.3" const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` -const workspaceRoot = `${vscode.workspace.rootPath}` +export const workspaceRoot = `${vscode.workspace.rootPath}` const disableDottyIDEFile = path.join(workspaceRoot, ".dotty-ide-disabled") const sbtProjectDir = path.join(workspaceRoot, "project") const sbtPluginFile = path.join(sbtProjectDir, "dotty-plugin.sbt") diff --git a/vscode-dotty/src/sbt-server.ts b/vscode-dotty/src/sbt-server.ts index ee4f2e44e479..087e6a88d8b1 100644 --- a/vscode-dotty/src/sbt-server.ts +++ b/vscode-dotty/src/sbt-server.ts @@ -18,6 +18,8 @@ import * as rpc from 'vscode-jsonrpc' import * as vscode from 'vscode' +import { workspaceRoot } from './extension' + /** The result of successful `sbt/exec` call. */ export interface ExecResult { status: string @@ -89,7 +91,7 @@ function connectSocket(socket: net.Socket): net.Socket { // the port file is hardcoded to a particular location relative to the build. function discoverUrl(): url.Url { - let pf = path.join(process.cwd(), 'project', 'target', 'active.json'); + let pf = path.join(workspaceRoot, 'project', 'target', 'active.json'); let portfile = JSON.parse(fs.readFileSync(pf).toString()); return url.parse(portfile.uri); } From db5207238a6e6d1d4a7401341e294aef69a7b994 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 17 Oct 2018 15:02:13 +0200 Subject: [PATCH 09/10] Address review --- vscode-dotty/src/extension.ts | 19 +++++++++---------- vscode-dotty/src/types.d.ts | 14 -------------- 2 files changed, 9 insertions(+), 24 deletions(-) delete mode 100644 vscode-dotty/src/types.d.ts diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index bbce7cb498f7..5af21f57a07c 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as cpp from 'promisify-child-process'; +import * as pcp from 'promisify-child-process'; import * as compareVersions from 'compare-versions'; import { ChildProcess } from "child_process"; @@ -39,11 +39,10 @@ const sbtBuildPropertiesFile = path.join(sbtProjectDir, "build.properties") const sbtBuildSbtFile = path.join(workspaceRoot, "build.sbt") const languageServerArtifactFile = path.join(workspaceRoot, ".dotty-ide-artifact") -function isUnconfiguredProject() { - return !( fs.existsSync(disableDottyIDEFile) - || fs.existsSync(sbtPluginFile) - || fs.existsSync(sbtBuildPropertiesFile) - || fs.existsSync(sbtBuildSbtFile) +function isConfiguredProject() { + return ( fs.existsSync(sbtPluginFile) + || fs.existsSync(sbtBuildPropertiesFile) + || fs.existsSync(sbtBuildSbtFile) ) } @@ -81,9 +80,9 @@ export function activate(context: ExtensionContext) { }, false) }) - } else { + } else if (!fs.existsSync(disableDottyIDEFile)) { let configuredProject: Thenable = Promise.resolve() - if (isUnconfiguredProject()) { + if (!isConfiguredProject()) { configuredProject = vscode.window.showInformationMessage( "This looks like an unconfigured Scala project. Would you like to start the Dotty IDE?", "Yes", "No" @@ -220,7 +219,7 @@ function runLanguageServer(coursierPath: string, languageServerArtifactFile: str function startNewSbtInstance(coursierPath: string) { fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => { - sbtProcess = cpp.spawn("java", [ + sbtProcess = pcp.spawn("java", [ "-Dsbt.log.noformat=true", "-classpath", sbtClasspath, "xsbt.boot.Boot" @@ -265,7 +264,7 @@ function fetchWithCoursier(coursierPath: string, artifact: string, extra: string "-p", artifact ].concat(extra) - const coursierProc = cpp.spawn("java", args) + const coursierProc = pcp.spawn("java", args) let classPath = "" diff --git a/vscode-dotty/src/types.d.ts b/vscode-dotty/src/types.d.ts deleted file mode 100644 index a15062140cf4..000000000000 --- a/vscode-dotty/src/types.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'child-process-promise' { - - import {ChildProcess} from "child_process"; - - interface ChildPromiseResult { - code: number; - } - - interface ChildProcessPromise extends Promise { - childProcess: ChildProcess; - } - - function spawn(command: string, args: string[]): ChildProcessPromise; -} From d27f5a4079d3f7aee045079f9a7ec8b77174793a Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 17 Oct 2018 15:04:12 +0200 Subject: [PATCH 10/10] Start sbt only if necessary, don't keep server alive This commit changes the behavior of the IDE so that it runs `configureIDE` only if the project hasn't been configured yet. sbt will now be started only if necessary, and the server will not be kept alive longer than necessary. --- vscode-dotty/src/extension.ts | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/vscode-dotty/src/extension.ts b/vscode-dotty/src/extension.ts index 5af21f57a07c..4a215bab4f43 100644 --- a/vscode-dotty/src/extension.ts +++ b/vscode-dotty/src/extension.ts @@ -26,9 +26,6 @@ export let client: LanguageClient /** The sbt process that may have been started by this extension */ let sbtProcess: ChildProcess | undefined -/** The status bar where the show the status of sbt server */ -let sbtStatusBar: vscode.StatusBarItem - const sbtVersion = "1.2.3" const sbtArtifact = `org.scala-sbt:sbt-launch:${sbtVersion}` export const workspaceRoot = `${vscode.workspace.rootPath}` @@ -95,11 +92,14 @@ export function activate(context: ExtensionContext) { return Promise.reject() } }) + .then(_ => connectToSbt(coursierPath)) + .then(sbt => { + return withProgress("Configuring Dotty IDE...", configureIDE(sbt)) + .then(_ => { sbtserver.tellSbt(outputChannel, sbt, "exit") }) + }) } configuredProject - .then(_ => connectToSbt(coursierPath)) - .then(sbt => withProgress("Configuring Dotty IDE...", configureIDE(sbt))) .then(_ => runLanguageServer(coursierPath, languageServerArtifactFile)) } } @@ -109,34 +109,14 @@ export function activate(context: ExtensionContext) { * connection is still alive. If it dies, restart sbt server. */ function connectToSbt(coursierPath: string): Thenable { - if (!sbtStatusBar) sbtStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right) - sbtStatusBar.text = "sbt server: connecting $(sync)" - sbtStatusBar.show() return offeringToRetry(() => { return withSbtInstance(coursierPath).then(connection => { - connection.onClose(() => markSbtDownAndReconnect(coursierPath)) - markSbtUp() return connection }) }, "Couldn't connect to sbt server (see log for details)") } -/** Mark sbt server as alive in the status bar */ -function markSbtUp() { - sbtStatusBar.text = "sbt server: up $(check)" -} - -/** Mark sbt server as dead and try to reconnect */ -function markSbtDownAndReconnect(coursierPath: string) { - sbtStatusBar.text = "sbt server: down $(x)" - if (sbtProcess) { - sbtProcess.kill() - sbtProcess = undefined - } - connectToSbt(coursierPath) -} - export function deactivate() { // If sbt was started by this extension, kill the process. // FIXME: This will be a problem for other clients of this server.