Skip to content
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

Implement textEdit/codeAction #154

Merged
merged 5 commits into from
Dec 29, 2017
Merged

Conversation

olafurpg
Copy link
Member

This PR implements a codeAction to remove unused imports with scalafix. The patch produced by scalafix is converted to precise TextEdit per changed token, instead of sending a single TextEdit to replace the whole file as we do with scalafmt.

2017-12-22 22 33 45

codeActions appear as lightbulbs above the cursor and when the lightbulb is clicked a dialogue opens with a list of commands to run (not shown in gif).

All of ScalafixPatchEnrichments is copy pasted from the scalafix sources, with the next release of scalafix this file will no longer be necessary since in master we have an API to get List[TokenPatch] from a Patch

@olafurpg
Copy link
Member Author

First time scalafix fails the build 😄

--- /home/travis/build/scalameta/language-server/languageserver/src/main/scala/langserver/messages/Commands.scala
+++ <expected fix from ExplicitResultTypes+RemoveUnusedImports+Disable>
@@ -5,6 +5,7 @@
 import langserver.types._
 import langserver.utils.JsonRpcUtils
 import play.api.libs.json.OFormat
+import langserver.messages.ApplyWorkspaceEditParams
 
 sealed trait Message
 sealed trait ServerCommand extends Message
@@ -229,7 +230,7 @@
 case class WorkspaceSymbolRequest(params: WorkspaceSymbolParams) extends ServerCommand
 case class ApplyWorkspaceEditParams(label: Option[String], edit: WorkspaceEdit)
 object ApplyWorkspaceEditParams {
-  implicit val format = Json.format[ApplyWorkspaceEditParams]
+  implicit val format: OFormat[ApplyWorkspaceEditParams] = Json.format[ApplyWorkspaceEditParams]
 }

