Skip to content

Let VSCode start the language server #4304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ object Build {
}
val dottyNonBootstrappedVersion = dottyVersion + "-nonbootstrapped"

val sbtDottyName = "sbt-dotty"
val sbtDottyVersion = {
val base = "0.2.3"
if (isRelease) base else base + "-SNAPSHOT"
}

val agentOptions = List(
// "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
// "-agentpath:/home/dark/opt/yjp-2013-build-13072/bin/linux-x86-64/libyjpagent.so"
Expand Down Expand Up @@ -930,11 +936,8 @@ object Build {
lazy val `sbt-dotty` = project.in(file("sbt-dotty")).
settings(commonSettings).
settings(
version := {
val base = "0.2.3"
if (isRelease) base else base + "-SNAPSHOT"
},

name := sbtDottyName,
version := sbtDottyVersion,
// Keep in sync with inject-sbt-dotty.sbt
libraryDependencies ++= Seq(
Dependencies.`jackson-databind`,
Expand Down Expand Up @@ -973,9 +976,16 @@ object Build {
publishArtifact := false,
includeFilter in unmanagedSources := NothingFilter | "*.ts" | "**.json",
watchSources in Global ++= (unmanagedSources in Compile).value,
compile in Compile := {
resourceGenerators in Compile += Def.task {
val defaultIDEConfig = baseDirectory.value / "out" / "default-dotty-ide-config"
IO.write(defaultIDEConfig, dottyVersion)
val dottyPluginSbtFile = baseDirectory.value / "out" / "dotty-plugin.sbt"
IO.write(dottyPluginSbtFile, s"""addSbtPlugin("$dottyOrganization" % "$sbtDottyName" % "$sbtDottyVersion")""")
Seq(defaultIDEConfig, dottyPluginSbtFile)
},
compile in Compile := Def.task {
val workingDir = baseDirectory.value
val coursier = workingDir / "out/coursier"
val coursier = workingDir / "out" / "coursier"
val packageJson = workingDir / "package.json"
if (!coursier.exists || packageJson.lastModified > coursier.lastModified)
runProcess(Seq("npm", "install"), wait = true, directory = workingDir)
Expand All @@ -988,7 +998,7 @@ object Build {
runProcess(codeCommand.value ++ Seq("--install-extension", "daltonjorge.scala"), wait = true)

sbt.internal.inc.Analysis.Empty
},
}.dependsOn(managedResources in Compile).value,
sbt.Keys.`package`:= {
runProcess(Seq("vsce", "package"), wait = true, directory = baseDirectory.value)

Expand Down
1 change: 1 addition & 0 deletions vscode-dotty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
],
"main": "./out/src/extension",
"activationEvents": [
"onLanguage:scala",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with doing this is that we conflict with every other language server for Scala

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What can we do instead? The goal of this PR is to be able to get the Dotty IDE working without any prior setup (so, nobody would have generated .dotty-ide.json).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, if there's no configuration for the IDE a message is shown asking the user whether she wants to start the IDE or not. If there's a conflict with another language server, she can just say no. If the IDE is configured, then it'll automatically start up.

"workspaceContains:.dotty-ide.json"
],
"languages": [
Expand Down
150 changes: 106 additions & 44 deletions vscode-dotty/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,73 +16,135 @@ export function activate(context: ExtensionContext) {
extensionContext = context
outputChannel = vscode.window.createOutputChannel('Dotty Language Client');

const artifactFile = `${vscode.workspace.rootPath}/.dotty-ide-artifact`
fs.readFile(artifactFile, (err, data) => {
if (err) {
outputChannel.append(`Unable to parse ${artifactFile}`)
throw err
}
const artifact = data.toString().trim()

if (process.env['DLS_DEV_MODE']) {
const portFile = `${vscode.workspace.rootPath}/.dotty-ide-dev-port`
fs.readFile(portFile, (err, port) => {
if (err) {
outputChannel.append(`Unable to parse ${portFile}`)
throw err
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');

if (process.env['DLS_DEV_MODE']) {
const portFile = `${vscode.workspace.rootPath}/.dotty-ide-dev-port`
fs.readFile(portFile, (err, port) => {
if (err) {
outputChannel.append(`Unable to parse ${portFile}`)
throw err
}

run({
module: context.asAbsolutePath('out/src/passthrough-server.js'),
args: [ port.toString() ]
})
})

} 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 (!fs.existsSync(disableDottyIDEFile) && !fs.existsSync(buildSbtFile)) {
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)
})
}
})
} else {
fs.appendFile(disableDottyIDEFile, "", _ => {})
}
})
}
}
}

function runLanguageServer(coursierPath: string, languageServerArtifactFile: string) {
fs.readFile(languageServerArtifactFile, (err, data) => {
if (err) throw err
else {
const languageServerArtifact = data.toString().trim()
fetchWithCoursier(coursierPath, languageServerArtifact).then((languageServerClasspath) => {
run({
module: context.asAbsolutePath('out/src/passthrough-server.js'),
args: [ port.toString() ]
command: "java",
args: ["-classpath", languageServerClasspath, "dotty.tools.languageserver.Main", "-stdio"]
})
})
} else {
fetchAndRun(artifact)
}
})
}

function fetchAndRun(artifact: string) {
const coursierPath = path.join(extensionContext.extensionPath, './out/coursier');

vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: 'Fetching the Dotty Language Server'
}, (progress) => {
function fetchAndConfigure(coursierPath: string, sbtArtifact: string, languageServerScalaVersion: string, dottyPluginSbtFile: string) {
return fetchWithCoursier(coursierPath, sbtArtifact).then((sbtClasspath) => {
return configureIDE(sbtClasspath, languageServerScalaVersion, dottyPluginSbtFile)
})
}

const coursierPromise =
cpp.spawn("java", [
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",
"-p",
artifact
])
const coursierProc = coursierPromise.childProcess
].concat(extra)
const coursierPromise = cpp.spawn("java", args)
const coursierProc = coursierPromise.childProcess

let classPath = ""
let classPath = ""

coursierProc.stdout.on('data', (data: Buffer) => {
classPath += data.toString().trim()
})
coursierProc.stderr.on('data', (data: Buffer) => {
let msg = data.toString()
outputChannel.append(msg)
coursierProc.stdout.on('data', (data: Buffer) => {
classPath += data.toString().trim()
})

coursierProc.on('close', (code: number) => {
if (code != 0) {
let msg = `Couldn't fetch '${ artifact }' (exit code ${ code }).`
outputChannel.append(msg)
throw new Error(msg)
}
})
return coursierPromise.then(() => { return classPath })
})
}

function configureIDE(sbtClasspath: string, languageServerScalaVersion: string, dottyPluginSbtFile: string) {
return vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: 'Configuring the IDE for Dotty...'
}, (progress) => {

// Run sbt to configure the IDE. If the `DottyPlugin` is not present, dynamically load it and
// eventually run `configureIDE`.
const sbtPromise =
cpp.spawn("java", [
"-classpath", sbtClasspath,
"xsbt.boot.Boot",
`--addPluginSbtFile=${dottyPluginSbtFile}`,
`set every scalaVersion := "${languageServerScalaVersion}"`,
"configureIDE"
])

coursierProc.on('close', (code: number) => {
const sbtProc = sbtPromise.childProcess
sbtProc.on('close', (code: number) => {
if (code != 0) {
let msg = "Fetching the language server failed."
const msg = "Configuring the IDE failed."
outputChannel.append(msg)
throw new Error(msg)
}

run({
command: "java",
args: ["-classpath", classPath, "dotty.tools.languageserver.Main", "-stdio"]
})
})
return coursierPromise

return sbtPromise
})
}

Expand Down