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 code action for importing missing symbols #1065

Merged
merged 13 commits into from
Dec 11, 2019
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package scala.meta.internal.metals

import org.eclipse.{lsp4j => l}
import scala.meta.pc.CancelToken
import scala.concurrent.ExecutionContext
import scala.concurrent.Future

final class CodeActionProvider(
compilers: Compilers
) {

def codeActions(
params: l.CodeActionParams,
token: CancelToken
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = {

for {
quickFixes <- Future
.sequence(
List(QuickFix.ImportMissingSymbol)
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
.map(_.contribute(params, compilers, token))
)
.map(_.flatten)
} yield quickFixes

}

}
13 changes: 13 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import scala.meta.pc.PresentationCompiler
import scala.meta.pc.SymbolSearch
import scala.concurrent.Future
import java.{util => ju}
import scala.meta.pc.AutoImportsResult

/**
* Manages lifecycle for presentation compilers in all build targets.
Expand Down Expand Up @@ -150,6 +151,16 @@ class Compilers(
pc.complete(CompilerOffsetParams.fromPos(pos, token)).asScala
}.getOrElse(Future.successful(new CompletionList()))

def autoImports(
params: TextDocumentPositionParams,
name: String,
token: CancelToken
): Future[ju.List[AutoImportsResult]] = {
withPC(params, None) { (pc, pos) =>
pc.autoImports(name, CompilerOffsetParams.fromPos(pos, token)).asScala
}.getOrElse(Future.successful(new ju.ArrayList))
}

def hover(
params: TextDocumentPositionParams,
token: CancelToken,
Expand All @@ -162,6 +173,7 @@ class Compilers(
}.getOrElse {
Future.successful(Option.empty)
}

def definition(
params: TextDocumentPositionParams,
token: CancelToken
Expand All @@ -178,6 +190,7 @@ class Compilers(
)
}
}.getOrElse(Future.successful(DefinitionResult.empty))

def signatureHelp(
params: TextDocumentPositionParams,
token: CancelToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class MetalsLanguageServer(
private var bloopServers: BloopServers = _
private var bspServers: BspServers = _
private var codeLensProvider: CodeLensProvider = _
private var codeActionProvider: CodeActionProvider = _
private var definitionProvider: DefinitionProvider = _
private var semanticDBIndexer: SemanticdbIndexer = _
private var implementationProvider: ImplementationProvider = _
Expand Down Expand Up @@ -404,6 +405,7 @@ class MetalsLanguageServer(
Option(params)
)
)
codeActionProvider = new CodeActionProvider(compilers)
doctor = new Doctor(
workspace,
buildTargets,
Expand Down Expand Up @@ -495,6 +497,7 @@ class MetalsLanguageServer(
capabilities.setWorkspaceSymbolProvider(true)
capabilities.setDocumentSymbolProvider(true)
capabilities.setDocumentFormattingProvider(true)
capabilities.setCodeActionProvider(true)
capabilities.setTextDocumentSync(TextDocumentSyncKind.Full)
if (config.isNoInitialized) {
sh.schedule(
Expand Down Expand Up @@ -1089,9 +1092,8 @@ class MetalsLanguageServer(
def codeAction(
params: CodeActionParams
): CompletableFuture[util.List[CodeAction]] =
CancelTokens { _ =>
scribe.warn("textDocument/codeAction is not supported.")
null
CancelTokens.future { token =>
codeActionProvider.codeActions(params, token).map(_.asJava)
}

@JsonRequest("textDocument/codeLens")
Expand Down
67 changes: 67 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/QuickFix.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package scala.meta.internal.metals

import scala.concurrent.Future
import scala.meta.pc.CancelToken
import org.eclipse.{lsp4j => l}
import scala.concurrent.ExecutionContext
import scala.meta.internal.metals.MetalsEnrichments._

trait QuickFix {
def contribute(
params: l.CodeActionParams,
compilers: Compilers,
token: CancelToken
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]]
}

object QuickFix {

object ImportMissingSymbol extends QuickFix {
def label(name: String, packageName: String): String =
s"Import '$name' from package '$packageName'"

override def contribute(
params: l.CodeActionParams,
compilers: Compilers,
token: CancelToken
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = {

def importMissingSymbol(
diagnostic: l.Diagnostic,
name: String
): Future[Seq[l.CodeAction]] = {
val textDocumentPositionParams = new l.TextDocumentPositionParams(
params.getTextDocument(),
diagnostic.getRange.getEnd()
)
compilers
.autoImports(textDocumentPositionParams, name, token)
.map { imports =>
imports.asScala.map { i =>
val uri = params.getTextDocument().getUri()
val edit = new l.WorkspaceEdit(Map(uri -> i.edits).asJava)

val codeAction = new l.CodeAction()

codeAction.setTitle(label(name, i.packageName))
codeAction.setKind(l.CodeActionKind.QuickFix)
codeAction.setDiagnostics(List(diagnostic).asJava)
codeAction.setEdit(edit)

codeAction
}
}
}

Future
.sequence(params.getContext().getDiagnostics().asScala.collect {
case d @ ScalacDiagnostic.SymbolNotFound(name)
if d.getRange().encloses(params.getRange().getEnd()) =>
importMissingSymbol(d, name)
})
.map(_.flatten)

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package scala.meta.internal.metals

import org.eclipse.{lsp4j => l}

object ScalacDiagnostic {
object SymbolNotFound {
private val regex = """not found: (value|type) (\w+)""".r
def unapply(d: l.Diagnostic): Option[String] = d.getMessage() match {
case regex(_, name) => Some(name)
case _ => None
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package scala.meta.pc;

import java.util.List;
import org.eclipse.lsp4j.TextEdit;

public interface AutoImportsResult {
public String packageName();
public List<TextEdit> edits();
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public abstract class PresentationCompiler {
*/
public abstract CompletableFuture<DefinitionResult> definition(OffsetParams params);

/**
* Return the necessary imports for a symbol at the given position.
*/
public abstract CompletableFuture<List<AutoImportsResult>> autoImports(String name, OffsetParams params);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could do something more general:

public abstract CompletableFuture<List<CodeActionResult>> codeAction(String input, ActionType actionType, OffsetParams params);

enum ActionType{
  Import, Implement, TypeAnnotation
}

otherwise we will be adding a lot of different methods for each action.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point: I'm torn about this. From one end, I share your concern, from another I'm not sure of what code actions will be implemented so I'm wary of generalizing the API without knowing the use cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, let's leave it then and see if generalizing it will be useful in the future.


/**
* Returns the Protobuf byte array representation of a SemanticDB <code>TextDocument</code> for the given source.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package scala.meta.internal.pc

import scala.meta.pc.OffsetParams
import scala.meta.pc.AutoImportsResult
import scala.collection.mutable
import scala.collection.JavaConverters._
import org.eclipse.lsp4j.TextEdit

final class AutoImportsProvider(
val compiler: MetalsGlobal,
name: String,
params: OffsetParams
) {
import compiler._

def autoImports(): List[AutoImportsResult] = {
val unit = addCompilationUnit(
code = params.text(),
filename = params.filename(),
cursor = Some(params.offset())
)
val pos = unit.position(params.offset)
// make sure the compilation unit is loaded
typedTreeAt(pos)
gabro marked this conversation as resolved.
Show resolved Hide resolved
val importPosition = autoImportPosition(pos, params.text())
val context = doLocateImportContext(pos, importPosition)
val isSeen = mutable.Set.empty[String]
val symbols = List.newBuilder[Symbol]

def visit(sym: Symbol): Boolean = {
val id = sym.fullName
if (!isSeen(id)) {
isSeen += id
symbols += sym
true
}
false
}

val visitor = new CompilerSearchVisitor(name, context, visit)

search.search(name, buildTargetIdentifier, visitor)

def isExactMatch(sym: Symbol, name: String): Boolean =
sym.name.dropLocal.decoded == name
gabro marked this conversation as resolved.
Show resolved Hide resolved

symbols.result.collect {
case sym if isExactMatch(sym, name) =>
val ident = Identifier.backtickWrap(sym.name.dropLocal.decoded)
val pkg = sym.owner.fullName
val edits = importPosition match {
case None =>
// No import position means we can't insert an import without clashing with
// existing symbols in scope, so we just do nothing
Nil
case Some(value) =>
val (short, edits) = ShortenedNames.synthesize(
TypeRef(ThisType(sym.owner), sym, Nil),
pos,
context,
value
)
val namePos =
pos
.withStart(pos.start - name.length())
.withEnd(pos.end)
.toLSP
val nameEdit = new TextEdit(namePos, short)
nameEdit :: edits
}
AutoImportsResultImpl(pkg, edits.asJava)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package scala.meta.internal.pc

import scala.meta.pc.AutoImportsResult
import java.{util => ju}
import org.eclipse.lsp4j.TextEdit

case class AutoImportsResultImpl(packageName: String, edits: ju.List[TextEdit])
extends AutoImportsResult
Loading