CodeActionResult(Nil)
val removeUnusedImports = request.params.context.diagnostics.collectFirst {
case Diagnostic(_, _, _, Some("scalac"), "Unused import") =>
// This command doesn't seem to do much
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment confuses me.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a leftover comment before I made it remove unused imports, comment removed

@olafurpg olafurpg force-pushed the codeAction branch 2 times, most recently from f6653a5 to 795e27f Compare December 23, 2017 15:18
case Some(document) =>
removeUnused(
uri,
schema.Database(document :: Nil).toDb(None).documents.head
Copy link
Collaborator

Choose a reason for hiding this comment

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

This reads to me like it is creating a single document database just to convert it to a different database format to then extract the single document provided initially? Is this correct? If so a method to name the purpose would be helpful, or just an extension method, toOtherDocumentType.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, while this is not new code introduced here schema.Database(...).toDb... is also confusing. It reads as “Create a db then convert it to a db”.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've moved this to an extension method.

The API around scala.meta.Database / schema.Database conversions is quite awkward, we used to have fine grained conversions per type Document/Message/Synthetic but now it's all in a big fat method only to convert scala.meta.Database. language-server has been a great test on a lot of the scalameta APIs, I would love to incorporate some of our workarounds here into the core API

// Copy-pasta from scalafix because all of these methods are private.
// We should expose a package private API to get a list of token patches from
// a Patch.
// TODO(olafur): Figure out how to expose a minimal public API in scalafix.Patch
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can actually remove this with the next scalafix release, Guillaume already refactored this API to support --diff and // scalafix:off for rewrites

Copy link
Collaborator

@ShaneDelmore ShaneDelmore left a comment

Choose a reason for hiding this comment

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

I had a couple of questions but overall it looks really good.

In case an exception happens on server startup then the server exited
silently.
Users can put the cursor or hover over an unused import to see a
lightbulb appear. If the user clicks on the lightbulb, a dialogue
appears with options to run commands like "Remove unused import".

This commit turned out to be quite tricky since I wasn't sure how to
submit a request from the server to the client for workspace/applyEdit.
It turned out to be quite dead simple, just send a JsonRpcRequestMessage
and discard the response.
@olafurpg
Copy link
Member Author

Thank you for the review @ShaneDelmore !

Copy link
Member

@laughedelic laughedelic left a comment

Choose a reason for hiding this comment

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

Cool 💯 I tried it out and it works! I left just a couple of questions.

2017-12-26 03 02 17

One unexpected thing for me was that it removes all unused imports while I'd expect a code action bound to a diagnostic act only on the underlined region. Is it by design or is it just the way Scalafix does it by default?

@@ -58,6 +59,13 @@ abstract class Connection(inStream: InputStream, outStream: OutputStream)(implic
}
}

var i = new AtomicInteger()
Copy link
Member

Choose a reason for hiding this comment

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

Should it be private?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! And it can also be a val

@@ -14,6 +16,10 @@ class CompilerSuite extends MegaSuite {
Nil
)

def toDocument(name: String, code: String): Document = {
InteractiveSemanticdb.toDocument(compiler, code, name, 10000)
Copy link
Member

Choose a reason for hiding this comment

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

What is this number?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I have some WIP experiments to use monix Task to avoid ad-hoc timeouts and support proper cancellation for presentation compiler requests

@@ -10,6 +10,7 @@ case object WorkspaceCommand extends Enum[WorkspaceCommand] {

case object ClearIndexCache extends WorkspaceCommand
case object ResetPresentationCompiler extends WorkspaceCommand
case object ScalafixUnusedImports extends WorkspaceCommand
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible (in the future) to adapt Scalafix rewrites to code actions so that it won't be necessary to define a WorkspaceCommand for each type of rewrite?

Copy link
Member Author

Choose a reason for hiding this comment

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

Scalafix absolutely, scalafix can run custom rules from configuration in .scalafix.conf, we can probably embed that config as command arguments to get away with only a single Scalafix WorkspaceCommand.

Copy link
Member Author

@olafurpg olafurpg left a 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!

case Some(document) =>
removeUnused(
uri,
schema.Database(document :: Nil).toDb(None).documents.head
Copy link
Member Author

Choose a reason for hiding this comment

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

I've moved this to an extension method.

The API around scala.meta.Database / schema.Database conversions is quite awkward, we used to have fine grained conversions per type Document/Message/Synthetic but now it's all in a big fat method only to convert scala.meta.Database. language-server has been a great test on a lot of the scalameta APIs, I would love to incorporate some of our workarounds here into the core API

// Copy-pasta from scalafix because all of these methods are private.
// We should expose a package private API to get a list of token patches from
// a Patch.
// TODO(olafur): Figure out how to expose a minimal public API in scalafix.Patch
Copy link
Member Author

Choose a reason for hiding this comment

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

I think we can actually remove this with the next scalafix release, Guillaume already refactored this API to support --diff and // scalafix:off for rewrites

@@ -58,6 +59,13 @@ abstract class Connection(inStream: InputStream, outStream: OutputStream)(implic
}
}

var i = new AtomicInteger()
Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! And it can also be a val

@@ -10,6 +10,7 @@ case object WorkspaceCommand extends Enum[WorkspaceCommand] {

case object ClearIndexCache extends WorkspaceCommand
case object ResetPresentationCompiler extends WorkspaceCommand
case object ScalafixUnusedImports extends WorkspaceCommand
Copy link
Member Author

Choose a reason for hiding this comment

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

Scalafix absolutely, scalafix can run custom rules from configuration in .scalafix.conf, we can probably embed that config as command arguments to get away with only a single Scalafix WorkspaceCommand.

@@ -14,6 +16,10 @@ class CompilerSuite extends MegaSuite {
Nil
)

def toDocument(name: String, code: String): Document = {
InteractiveSemanticdb.toDocument(compiler, code, name, 10000)
Copy link
Member Author

Choose a reason for hiding this comment

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

I have some WIP experiments to use monix Task to avoid ad-hoc timeouts and support proper cancellation for presentation compiler requests

@olafurpg
Copy link
Member Author

@laughedelic PS, that gif from atom looks amazing 😍

@olafurpg
Copy link
Member Author

One unexpected thing for me was that it removes all unused imports while I'd expect a code action bound to a diagnostic act only on the underlined region. Is it by design or is it just the way Scalafix does it by default?

This is intentional, we could make scalafix remove only a single unused import but I'm not sure how useful that would be. In IntelliJ there is only a single "Organize imports" which both removes unused imports and sorts/groups them. To remove a single imports it's easier to just remove it manually, IMO

@olafurpg olafurpg merged commit cdbf2f2 into scalameta:master Dec 29, 2017
@olafurpg olafurpg deleted the codeAction branch December 29, 2017 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants