-
Notifications
You must be signed in to change notification settings - Fork 1.1k
IDE support for Dotty via the Language Server Protocol, including a Visual Studio Code extension #2532
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
Conversation
nice! btw, we're going to add language server to ensime. This will serve as an excellent reference implementation for ensime 3.0. Hmm, I guess scastie might need to use this for dotty support. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that cancelToken
is not used anywhere, I assume we'd need to implement safe points in the compiler to use that.
Let's remember to open an issue for that once this is merged.
Otherwise LGTM, happy to have this in!
classRoot.symbol.asClass.unpickler = unpickler | ||
moduleRoot.symbol.asClass.unpickler = unpickler | ||
case _ => | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What was the reason behind making this non-RT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by RT here? Referentially transparent? This method is never called multiple times on the same denotation.
*/ | ||
@tailrec def sourceSymbol(sym: Symbol)(implicit ctx: Context): Symbol = | ||
if (!sym.exists) | ||
sym |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> NoSymbol
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's completely equivalent, do you want me to change this for style reasons?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll leave that up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think either works fine.
|
||
private var myCtx: Context = myInitCtx | ||
|
||
def currentCtx: Context = myCtx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, very much so, see usage in DottyLanguageServer
// Like in `ZipArchiveFileLookup` we assume that zips are immutable | ||
private val zipClassPathClasses: Seq[String] = zipClassPaths.flatMap { zipCp => | ||
// (new ZipFile(zipCp.asInstanceOf[ZipArchiveFileLookup[_]].zipFile).stream.collect(Collectors.toList()).asScala: List[ZipEntry]) | ||
// Working with Java 8 stream without SAMs and scala-java8-compat is awful. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
private val myOpenedTrees = new mutable.LinkedHashMap[URI, List[SourceTree]] | ||
|
||
def openedFiles: Map[URI, SourceFile] = myOpenedFiles | ||
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see usage in DottyLanguageServer
} | ||
|
||
override def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = | ||
/*thisServer.synchronized*/ {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a docstring here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A docstring for what? If you're curious what didChangeConfiguration
is for, see the spec: https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#didchangeconfiguration-notification
The commented synchronized
is so that if we ever implement this method, we don't forget to make it synchronized
/*thisServer.synchronized*/ {} | ||
|
||
override def didChangeWatchedFiles(params: DidChangeWatchedFilesParams): Unit = | ||
/*thisServer.synchronized*/ {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
/*thisServer.synchronized*/ {} | ||
|
||
override def didSave(params: DidSaveTextDocumentParams): Unit = | ||
/*thisServer.synchronized*/ {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
/** Run the Dotty Language Server. | ||
* | ||
* This is designed to be started from an editor supporting the Language Server | ||
* Protocol, the easiest way to fetch and run this is to use `coursier`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alignment
* This is designed to be started from an editor supporting the Language Server | ||
* Protocol, the easiest way to fetch and run this is to use `coursier`: | ||
* | ||
* coursier launch $artifact -M dotty.tools.languageserver.Main -- -stdio |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
```shell
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it, and it doesn't do any highlighting on this line, so no point in using this syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review!
classRoot.symbol.asClass.unpickler = unpickler | ||
moduleRoot.symbol.asClass.unpickler = unpickler | ||
case _ => | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by RT here? Referentially transparent? This method is never called multiple times on the same denotation.
*/ | ||
@tailrec def sourceSymbol(sym: Symbol)(implicit ctx: Context): Symbol = | ||
if (!sym.exists) | ||
sym |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's completely equivalent, do you want me to change this for style reasons?
|
||
private var myCtx: Context = myInitCtx | ||
|
||
def currentCtx: Context = myCtx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, very much so, see usage in DottyLanguageServer
private val myOpenedTrees = new mutable.LinkedHashMap[URI, List[SourceTree]] | ||
|
||
def openedFiles: Map[URI, SourceFile] = myOpenedFiles | ||
def openedTrees: Map[URI, List[SourceTree]] = myOpenedTrees |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see usage in DottyLanguageServer
|
||
## Current limitations, to be fixed: | ||
- Projects should be compiled with sbt before starting the IDE, this is | ||
automatically done for you if you run `sbt launchIDE`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't start if the project doesn't compile.
|
||
private[this] var myDrivers: mutable.Map[ProjectConfig, InteractiveDriver] = _ | ||
|
||
def drivers: mutable.Map[ProjectConfig, InteractiveDriver] = thisServer.synchronized { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, it shouldn't be.
|
||
def drivers: mutable.Map[ProjectConfig, InteractiveDriver] = thisServer.synchronized { | ||
if (myDrivers == null) { | ||
assert(rootUri != null, "`drivers` cannot be called before `initialize`") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, initialize
is the first message sent by the client to the server(https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#initialize), if this message wasn't sent, the serve will not do anything, so this method won't be called.
val driver = driverFor(uri) | ||
|
||
val change = params.getContentChanges.get(0) | ||
assert(change.getRange == null, "TextDocumentSyncKind.Incremental support is not implemented") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should never trigger because in initialize we set c.setTextDocumentSync(TextDocumentSyncKind.Full)
, meaning we don't support TextDocumentSyncKind.Incremental
. didChange
is a notification, not a request, so the client does not expect a response. Note that this assertion will be caught by LSP4J and so won't bring down the entire language server.
} | ||
|
||
override def didChangeConfiguration(params: DidChangeConfigurationParams): Unit = | ||
/*thisServer.synchronized*/ {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A docstring for what? If you're curious what didChangeConfiguration
is for, see the spec: https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#didchangeconfiguration-notification
The commented synchronized
is so that if we ever implement this method, we don't forget to make it synchronized
* This is designed to be started from an editor supporting the Language Server | ||
* Protocol, the easiest way to fetch and run this is to use `coursier`: | ||
* | ||
* coursier launch $artifact -M dotty.tools.languageserver.Main -- -stdio |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it, and it doesn't do any highlighting on this line, so no point in using this syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except for minor comments, LGTM
})) | ||
|
||
if (currentHasPos) { | ||
p match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we factor this out into a method forceIfLazy
in Positioned
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, but I think it would be even better if we used something higher-level than dealing with productIterator by hand, that would take care of lazy trees, I'll leave a FIXME in the code.
val currentHasPos = | ||
p.pos.contains(pos) && (!p.pos.isZeroExtent || | ||
(path match { | ||
case p1 :: _ => p1.pos.isZeroExtent || !p1.pos.contains(pos) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to have an explanation of this logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I forgot to clean this up, and trying to document it I realized it's broken: I was trying to get the next node in the tree that the path could go through, but this is actually pretty hard to do. I've given up on that and instead added a skipZeroExtent
parameter in NavigateAST#pathTo
(that I use in Interactive#pathTo
): b3befee
@@ -552,6 +553,33 @@ object Symbols { | |||
|
|||
type ThisName = TypeName | |||
|
|||
/** If this is a top-level class, and if `-Ykeep-trees` is set, return the tree |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be -Yretain-trees.
@@ -552,6 +553,33 @@ object Symbols { | |||
|
|||
type ThisName = TypeName | |||
|
|||
/** If this is a top-level class, and if `-Ykeep-trees` is set, return the tree | |||
* for this class, otherwise null. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use EmptyTree
as a sentinel instead? I am not keen on having null
as a semantically significant value except when used locally meaning "this cache has not been set".
*/ | ||
def tree(implicit ctx: Context): tpd.TypeDef = { | ||
if (unpickler != null && !denot.isAbsent) { | ||
assert(myTree == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's scope to unify the way we do this here with how we store lazy trees in inline functions. Not necessary to do this now, but it would be good to leave a comment telling us to come back to it later.
else sym.companionModule.info.member(name).symbol | ||
else { | ||
var module = sym.companionModule | ||
if (module == NoSymbol && sym.isAbsent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!module.exists
is more idiomatic
*/ | ||
@tailrec def sourceSymbol(sym: Symbol)(implicit ctx: Context): Symbol = | ||
if (!sym.exists) | ||
sym |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think either works fine.
You need |
$ brew cask install visual-studio-code Is the quick way on macOS |
@smarter Thanks for the tip. I could launch it now. Looks pretty! I tried it on dotty itself, but so far did not get much to work beyond syntax highlighting. Let's check on Monday together? |
@odersky Sure, did you get any message in the console you launched it from? |
@smarter In the console everything looked normal. Here's what I got: /Users/odersky/workspace/dotty> sbt launchIDE
|
@odersky: Thanks to @liufengyun I fixed a few issues that could have prevented the IDE from running and updated the branch. But the sbt output you're getting makes me wonder if you're on the correct branch, can you check that you're on branch |
FIXME: This is too easy to forget, some sort of Traverser should be used instead of using productIterator by hand.
When typechecking a class from source or when unpickling it from tasty, we normally do not keep a reference to the class tree, but there's a wealth of information only present in the tree that we may wish to use. In an IDE for example, this makes it easy to provide features like "jump to definition". This commit unlocks this potential by adding a `ClassSymbol#tree` method that, when used together with the `-Yretain-trees` flag, returns the tree corresponding to a top-level class. This also replaces the `unpicklers` map in `CompilationUnit` by `ClassSymbol#unpickler`.
When we unpickle a Scala2 module we might not have a companion class to unpickle, when this happen we call markAbsent() on the denotation of the companion class, which means that calls to companionModule will not work. Fixed by falling back to the more robust scalacLinkedClass.
If a tree has no position or a zero-extent position, it should be synthetic. We can preserve this invariant by always setting a zero-extent position for these trees here.
Synthetic trees should get zero-extent positions so that they don't get returned by Interactive#pathTo. This makes it easier to show information related to non-synthetic trees in IDEs.
Previously, the position of class parents was the position of the whole class, which means that NavigateAST#pathTo always returned the class parents when looking up a position in the body of the class. Fixed by using the position of the name of the class instead.
The position of a tree should contain the position of all its children, but that was not the case before this commit for Bind nodes created from Typed nodes.
Before this commit, we only created .tasty files under -YemitTasty, we haven't decided yet if this should be the default, but even if we don't end up doing that, it is still useful to emit empty .tasty files to signal that the corresponding .class file has a tasty section, this is much simpler to check than parsing classfiles to look for a tasty section. Also, fix -YemitTasty tof work with AbstractFile who are not backed by java.io.File, previously this lead to NullPointerException.
As specified in https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.16.1, an annotation argument of type boolean, byte, char or short will be represented as a CONSTANT_Integer in the classfile, so when we parse it we get an Int, but before this commit we did not convert this Int to the correct type for the annotation argument, thus creating ill-typed trees.
This way they won't show up in the IDE
This commit adds a set of APIs in dotty.tools.dotc.interactive._ designed for interactive uses of the compiler like in IDEs. This API is just a first draft driven by the requirements of the Dotty Language Server and will evolve based on usage. Usage is roughly as follow: - Create an instance of InteractiveDriver with the compiler flags you need - Call InteractiveDriver#run(uri, code) to typecheck `code` and keep a reference to it via the identifier `uri`. The return value of this method are the errors/warnings/infos generated during the compilation - Use InteractiveDriver#openedTrees(uri) to get all top-level class typechecked trees generated for `uri`, or InteractiveDriver#allTrees to get all top-level trees opened in this driver instance and available on the classpath (unpickled from the TASTY section of classfiles) - Query the trees using one of the many methods in the Interactive object. See the code in language-server/ added in the following commit for a concrete example.
This is an implementation of the Language Server Protocol for Dotty, which allows us to provide rich IDE features in every editor supporting this protocol, a Visual Studio Code extension demonstrating this is part of the next commit. For more information on the LSP, see https://github.com/Microsoft/language-server-protocol Fully supported features: - Typechecking as you type to show compiler errors/warnings - Type information on hover - Go to definition (in the current project) - Find all references Partially working features: - Completion - Renaming - Go to definition in external projects Unimplemented features: - Documentation on hover - Formatting code (requires integrating with scalafmt) - Quick fixes (probably by integrating with scalafix) Current limitations, to be fixed: - Projects should be compiled with sbt before starting the IDE, this is automatically done for you if you run `sbt launchIDE`. - Once the IDE is started, source files that are not opened in the IDE should not be modified in some other editor, the IDE won't pick up these changes. - Not all compiler errors/warnings are displayed, just those occuring during typechecking.
This extension uses the Dotty Language Server to provide IDE features in Visual Studio Code.
This means the dotty build can use the latest unreleased version of the sbt-dotty plugin instead of depending on a published version, in the next commit we'll add IDE support to the sbt-dotty plugin and immediately be able to use them on the dotty build itself.
This adds commands to the sbt-dotty plugin to generate the .dotty-ide.json config file used by the Dotty Language Server and to start Visual Studio Code with everything set up correctly. For end-users, using the IDE is as simple as: 1. Installing vscode (https://code.visualstudio.com) 2. In your project, run `sbt launchIDE`
I encounter a NPE when firing up Seems https://github.com/lampepfl/dotty/blob/352c09e481cb4893812c5f49959e587842448fe0/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala#L83 |
@HerringtonDarkholme could you post the content of the generated .dotty-ide.json file in the project, as well as the output of the |
The exact stacktrace you got would be helpful too |
I'm on Mac OSX. .dotty-ide.json
CallStack:
|
@HerringtonDarkholme My guess is that you have some scala files directly in the directory |
This is my
|
I have identified one problem. A And alas, However, if user has already opened |
@HerringtonDarkholme Thanks for the diagnostic! Can you try forcing code to run in a new instance using |
@smarter |
Yes, fresh launching makes everything work. ( forget to report back, sorry |
@wxdong5211 @HerringtonDarkholme Your issues should now be fixed if you use the latest nightly build of dotty ( |
It works fine for me! Thank @smarter ! |
windoge ok too , @smarter 👍 |
This PR introduces IDE support to Dotty, this requires several components (see each individual commit messages for more detailed explanations):
dotty.tools.dotc.interactive.*
. These APIs were developed for the Dotty Language Server but could evolve to support other usages like the REPL or other IDEs.Quick start
Status
Fully supported features:
Partially working features:
Unimplemented features:
Current limitations, to be fixed:
automatically done for you if you run
sbt launchIDE
.should not be modified in some other editor, the IDE won't pick up
these changes.
during typechecking.
Future
There is much more that could be done, in particular see https://github.com/lampepfl/dotty/issues/1523 for a lot of potential improvements that we should keep in mind.