Skip to content

Commit

Permalink
Introduce AutoImportsProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
gabro committed Dec 6, 2019
1 parent 087f9a1 commit 14da943
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 20 deletions.
18 changes: 18 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala
Expand Up @@ -25,6 +25,8 @@ import scala.meta.pc.PresentationCompiler
import scala.meta.pc.SymbolSearch
import scala.concurrent.Future
import java.{util => ju}
import org.eclipse.lsp4j.CodeActionParams
import scala.meta.pc.AutoImportsResult

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

def autoImports(
params: CodeActionParams,
name: String,
token: CancelToken
): Future[ju.List[AutoImportsResult]] = {
val textDocumentPositionParams = new TextDocumentPositionParams(
params.getTextDocument(),
params.getRange().getEnd()
)
withPC(textDocumentPositionParams, 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 +178,7 @@ class Compilers(
}.getOrElse {
Future.successful(Option.empty)
}

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

def signatureHelp(
params: TextDocumentPositionParams,
token: CancelToken,
Expand Down
Expand Up @@ -29,34 +29,25 @@ object QuickFix {
diagnostic: l.Diagnostic,
name: String
): Future[Seq[CodeAction]] = {

// TODO(gabro): this is hack. Instead of computing the auto-imports for a name at a range,
// we run completions starting from the end of the range, and filter the completions that
// match exactly the name we're looking for
val completionParams = new l.CompletionParams(
params.getTextDocument(),
params.getRange().getEnd()
)
compilers.completions(completionParams, token).map { completions =>
scribe.info(completions.getItems().toString())
scribe.info(name)
completions.getItems().asScala.collect {
case completionItem if completionItem.getFilterText() == name =>
val pkg = completionItem.getDetail().trim()
val edit = new l.WorkspaceEdit()
compilers
.autoImports(params, name, token)
.map { imports =>
imports.asScala.map { i =>
val uri = params.getTextDocument().getUri()
val changes = Map(uri -> completionItem.getAdditionalTextEdits())
val edit = new l.WorkspaceEdit(Map(uri -> i.edits).asJava)

val codeAction = new l.CodeAction()
codeAction.setTitle(s"Import '$name' from package '$pkg'")

codeAction.setTitle(
s"Import '$name' from package '${i.packageName}'"
)
codeAction.setKind(l.CodeActionKind.QuickFix)
codeAction.setDiagnostics(List(diagnostic).asJava)

edit.setChanges(changes.asJava)
codeAction.setEdit(edit)

codeAction
}
}
}
}

Future
Expand Down
@@ -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();
}
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);

/**
* Returns the Protobuf byte array representation of a SemanticDB <code>TextDocument</code> for the given source.
*/
Expand Down
148 changes: 148 additions & 0 deletions mtags/src/main/scala/scala/meta/internal/pc/AutoImportsProvider.scala
@@ -0,0 +1,148 @@
package scala.meta.internal.pc

import scala.meta.pc.OffsetParams
import org.eclipse.{lsp4j => l}
import scala.meta.pc.SymbolSearchVisitor
import java.nio.file.Path
import scala.meta.pc.AutoImportsResult
import scala.collection.mutable
import scala.collection.JavaConverters._
import scala.util.control.NonFatal

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)
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)

symbols.result.collect {
case sym if sym.name.dropLocal.decoded == name =>
val ident = Identifier.backtickWrap(sym.name.dropLocal.decoded)
val pkg = sym.owner.fullName
val edits = importPosition match {
case None => Nil
case Some(value) =>
val (short, edits) = ShortenedNames.synthesize(
TypeRef(ThisType(sym.owner), sym, Nil),
pos,
context,
value
)
edits
}
AutoImportsResultImpl(pkg, edits.asJava)
}
}

private class CompilerSearchVisitor(
query: String,
context: Context,
visitSymbol: Symbol => Boolean
) extends SymbolSearchVisitor {

def visit(top: SymbolSearchCandidate): Int = {
var added = 0
for {
sym <- loadSymbolFromClassfile(top)
if context.lookupSymbol(sym.name, _ => true).symbol != sym
} {
if (visitSymbol(sym)) {
added += 1
}
}
added
}

def visitClassfile(pkg: String, filename: String): Int = {
visit(SymbolSearchCandidate.Classfile(pkg, filename))
}

def visitWorkspaceSymbol(
path: Path,
symbol: String,
kind: l.SymbolKind,
range: l.Range
): Int = {
visit(SymbolSearchCandidate.Workspace(symbol))
}

def shouldVisitPackage(pkg: String): Boolean =
packageSymbolFromString(pkg).isDefined

override def isCancelled: Boolean =
false

}

// FIXME(gabro): this is copy-pasted from CompletionProvider, we should de-dupe it
private def loadSymbolFromClassfile(
classfile: SymbolSearchCandidate
): List[Symbol] = {
def isAccessible(sym: Symbol): Boolean = {
sym != NoSymbol && {
sym.info // needed to fill complete symbol
sym.isPublic
}
}
try {
classfile match {
case SymbolSearchCandidate.Classfile(pkgString, filename) =>
val pkg = packageSymbolFromString(pkgString).getOrElse(
throw new NoSuchElementException(pkgString)
)
val names = filename
.stripSuffix(".class")
.split('$')
.iterator
.filterNot(_.isEmpty)
.toList
val members = names.foldLeft(List[Symbol](pkg)) {
case (accum, name) =>
accum.flatMap { sym =>
if (!isAccessible(sym) || !sym.isModuleOrModuleClass) Nil
else {
sym.info.member(TermName(name)) ::
sym.info.member(TypeName(name)) ::
Nil
}
}
}
members.filter(sym => isAccessible(sym))
case SymbolSearchCandidate.Workspace(symbol) =>
val gsym = inverseSemanticdbSymbol(symbol)
if (isAccessible(gsym)) gsym :: Nil
else Nil
}
} catch {
case NonFatal(_) => Nil
}
}

}
@@ -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
Expand Up @@ -26,6 +26,8 @@ import scala.meta.pc.PresentationCompilerConfig
import java.util.concurrent.CompletableFuture
import scala.meta.pc.DefinitionResult
import scala.collection.Seq
import java.{util => ju}
import scala.meta.pc.AutoImportsResult

case class ScalaPresentationCompiler(
buildTargetIdentifier: String = "",
Expand Down Expand Up @@ -81,13 +83,25 @@ case class ScalaPresentationCompiler(
items.setIsIncomplete(true)
items
}

override def complete(
params: OffsetParams
): CompletableFuture[CompletionList] =
access.withInterruptableCompiler(emptyCompletion, params.token) { global =>
new CompletionProvider(global, params).completions()
}

override def autoImports(
name: String,
params: OffsetParams
): CompletableFuture[ju.List[AutoImportsResult]] =
access.withInterruptableCompiler(
List.empty[AutoImportsResult].asJava,
params.token
) { global =>
new AutoImportsProvider(global, name, params).autoImports().asJava
}

// NOTE(olafur): hover and signature help use a "shared" compiler instance because
// we don't typecheck any sources, we only poke into the symbol table.
// If we used a shared compiler then we risk hitting `Thread.interrupt`,
Expand Down

0 comments on commit 14da943

Please sign in to comment.