Skip to content

Commit

Permalink
Merge pull request #578 from olafurpg/workspace-import
Browse files Browse the repository at this point in the history
Insert local import when completing workspace symbol.
  • Loading branch information
olafurpg committed Mar 20, 2019
2 parents a062a98 + 8991ee3 commit c9b4d83
Show file tree
Hide file tree
Showing 21 changed files with 626 additions and 179 deletions.
Expand Up @@ -1025,6 +1025,7 @@ class MetalsLanguageServer(
build: BuildServerConnection
): Future[BuildChange] = {
cancelables.add(build)
compilers.cancel()
buildServer = Some(build)
val importedBuild = timed("imported build") {
for {
Expand Down
Expand Up @@ -42,7 +42,9 @@ class ClasspathSearch(
classfiles.add(classfile)
}
var nonExactMatches = 0
var searchResult = SymbolSearch.Result.COMPLETE
var searchResult =
if (query.isExact) SymbolSearch.Result.INCOMPLETE
else SymbolSearch.Result.COMPLETE
for {
hit <- classfiles.pollingIterator
if {
Expand Down
Expand Up @@ -19,6 +19,7 @@ case class WorkspaceSymbolQuery(
alternatives: Array[AlternativeQuery],
isTrailingDot: Boolean
) {
def isExact: Boolean = query.length < Fuzzy.ExactSearchLimit
def matches(bloom: BloomFilter[CharSequence]): Boolean =
alternatives.exists(_.matches(bloom))
def matches(symbol: CharSequence): Boolean =
Expand Down
Expand Up @@ -29,9 +29,16 @@ class CompletionProvider(
cursor = Some(params.offset)
)
val pos = unit.position(params.offset)
val editRange = pos
.withStart(inferIdentStart(pos, params.text()))
.withEnd(inferIdentEnd(pos, params.text()))
.toLSP
def textEdit(newText: String) = new l.TextEdit(editRange, newText)
val (i, completion) = safeCompletionsAt(pos)
val history = new ShortenedNames()
val sorted = i.results.sorted(memberOrdering(history, completion))
lazy val context = doLocateContext(pos)
lazy val importPosition = autoImportPosition(pos, params.text())
val items = sorted.iterator.zipWithIndex.map {
case (r, idx) =>
params.checkCanceled()
Expand All @@ -44,6 +51,8 @@ class CompletionProvider(
o.label
case o: TextEditMember =>
o.label.getOrElse(ident)
case o: WorkspaceMember =>
s"$ident - ${o.sym.owner.fullName}"
case _ =>
ident
}
Expand Down Expand Up @@ -74,26 +83,42 @@ class CompletionProvider(
}
}

item.setInsertTextFormat(InsertTextFormat.Snippet)

r match {
case i: TextEditMember =>
item.setTextEdit(i.edit)
item.setInsertTextFormat(InsertTextFormat.Snippet)
case i: OverrideDefMember =>
item.setTextEdit(i.edit)
item.setAdditionalTextEdits(i.autoImports.asJava)
item.setInsertTextFormat(InsertTextFormat.Snippet)
case w: WorkspaceMember =>
item.setInsertTextFormat(InsertTextFormat.Snippet)
item.setInsertText(w.sym.fullName + suffix)
importPosition match {
case None =>
// No import position, fully qualify the name in-place.
item.setTextEdit(textEdit(w.sym.fullName + suffix))
case Some(value) =>
val (short, edits) = ShortenedNames.synthesize(
TypeRef(
ThisType(w.sym.owner),
w.sym,
Nil
),
pos,
context,
value
)
item.setAdditionalTextEdits(edits.asJava)
item.setTextEdit(textEdit(short + suffix))
}
case _ =>
val baseLabel = Option(item.getTextEdit).fold(label)(_.getNewText)
if (r.sym.isNonNullaryMethod) {
item.setInsertTextFormat(InsertTextFormat.Snippet)
r.sym.paramss match {
case Nil =>
case Nil :: Nil =>
item.setInsertText(label + "()")
item.setTextEdit(textEdit(baseLabel + "()"))
case _ =>
item.setInsertText(label + "($0)")
item.setTextEdit(textEdit(baseLabel + "($0)"))
metalsConfig
.parameterHintsCommand()
.asScala
Expand All @@ -104,8 +129,7 @@ class CompletionProvider(
}
}
} else if (!suffix.isEmpty) {
item.setInsertTextFormat(InsertTextFormat.Snippet)
item.setInsertText(label + suffix)
item.setTextEdit(textEdit(baseLabel + suffix))
}
}

Expand Down Expand Up @@ -308,7 +332,7 @@ class CompletionProvider(
pos: Position,
visit: Member => Boolean
): SymbolSearch.Result = {
if (query.isEmpty) SymbolSearch.Result.COMPLETE
if (query.isEmpty) SymbolSearch.Result.INCOMPLETE
else {
val context = doLocateContext(pos)
val visitor = new CompilerSearchVisitor(query, context, visit)
Expand Down Expand Up @@ -401,4 +425,26 @@ class CompletionProvider(
}
}

/**
* Returns the start offset of the identifier starting as the given offset position.
*/
def inferIdentStart(pos: Position, text: String): Int = {
var i = pos.point - 1
while (i > 0 && text.charAt(i).isUnicodeIdentifierPart) {
i -= 1
}
i + 1
}

/**
* Returns the end offset of the identifier starting as the given offset position.
*/
def inferIdentEnd(pos: Position, text: String): Int = {
var i = pos.point
while (i < text.length && text.charAt(i).isUnicodeIdentifierPart) {
i += 1
}
i
}

}
134 changes: 93 additions & 41 deletions mtags/src/main/scala/scala/meta/internal/pc/Completions.scala
Expand Up @@ -297,6 +297,53 @@ trait Completions { this: MetalsGlobal =>

}

/**
* A position to insert new imports
*
* @param offset the offset where to place the import.
* @param indent the indentation at which to place the import.
*/
case class AutoImportPosition(offset: Int, indent: Int)

/**
* Extractor for tree nodes where we can insert import statements.
*/
object NonSyntheticBlock {
def unapply(tree: Tree): Option[List[Tree]] = tree match {
case t: Template => Some(t.body)
case b: Block => Some(b.stats)
case _ => None
}
}

/**
* Extractor for a tree node where we can insert a leading import statements.
*/
object NonSyntheticStatement {
def unapply(tree: Tree): Boolean = tree match {
case t: ValDef => !t.name.containsChar('$')
case _ => true
}
}

def autoImportPosition(
pos: Position,
text: String
): Option[AutoImportPosition] = {
if (lastEnclosing.isEmpty) {
locateTree(pos)
}
lastEnclosing.headOption match {
case Some(_: Import) => None
case _ =>
lastEnclosing.sliding(2).collectFirst {
case List(stat @ NonSyntheticStatement(), NonSyntheticBlock(stats)) =>
val top = stats.find(_.pos.includes(stat.pos)).getOrElse(stat)
val line = top.pos.source.lineToOffset(top.pos.focusStart.line - 1)
AutoImportPosition(line, inferIndent(line, text))
}
}
}
def completionPosition(pos: Position, text: String): CompletionPosition = {
lastEnclosing match {
case (name: Ident) :: (a: Apply) :: _ =>
Expand Down Expand Up @@ -706,20 +753,6 @@ trait Completions { this: MetalsGlobal =>
val range = pos.withStart(editStart).withEnd(pos.point).toLSP
val lineStart = pos.source.lineToOffset(pos.line - 1)

// Infers the indentation at the completion position by counting the number of leading
// spaces in the line.
// For example:
// class Main {
// def foo<COMPLETE> // inferred indent is 2 spaces.
// }
def inferIndent: Int = {
var i = 0
while (lineStart + i < text.length && text.charAt(lineStart + i) == ' ') {
i += 1
}
i
}

// Returns all the symbols of all transitive supertypes in the enclosing scope.
// For example:
// class Main extends Serializable {
Expand Down Expand Up @@ -747,31 +780,6 @@ trait Completions { this: MetalsGlobal =>
isVisited
}

// Returns the symbols that have been renamed in this scope.
// For example:
// import java.lang.{Boolean => JBoolean}
// class Main {
// // renamedSymbols: Map(j.l.Boolean => JBoolean)
// }
def renamedSymbols(context: Context): collection.Map[Symbol, Name] = {
val result = mutable.Map.empty[Symbol, Name]
context.imports.foreach { imp =>
lazy val pre = imp.qual.tpe
imp.tree.selectors.foreach { sel =>
if (sel.rename != null) {
val member = pre.member(sel.name)
result(member) = sel.rename
member.companion match {
case NoSymbol =>
case companion =>
result(companion) = sel.rename
}
}
}
}
result
}

// Returns true if this symbol is a method that we can override.
def isOverridableMethod(sym: Symbol): Boolean = {
sym.isMethod &&
Expand Down Expand Up @@ -816,7 +824,7 @@ trait Completions { this: MetalsGlobal =>
}
val history = new ShortenedNames(
lookupSymbol = { name =>
context.lookupSymbol(name, _ => true)
context.lookupSymbol(name, _ => true) :: Nil
},
config = renameConfig,
renames = re,
Expand Down Expand Up @@ -852,7 +860,12 @@ trait Completions { this: MetalsGlobal =>
edit,
filter + sym.name.decoded,
sym,
history.autoImports(pos, context, lineStart, inferIndent)
history.autoImports(
pos,
context,
lineStart,
inferIndent(lineStart, text)
)
)
}

Expand Down Expand Up @@ -1012,4 +1025,43 @@ trait Completions { this: MetalsGlobal =>
inverseSemanticdbSymbol(sym) -> nme
}
.filterKeys(_ != NoSymbol)

// Infers the indentation at the completion position by counting the number of leading
// spaces in the line.
// For example:
// class Main {
// def foo<COMPLETE> // inferred indent is 2 spaces.
// }
def inferIndent(lineStart: Int, text: String): Int = {
var i = 0
while (lineStart + i < text.length && text.charAt(lineStart + i) == ' ') {
i += 1
}
i
}

// Returns the symbols that have been renamed in this scope.
// For example:
// import java.lang.{Boolean => JBoolean}
// class Main {
// // renamedSymbols: Map(j.l.Boolean => JBoolean)
// }
def renamedSymbols(context: Context): collection.Map[Symbol, Name] = {
val result = mutable.Map.empty[Symbol, Name]
context.imports.foreach { imp =>
lazy val pre = imp.qual.tpe
imp.tree.selectors.foreach { sel =>
if (sel.rename != null) {
val member = pre.member(sel.name)
result(member) = sel.rename
member.companion match {
case NoSymbol =>
case companion =>
result(companion) = sel.rename
}
}
}
}
result
}
}

0 comments on commit c9b4d83

Please sign in to comment.