diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index 5237f19d19ae..025a2022500d 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -90,29 +90,22 @@ object Completion: val completionSymbolKind: Mode = path match - case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport - case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport - case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term // literal completions + case GenericImportSelector(sel) => + if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@ + else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport + else Mode.None // import scala.{util => u@@} + case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@ + case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions case (ref: untpd.RefTree) :: _ => - if (ref.name.isTermName) Mode.Term - else if (ref.name.isTypeName) Mode.Type - else Mode.None + val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope - case (sel: untpd.ImportSelector) :: _ => - if sel.imported.span.contains(pos.span) then Mode.ImportOrExport - else Mode.None // Can't help completing the renaming + if (ref.name.isTermName) Mode.Term | maybeSelectMembers + else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers + else Mode.None - case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport case _ => Mode.None - val completionKind: Mode = - path match - case Nil | (_: untpd.PackageDef) :: _ => Mode.None - case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.Member - case (_: untpd.Select) :: _ => Mode.Member - case _ => Mode.Scope - - completionSymbolKind | completionKind + completionSymbolKind /** When dealing with in varios palces we check to see if they are * due to incomplete backticks. If so, we ensure we get the full prefix @@ -141,18 +134,11 @@ object Completion: i + 1 path match - case (sel: untpd.ImportSelector) :: _ => - completionPrefix(sel.imported :: Nil, pos) - - case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven => - if sel.isWildcard then pos.source.content()(pos.point - 1).toString + case GenericImportSelector(sel) => + if sel.isGiven then completionPrefix(sel.bound :: Nil, pos) + else if sel.isWildcard then pos.source.content()(pos.point - 1).toString else completionPrefix(sel.imported :: Nil, pos) - case (tree: untpd.ImportOrExport) :: _ => - tree.selectors.find(_.span.contains(pos.span)).map: selector => - completionPrefix(selector :: Nil, pos) - .getOrElse("") - // Foo.`se will result in Select(Ident(Foo), ) case (select: untpd.Select) :: _ if select.name == nme.ERROR => checkBacktickPrefix(select.source.content(), select.nameSpan.start, select.span.end) @@ -169,6 +155,20 @@ object Completion: end completionPrefix + private object GenericImportSelector: + def unapply(path: List[untpd.Tree]): Option[untpd.ImportSelector] = + path match + case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ => Some(sel) + case (sel: untpd.ImportSelector) :: _ => Some(sel) + case _ => None + + private object GenericImportOrExport: + def unapply(path: List[untpd.Tree]): Option[untpd.ImportOrExport] = + path match + case untpd.Ident(_) :: (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport) + case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport) + case _ => None + /** Inspect `path` to determine the offset where the completion result should be inserted. */ def completionOffset(untpdPath: List[untpd.Tree]): Int = untpdPath match @@ -211,7 +211,6 @@ object Completion: case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual) case tpd.Select(qual, _) :: _ => Map.empty case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case (_: untpd.ImportSelector) :: tpd.Import(expr, _) :: _ => completer.directMemberCompletions(expr) case _ => completer.scopeCompletions interactiv.println(i"""completion info with pos = $pos, diff --git a/compiler/test/dotty/tools/dotc/interactive/CustomCompletion.scala b/compiler/test/dotty/tools/dotc/interactive/CustomCompletion.scala deleted file mode 100644 index 7b422a1164ae..000000000000 --- a/compiler/test/dotty/tools/dotc/interactive/CustomCompletion.scala +++ /dev/null @@ -1,130 +0,0 @@ -package dotty.tools.dotc.interactive - -import dotty.tools.dotc.ast.tpd._ -import dotty.tools.dotc.ast.untpd -import dotty.tools.dotc.core.Contexts._ -import dotty.tools.dotc.core.Denotations.SingleDenotation -import dotty.tools.dotc.core.Flags._ -import dotty.tools.dotc.core.NameOps._ -import dotty.tools.dotc.core.Names.{Name, termName} -import dotty.tools.dotc.core.StdNames.nme -import dotty.tools.dotc.core.Symbols.{Symbol, defn} -import dotty.tools.dotc.core.TypeError -import dotty.tools.dotc.util.Chars.{isOperatorPart, isScalaLetter} -import dotty.tools.dotc.util.SourcePosition - -object CustomCompletion { - - def completions( - pos: SourcePosition, - dependencyCompleteOpt: Option[String => (Int, Seq[String])], - enableDeep: Boolean - )(using Context): (Int, List[Completion]) = { - val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) - computeCompletions(pos, path, dependencyCompleteOpt, enableDeep)(using Interactive.contextOfPath(path)) - } - - def computeCompletions( - pos: SourcePosition, - path: List[Tree], - dependencyCompleteOpt: Option[String => (Int, Seq[String])], - enableDeep: Boolean - )(using Context): (Int, List[Completion]) = { - val mode = Completion.completionMode(path, pos) - val prefix = Completion.completionPrefix(path, pos) - val completer = new DeepCompleter(mode, prefix, pos) - - var extra = List.empty[Completion] - - val completions = path match { - case Select(qual, _) :: _ => completer.selectionCompletions(qual) - case Import(Ident(name), _) :: _ if name.decode.toString == "$ivy" && dependencyCompleteOpt.nonEmpty => - val complete = dependencyCompleteOpt.get - val (pos, completions) = complete(prefix) - val input0 = prefix.take(pos) - extra ++= completions.distinct.toList - .map(s => Completion(label(termName(input0 + s)), "", Nil)) - Map.empty - case Import(expr, _) :: _ => completer.directMemberCompletions(expr) - case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) - case _ => - completer.scopeCompletions ++ { - if (enableDeep) completer.deepCompletions - else Nil - } - } - - val describedCompletions = extra ++ describeCompletions(completions) - val offset = Completion.completionOffset(path) - - (pos.span.start - prefix.length, describedCompletions) - } - - private type CompletionMap = Map[Name, Seq[SingleDenotation]] - - private def describeCompletions(completions: CompletionMap)(using Context): List[Completion] = { - for - (name, denots) <- completions.toList - denot <- denots - yield - Completion(label(name), Completion.description(denot), List(denot.symbol)) - } - - class DeepCompleter(mode: Completion.Mode, prefix: String, pos: SourcePosition) extends Completion.Completer(mode, prefix, pos): - def deepCompletions(using Context): Map[Name, Seq[SingleDenotation]] = { - - def allMembers(s: Symbol) = - try s.info.allMembers - catch { - case _: dotty.tools.dotc.core.TypeError => Nil - } - def rec(t: Symbol): Seq[Symbol] = { - val children = - if (t.is(Package) || t.is(PackageVal) || t.is(PackageClass)) { - allMembers(t).map(_.symbol).filter(_ != t).flatMap(rec) - } else Nil - - t +: children.toSeq - } - - val syms = for { - member <- allMembers(defn.RootClass).map(_.symbol).toList - sym <- rec(member) - if sym.name.toString.startsWith(prefix) - } yield sym - - syms.map(sym => (sym.fullName, List(sym: SingleDenotation))).toMap - } - - private val bslash = '\\' - private val specialChars = Set('[', ']', '(', ')', '{', '}', '.', ',', ';') - - def label(name: Name): String = { - - def maybeQuote(name: Name, recurse: Boolean): String = - if (recurse && name.isTermName) - name.asTermName.qualToString(maybeQuote(_, true), maybeQuote(_, false)) - else { - // initially adapted from - // https://github.com/scala/scala/blob/decbd53f1bde4600c8ff860f30a79f028a8e431d/src/reflect/scala/reflect/internal/Printers.scala#L573-L584 - val decName = name.decode.toString - val hasSpecialChar = decName.exists { ch => - specialChars(ch) || ch.isWhitespace - } - def isOperatorLike = (name.isOperatorName || decName.exists(isOperatorPart)) && - decName.exists(isScalaLetter) && - !decName.contains(bslash) - lazy val term = name.toTermName - - val needsBackTicks = hasSpecialChar || - isOperatorLike || - nme.keywords(term) && term != nme.USCOREkw - - if (needsBackTicks) s"`$decName`" - else decName - } - - maybeQuote(name, true) - } -} - diff --git a/compiler/test/dotty/tools/dotc/interactive/CustomCompletionTests.scala b/compiler/test/dotty/tools/dotc/interactive/CustomCompletionTests.scala deleted file mode 100644 index a43a5cafce21..000000000000 --- a/compiler/test/dotty/tools/dotc/interactive/CustomCompletionTests.scala +++ /dev/null @@ -1,171 +0,0 @@ -package dotty.tools -package dotc.interactive - -import dotc.ast.tpd -import dotc.{CompilationUnit, Compiler, Run} -import dotc.core.Contexts.Context -import dotc.core.Mode -import dotc.reporting.StoreReporter -import dotc.util.{SourceFile, SourcePosition} -import dotc.util.Spans.Span - -import org.junit.Test - -class CustomCompletionTests extends DottyTest: - - private def completions( - input: String, - dependencyCompleter: Option[String => (Int, Seq[String])] = None, - deep: Boolean = false, - extraDefinitions: String = "" - ): (Int, Seq[Completion]) = - val prefix = extraDefinitions + """ - object Wrapper { - val expr = { - """ - val suffix = """ - } - } - """ - - val allCode = prefix + input + suffix - val index = prefix.length + input.length - - val run = new Run( - new Compiler, - initialCtx.fresh - .addMode(Mode.ReadPositions | Mode.Interactive) - // discard errors - comment out this line to print them in the console - .setReporter(new StoreReporter(null)) - .setSetting(initialCtx.settings.YstopAfter, List("typer")) - ) - val file = SourceFile.virtual("", allCode, maybeIncomplete = true) - given ctx: Context = run.runContext.withSource(file) - val unit = CompilationUnit(file) - ctx - .run.nn - .compileUnits(unit :: Nil, ctx) - - // ignoring compilation errors here - the input code - // to complete likely doesn't compile - - unit.tpdTree = { - import tpd._ - unit.tpdTree match { - case PackageDef(_, p) => - p.reverseIterator.collectFirst { - case TypeDef(_, tmpl: Template) => - tmpl.body - .collectFirst { case dd: ValDef if dd.name.show == "expr" => dd } - .getOrElse(sys.error("Unexpected tree shape")) - } - .getOrElse(sys.error("Unexpected tree shape")) - case _ => sys.error("Unexpected tree shape") - } - } - val ctx1 = ctx.fresh.setCompilationUnit(unit) - val srcPos = SourcePosition(file, Span(index)) - val (offset0, completions) = - if (deep || dependencyCompleter.nonEmpty) - CustomCompletion.completions(srcPos, dependencyCompleteOpt = dependencyCompleter, enableDeep = deep)(using ctx1) - else - Completion.completions(srcPos)(using ctx1) - val offset = offset0 - prefix.length - (offset, completions) - - - @Test def simple(): Unit = - val prefix = "scala.collection.immutable." - val input = prefix + "Ma" - - val (offset, completions0) = completions(input) - val labels = completions0.map(_.label) - - assert(offset == prefix.length) - assert(labels.contains("Map")) - - @Test def custom(): Unit = - val prefix = "import $ivy." - val input = prefix + "scala" - - val dependencies = Seq( - "scalaCompiler", - "scalaLibrary", - "other" - ) - val (offset, completions0) = completions( - input, - dependencyCompleter = Some { dep => - val matches = dependencies.filter(_.startsWith(dep)) - (0, matches) - } - ) - val labels = completions0.map(_.label) - - assert(offset == prefix.length) - assert(labels.contains("scalaCompiler")) - assert(labels.contains("scalaLibrary")) - assert(labels.length == 2) - - @Test def backTicks(): Unit = - val prefix = "Foo." - val input = prefix + "a" - - val extraDefinitions = - """object Foo { def a1 = 2; def `a-b` = 3 } - |""".stripMargin - val (offset, completions0) = completions( - input, - extraDefinitions = extraDefinitions, - deep = true // Enables CustomCompleter - ) - val labels = completions0.map(_.label) - - assert(offset == prefix.length) - assert(labels.contains("a1")) - assert(labels.contains("`a-b`")) - - @Test def backTicksDependencies(): Unit = - val prefix = "import $ivy." - val input = prefix + "`org.scala-lang:scala-`" - - val dependencies = Seq( - "org.scala-lang:scala-compiler", - "org.scala-lang:scala-library", - "other" - ) - val (offset, completions0) = completions( - input, - dependencyCompleter = Some { dep => - val matches = dependencies.filter(_.startsWith(dep)) - (0, matches) - } - ) - val labels = completions0.map(_.label) - - // Seems backticks mess with that for now... - // assert(offset == prefix.length) - assert(labels.contains("`org.scala-lang:scala-compiler`")) - assert(labels.contains("`org.scala-lang:scala-library`")) - assert(labels.length == 2) - - @Test def deep(): Unit = - val prefix = "" - val input = prefix + "ListBuf" - - val (offset, completions0) = completions(input, deep = true) - val labels = completions0.map(_.label) - - assert(offset == prefix.length) - assert(labels.contains("scala.collection.mutable.ListBuffer")) - - @Test def deepType(): Unit = - val prefix = "" - val input = prefix + "Function2" - - val (offset, completions0) = completions(input, deep = true) - val labels = completions0.map(_.label) - - assert(offset == prefix.length) - assert(labels.contains("scala.Function2")) - diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 335516d8c7f9..410a7efd4792 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -96,10 +96,10 @@ class CompletionTest { @Test def importCompleteFromPackage: Unit = { withSources( - code"""package a - abstract class MyClass""", - code"""package b - import a.My${m1}""" + code"""|package a + |abstract class MyClass""", + code"""|package b + |import a.My${m1}""" ).completion(("MyClass", Class, "a.MyClass")) } @@ -111,11 +111,11 @@ class CompletionTest { } @Test def importCompleteIncludesSynthetic: Unit = { - code"""case class MyCaseClass(foobar: Int) - object O { - val x = MyCaseClass(0) - import x.c${m1} - }""" + code"""|case class MyCaseClass(foobar: Int) + |object O { + | val x = MyCaseClass(0) + | import x.c${m1} + |}""" .completion( ("copy", Method, "(foobar: Int): MyCaseClass"), ("canEqual", Method, "(that: Any): Boolean"), @@ -131,11 +131,11 @@ class CompletionTest { @Test def importCompleteWithClassAndCompanion: Unit = { withSources( - code"""package pkg0 - class Foo - object Foo""", - code"""package pgk1 - import pkg0.F${m1}""" + code"""|package pkg0 + |class Foo + |object Foo""", + code"""|package pgk1 + |import pkg0.F${m1}""" ).completion( ("Foo", Class, "pkg0.Foo"), ("Foo", Module, "pkg0.Foo"), @@ -144,22 +144,22 @@ class CompletionTest { @Test def importCompleteIncludePackage: Unit = { withSources( - code"""package foo.bar - abstract classFizz""", + code"""|package foo.bar + |abstract classFizz""", code"""import foo.b${m1}""" ).completion(("bar", Module, "foo.bar")) } @Test def importCompleteIncludeMembers: Unit = { withSources( - code"""object MyObject { - val myVal = 0 - def myDef = 0 - var myVar = 0 - object myObject - abstract class myClass - trait myTrait - }""", + code"""|object MyObject { + | val myVal = 0 + | def myDef = 0 + | var myVar = 0 + | object myObject + | abstract class myClass + | trait myTrait + |}""", code"""import MyObject.my${m1}""" ).completion( ("myVal", Field, "Int"), @@ -201,9 +201,9 @@ class CompletionTest { } @Test def completeJavaModuleClass: Unit = { - code"""object O { - val out = java.io.FileDesc${m1} - }""" + code"""|object O { + | val out = java.io.FileDesc${m1} + |}""" .completion(("FileDescriptor", Module, "java.io.FileDescriptor")) } @@ -213,22 +213,26 @@ class CompletionTest { ("FileDescriptor", Class, "java.io.FileDescriptor"), ("FileDescriptor", Module, "java.io.FileDescriptor"), ) + } + @Test def noImportRename: Unit = { + code"""import java.io.{FileDescriptor => Fo$m1}""" + .noCompletions() } @Test def importGivenByType: Unit = { - code"""trait Foo - object Bar - import Bar.{given Fo$m1}""" + code"""|trait Foo + |object Bar + |import Bar.{given Fo$m1}""" .completion(("Foo", Class, "Foo")) } @Test def markDeprecatedSymbols: Unit = { - code"""object Foo { - @deprecated - val bar = 0 - } - import Foo.ba${m1}""" + code"""|object Foo { + | @deprecated + | val bar = 0 + |} + |import Foo.ba${m1}""" .completion(results => { assertEquals(1, results.size) val result = results.head @@ -290,11 +294,11 @@ class CompletionTest { } @Test def completionOnRenamedImport2: Unit = { - code"""import java.util.{HashMap => MyImportedSymbol} - trait Foo { - import java.io.{FileDescriptor => MyImportedSymbol} - val x: MyImp${m1} - }""" + code"""|import java.util.{HashMap => MyImportedSymbol} + |trait Foo { + | import java.io.{FileDescriptor => MyImportedSymbol} + | val x: MyImp${m1} + |}""" .completion( ("MyImportedSymbol", Class, "java.io.FileDescriptor"), ("MyImportedSymbol", Module, "java.io.FileDescriptor"), @@ -1679,4 +1683,28 @@ class CompletionTest { ("Numeric", Field, "Numeric"), ("Numeric", Method, "=> scala.math.Numeric.type") )) + + @Test def `empty-import-selector`: Unit = + code"""|import java.$m1 + |""" + .completion(results => { + val interestingResults = results.filter(_.getLabel().startsWith("util")) + assertEquals(1, interestingResults.size) + }) + + @Test def `empty-export-selector`: Unit = + code"""|export java.$m1 + |""" + .completion(results => { + val interestingResults = results.filter(_.getLabel().startsWith("util")) + assertEquals(1, interestingResults.size) + }) + + @Test def methodsWithInstantiatedTypeVars: Unit = + code"""|object M: + | Map.empty[Int, String].getOrEls$m1 + |""" + .completion(m1, Set( + ("getOrElse", Method, "[V1 >: String](key: Int, default: => V1): V1"), + )) } diff --git a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala index b95c5fb949e0..8546bbf62384 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/AutoImportsProvider.scala @@ -67,7 +67,7 @@ final class AutoImportsProvider( val results = symbols.result.filter(isExactMatch(_, name)) if results.nonEmpty then - val correctedPos = CompletionPos.infer(pos, params, path).sourcePos + val correctedPos = CompletionPos.infer(pos, params, path).toSourcePosition val mkEdit = path match // if we are in import section just specify full name diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteIvyCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteIvyCompletions.scala index 39f7144835c9..ec4a1813a437 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteIvyCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/AmmoniteIvyCompletions.scala @@ -13,7 +13,7 @@ object AmmoniteIvyCompletions: completionPos: CompletionPos, text: String )(using Context): List[CompletionValue] = - val pos = completionPos.sourcePos + val pos = completionPos.originalCursorPosition val query = selector.collectFirst { case sel: ImportSelector if sel.sourcePos.encloses(pos) && sel.sourcePos.`end` > pos.`end` => diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala index 53d867c924a6..6e828f8f2058 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala @@ -6,53 +6,45 @@ import java.net.URI import scala.meta.pc.OffsetParams import dotty.tools.dotc.ast.untpd.* -import dotty.tools.dotc.ast.untpd.ImportSelector import dotty.tools.dotc.core.Contexts.* -import dotty.tools.dotc.core.StdNames.* -import dotty.tools.dotc.util.Chars import dotty.tools.dotc.util.SourcePosition -import dotty.tools.dotc.util.Spans +import dotty.tools.dotc.util.Spans.* import dotty.tools.dotc.interactive.Completion import dotty.tools.pc.utils.MtagsEnrichments.* import org.eclipse.lsp4j as l -import scala.annotation.tailrec + +case object Cursor: + val value = "CURSOR" case class CompletionPos( - start: Int, - end: Int, - query: String, - cursorPos: SourcePosition, - sourceUri: URI + queryStart: Int, + identEnd: Int, + query: String, + originalCursorPosition: SourcePosition, + sourceUri: URI ): - - def sourcePos: SourcePosition = cursorPos.withSpan(Spans.Span(start, end)) - def stripSuffixEditRange: l.Range = new l.Range(cursorPos.offsetToPos(start), cursorPos.offsetToPos(end)) - def toEditRange: l.Range = cursorPos.withStart(start).withEnd(cursorPos.point).toLsp - -end CompletionPos + def queryEnd: Int = originalCursorPosition.point + def stripSuffixEditRange: l.Range = new l.Range(originalCursorPosition.offsetToPos(queryStart), originalCursorPosition.offsetToPos(identEnd)) + def toEditRange: l.Range = originalCursorPosition.withStart(queryStart).withEnd(originalCursorPosition.point).toLsp + def toSourcePosition: SourcePosition = originalCursorPosition.withSpan(Span(queryStart, queryEnd, queryEnd)) object CompletionPos: def infer( - cursorPos: SourcePosition, + sourcePos: SourcePosition, offsetParams: OffsetParams, adjustedPath: List[Tree] )(using Context): CompletionPos = - infer(cursorPos, offsetParams.uri().nn, offsetParams.text().nn, adjustedPath) + val identEnd = adjustedPath match + case (refTree: RefTree) :: _ if refTree.name.toString.contains(Cursor.value) => + refTree.span.end - Cursor.value.length + case _ => sourcePos.end - def infer( - cursorPos: SourcePosition, - uri: URI, - text: String, - adjustedPath: List[Tree] - )(using Context): CompletionPos = - val identEnd = inferIdentEnd(cursorPos, text) - val query = Completion.completionPrefix(adjustedPath, cursorPos) - val start = cursorPos.point - query.length() + val query = Completion.completionPrefix(adjustedPath, sourcePos) + val start = sourcePos.end - query.length() - CompletionPos(start, identEnd, query.nn, cursorPos, uri) - end infer + CompletionPos(start, identEnd, query.nn, sourcePos, offsetParams.uri.nn) /** * Infer the indentation by counting the number of spaces in the given line. @@ -76,12 +68,4 @@ object CompletionPos: (i, tabIndented) end inferIndent - /** - * Returns the end offset of the identifier starting as the given offset position. - */ - private def inferIdentEnd(pos: SourcePosition, text: String): Int = - var i = pos.point - while i < text.length() && Chars.isIdentifierPart(text.charAt(i)) do i += 1 - i - end CompletionPos diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala index 9d46a460850a..a583e31a9e0c 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala @@ -61,11 +61,10 @@ class CompletionProvider( val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) val indexedCtx = IndexedContext(locatedCtx) - val completionPos = - CompletionPos.infer(pos, params, adjustedPath)(using locatedCtx) + val completionPos = CompletionPos.infer(pos, params, adjustedPath)(using locatedCtx) val autoImportsGen = AutoImports.generator( - completionPos.sourcePos, + completionPos.toSourcePosition, text, unit.tpdTree, unit.comments, @@ -75,7 +74,6 @@ class CompletionProvider( val (completions, searchResult) = new Completions( - pos, text, locatedCtx, search, @@ -120,7 +118,7 @@ class CompletionProvider( * val a = 1 * @@ * }}} - * it's required to modify actual code by addition Ident. + * it's required to modify actual code by additional Ident. * * Otherwise, completion poisition doesn't point at any tree * because scala parser trim end position to the last statement pos. @@ -168,7 +166,7 @@ class CompletionProvider( additionalEdits: List[TextEdit] = Nil, range: Option[LspRange] = None ): CompletionItem = - val oldText = params.text().nn.substring(completionPos.start, completionPos.end) + val oldText = params.text().nn.substring(completionPos.queryStart, completionPos.identEnd) val editRange = if newText.startsWith(oldText) then completionPos.stripSuffixEditRange else completionPos.toEditRange @@ -287,6 +285,3 @@ class CompletionProvider( end match end completionItems end CompletionProvider - -case object Cursor: - val value = "CURSOR" diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index 6749f96911de..5e4d5d0f7384 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -37,7 +37,6 @@ import dotty.tools.dotc.core.Denotations.SingleDenotation import dotty.tools.dotc.interactive.Interactive class Completions( - pos: SourcePosition, text: String, ctx: Context, search: SymbolSearch, @@ -56,7 +55,7 @@ class Completions( given context: Context = ctx private lazy val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) - private lazy val completionMode = Completion.completionMode(adjustedPath, pos) + private lazy val completionMode = Completion.completionMode(adjustedPath, completionPos.originalCursorPosition) private lazy val shouldAddSnippet = path match @@ -83,8 +82,8 @@ class Completions( if !sym.name.endsWith(Cursor.value) then false else val realNameLength = sym.decodedName.length() - Cursor.value.length() - sym.source == pos.source && - sym.span.start + realNameLength == pos.span.end + sym.source == completionPos.originalCursorPosition.source && + sym.span.start + realNameLength == completionPos.queryEnd val generalExclude = isUninterestingSymbol(sym) || @@ -107,7 +106,7 @@ class Completions( end includeSymbol def completions(): (List[CompletionValue], SymbolSearch.Result) = - val (advanced, exclusive) = advancedCompletions(path, pos, completionPos) + val (advanced, exclusive) = advancedCompletions(path, completionPos) val (all, result) = if exclusive then (advanced, SymbolSearch.Result.COMPLETE) else @@ -116,19 +115,19 @@ class Completions( val allAdvanced = advanced ++ keywords path match // should not show completions for toplevel - case Nil | (_: PackageDef) :: _ if pos.source.file.extension != "sc" => + case Nil | (_: PackageDef) :: _ if completionPos.originalCursorPosition.source.file.extension != "sc" => (allAdvanced, SymbolSearch.Result.COMPLETE) case Select(qual, _) :: _ if qual.typeOpt.isErroneous => (allAdvanced, SymbolSearch.Result.COMPLETE) case Select(qual, _) :: _ => - val compilerCompletions = Completion.rawCompletions(pos, completionMode, completionPos.query, path, adjustedPath) + val compilerCompletions = Completion.rawCompletions(completionPos.originalCursorPosition, completionMode, completionPos.query, path, adjustedPath) val (compiler, result) = compilerCompletions .toList .flatMap(toCompletionValues) .filterInteresting(qual.typeOpt.widenDealias) (allAdvanced ++ compiler, result) case _ => - val compilerCompletions = Completion.rawCompletions(pos, completionMode, completionPos.query, path, adjustedPath) + val compilerCompletions = Completion.rawCompletions(completionPos.originalCursorPosition, completionMode, completionPos.query, path, adjustedPath) val (compiler, result) = compilerCompletions .toList .flatMap(toCompletionValues) @@ -267,9 +266,9 @@ class Completions( */ private def advancedCompletions( path: List[Tree], - pos: SourcePosition, completionPos: CompletionPos ): (List[CompletionValue], Boolean) = + val pos = completionPos.originalCursorPosition lazy val rawPath = Paths .get(pos.source.path).nn lazy val rawFileName = rawPath @@ -398,7 +397,6 @@ class Completions( val completions = InterpolatorCompletions .contribute( text, - pos, completionPos, indexedContext, lit, @@ -447,7 +445,7 @@ class Completions( // From Scala 3.1.3-RC3 (as far as I know), path contains // `Literal(Constant(null))` on head for an incomplete program, in this case, just ignore the head. case Literal(Constant(null)) :: tl => - advancedCompletions(tl, pos, completionPos) + advancedCompletions(tl, completionPos) case _ => val args = NamedArgCompletions.contribute( @@ -552,14 +550,7 @@ class Completions( else false, ) Some(search.searchMethods(query, buildTargetIdentifier, visitor).nn) - else - val filtered = indexedContext.scopeSymbols - .filter(sym => !sym.isConstructor && (!sym.is(Synthetic) || sym.is(Module))) - - filtered.map { sym => - visit(CompletionValue.Scope(sym.decodedName, sym, findSuffix(sym))) - } - Some(SymbolSearch.Result.INCOMPLETE) + else Some(SymbolSearch.Result.INCOMPLETE) end enrichWithSymbolSearch @@ -659,7 +650,7 @@ class Completions( private def isNotLocalForwardReference(sym: Symbol)(using Context): Boolean = !sym.isLocalToBlock || - !sym.srcPos.isAfter(pos) || + !sym.srcPos.isAfter(completionPos.originalCursorPosition) || sym.is(Param) private def computeRelevancePenalty( @@ -678,7 +669,7 @@ class Completions( def symbolRelevance(sym: Symbol): Int = var relevance = 0 // symbols defined in this file are more relevant - if pos.source != sym.source || sym.is(Package) then + if completionPos.originalCursorPosition.source != sym.source || sym.is(Package) then relevance |= IsNotDefinedInFile // fields are more relevant than non fields (such as method) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala index 95fbeec0cac0..2a8ead70ea33 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala @@ -12,6 +12,7 @@ import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Symbols.Symbol +import dotty.tools.dotc.util.Spans import dotty.tools.dotc.core.Types.Type import dotty.tools.dotc.util.SourcePosition import dotty.tools.pc.CompilerSearchVisitor @@ -24,7 +25,6 @@ object InterpolatorCompletions: def contribute( text: String, - pos: SourcePosition, completionPos: CompletionPos, indexedContext: IndexedContext, lit: Literal, @@ -35,12 +35,12 @@ object InterpolatorCompletions: config: PresentationCompilerConfig, buildTargetIdentifier: String )(using Context, ReportContext) = - InterpolationSplice(pos.span.point, text.toCharArray().nn, text) match + InterpolationSplice(completionPos.queryEnd, text.toCharArray().nn, text) match case Some(interpolator) => InterpolatorCompletions.contributeScope( text, lit, - pos, + completionPos, interpolator, indexedContext, completions, @@ -55,7 +55,6 @@ object InterpolatorCompletions: lit, path, text, - pos, completionPos, completions, snippetsEnabled, @@ -106,7 +105,6 @@ object InterpolatorCompletions: lit: Literal, path: List[Tree], text: String, - cursor: SourcePosition, completionPos: CompletionPos, completions: Completions, areSnippetsSupported: Boolean, @@ -166,7 +164,7 @@ object InterpolatorCompletions: label, Some(newText(name, suffix.toEditOpt, identOrSelect)), Nil, - Some(cursor.withStart(identOrSelect.span.start).toLsp), + Some(completionPos.originalCursorPosition.withStart(identOrSelect.span.start).toLsp), // Needed for VS Code which will not show the completion otherwise Some(identOrSelect.name.toString() + "." + label), denot.symbol, @@ -219,7 +217,7 @@ object InterpolatorCompletions: private def contributeScope( text: String, lit: Literal, - position: SourcePosition, + completionPos: CompletionPos, interpolator: InterpolationSplice, indexedContext: IndexedContext, completions: Completions, @@ -230,6 +228,7 @@ object InterpolatorCompletions: )(using ctx: Context, reportsContext: ReportContext): List[CompletionValue] = val litStartPos = lit.span.start val litEndPos = lit.span.end - Cursor.value.length() + val position = completionPos.originalCursorPosition val span = position.span val nameStart = span.withStart(span.start - interpolator.name.size) diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/KeywordsCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/KeywordsCompletions.scala index e912bc49032f..abbf43174bc7 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/KeywordsCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/KeywordsCompletions.scala @@ -21,7 +21,7 @@ object KeywordsCompletions: comments: List[Comment] )(using ctx: Context): List[CompletionValue] = lazy val notInComment = - checkIfNotInComment(completionPos.cursorPos, comments) + checkIfNotInComment(completionPos.originalCursorPosition, comments) path match case Nil | (_: PackageDef) :: _ if completionPos.query.isEmpty() => @@ -33,8 +33,7 @@ object KeywordsCompletions: case _ => val isExpression = this.isExpression(path) val isBlock = this.isBlock(path) - val isDefinition = - this.isDefinition(path, completionPos.query, completionPos.cursorPos) + val isDefinition = this.isDefinition(path, completionPos.query, completionPos.originalCursorPosition) val isMethodBody = this.isMethodBody(path) val isTemplate = this.isTemplate(path) val isPackage = this.isPackage(path) @@ -191,10 +190,10 @@ object KeywordsCompletions: case untpdTree: untpd.Tree => collectTypeAndModuleDefs(untpdTree, { case typeDef: (untpd.TypeDef | untpd.ModuleDef) => - typeDef.span.exists && typeDef.span.end < pos.sourcePos.span.start + typeDef.span.exists && typeDef.span.end < pos.queryStart case _ => false }) - .filter(tree => tree.span.exists && tree.span.end < pos.start) + .filter(tree => tree.span.exists && tree.span.end < pos.queryStart) .maxByOption(_.span.end) case _ => None } @@ -208,7 +207,7 @@ object KeywordsCompletions: template.derived.isEmpty ) - val untpdPath = NavigateAST.untypedPath(pos.cursorPos.span) + val untpdPath = NavigateAST.untypedPath(pos.originalCursorPosition.span) findLastSatisfyingTree(untpdPath).orElse { enclosing match case (typeDef: TypeDef) :: _ if typeDef.symbol.isEnumClass => untpdPath.headOption diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala index 963040db56e5..3790dd0473e4 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala @@ -532,8 +532,8 @@ class MatchCaseExtractor( name.toString().replace(Cursor.value, "") ) && (text .charAt( - completionPos.start - 1 - ) == ' ' || text.charAt(completionPos.start - 1) == '.') => + completionPos.queryStart - 1 + ) == ' ' || text.charAt(completionPos.queryStart - 1) == '.') => Some(qualifier) case _ => None end MatchExtractor diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionInterpolatorSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionInterpolatorSuite.scala index d70f28d08486..d9dc635ce21a 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionInterpolatorSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionInterpolatorSuite.scala @@ -40,11 +40,11 @@ class CompletionInterpolatorSuite extends BaseCompletionSuite: @Test def `string2` = checkEdit( - s"""|object Main { - | val myName = "" - | def message = "$$myNa@@me" - |} - |""".stripMargin, + """|object Main { + | val myName = "" + | def message = "$myNa@@me" + |} + |""".stripMargin, """|object Main { | val myName = "" | def message = s"${myName$0}me" @@ -558,18 +558,6 @@ class CompletionInterpolatorSuite extends BaseCompletionSuite: filter = _.contains("hello") ) - @Test def `brace-token-error-pos` = - checkEditLine( - """|object Main { - | val hello = "" - | ___ - |} - |""".stripMargin, - """s"Hello ${@@"""".stripMargin, - """s"Hello ${hello"""".stripMargin, - filter = _.contains("hello") - ) - @Test def `brace-token-error` = checkEditLine( """|object Main { @@ -759,3 +747,36 @@ class CompletionInterpolatorSuite extends BaseCompletionSuite: |}""".stripMargin, filter = _.contains("[A]") ) + + @Test def `dont-show-when-writing-before-dollar` = + check( + """|object M: + | val host = "" + | val path = "" + | + | println(s"host@@$path")} + |""".stripMargin, + "" + ) + + @Test def `show-when-writing-between-dollars` = + check( + """|object M: + | val host = "" + | val path = "" + | + | println(s"$host@@$path")} + |""".stripMargin, + "host: String" + ) + + @Test def `show-when-writing-between-dollars-2` = + check( + """|object M: + | val host = "" + | val path = "" + | + | println(s"$ho@@$path")} + |""".stripMargin, + "host: String" + ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala index dc972ec22533..c828cd4e6e67 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala @@ -38,7 +38,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | // tr@@ |} |""".stripMargin, - "transparentTrait - scala.annotation (commit: '')", + "", includeCommitCharacter = true ) @@ -57,7 +57,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | **/ |} |""".stripMargin, - "transparentTrait - scala.annotation (commit: '')", + "", includeCommitCharacter = true ) @@ -412,8 +412,6 @@ class CompletionKeywordSuite extends BaseCompletionSuite: |} """.stripMargin, """|def - |derived - scala.CanEqual - |deprecated - scala.runtime.stdLibPatches.language |""".stripMargin, ) @@ -428,7 +426,6 @@ class CompletionKeywordSuite extends BaseCompletionSuite: """.stripMargin, """|val |var - |varargs - scala.annotation |""".stripMargin ) @@ -467,13 +464,8 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | def hello(u@@) |}""".stripMargin, """|using (commit: '') - |unsafe - scala.caps (commit: '') - |unsafeNulls - scala.runtime.stdLibPatches.language (commit: '') - |unused - scala.annotation (commit: '') - |unshared - scala.annotation.internal (commit: '') |""".stripMargin, includeCommitCharacter = true, - topLines = Some(5) ) @Test def `not-using` = @@ -481,12 +473,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: """|object A{ | def hello(a: String, u@@) |}""".stripMargin, - """|unsafe - scala.caps - |unsafeNulls - scala.runtime.stdLibPatches.language - |unused - scala.annotation - |unshared - scala.annotation.internal - |unspecialized - scala.annotation""".stripMargin, - topLines = Some(5) + "", ) @Test def `extends-class` = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala index df6a3f705be9..6d2261bbfe66 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala @@ -54,9 +54,7 @@ class CompletionPatternSuite extends BaseCompletionSuite: | case ma@@ | } |}""".stripMargin, - """|macros - scala.languageFeature.experimental - |macroImpl - scala.reflect.macros.internal - |""".stripMargin + "" ) @Test def `bind2` = diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index 8f47582e4806..addd86fc32ab 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -1608,7 +1608,7 @@ class CompletionSuite extends BaseCompletionSuite: assertSingleItem = false ) - @Test def `prepend-instead-of-replace-duplicate-word` = + @Test def `prepend-duplicate-word` = checkEdit( """|object O: | println@@println() @@ -1622,10 +1622,10 @@ class CompletionSuite extends BaseCompletionSuite: @Test def `replace-when-inside` = checkEdit( """|object O: - | print@@ln() + | pri@@nt() |""".stripMargin, """|object O: - | println() + | print() |""".stripMargin, assertSingleItem = false ) @@ -1734,3 +1734,130 @@ class CompletionSuite extends BaseCompletionSuite: filter = _.contains("[") ) + @Test def `empty-import` = + check( + """|import @@ + |""".stripMargin, + """|java + |javax + |""".stripMargin, + filter = _.startsWith("java") + ) + + @Test def `empty-import-selector` = + check( + """|import java.@@ + |""".stripMargin, + """|util java + |""".stripMargin, + filter = _.startsWith("util") + ) + + @Test def `empty-export` = + check( + """|export @@ + |""".stripMargin, + """|java + |javax + |""".stripMargin, + filter = _.startsWith("java") + ) + + @Test def `empty-export-selector` = + check( + """|export java.@@ + |""".stripMargin, + """|util java + |""".stripMargin, + filter = _.startsWith("util") + ) + + @Test def `annotation` = + check( + """|@Over@@ + |object M {} + |""".stripMargin, + """|Override java.lang + |""".stripMargin, + filter = _ == "Override java.lang" + ) + + @Test def `no-annotation` = + check( + """| + |object M { + | Overr@@ + |} + |""".stripMargin, + """|Override java.lang + |""".stripMargin, + filter = _ == "Override java.lang" + ) + + @Test def `no-annotation-param-first-pos` = + check( + """| + |object M { + | def hello(Overr@@) + |} + |""".stripMargin, + "" + ) + + @Test def `no-annotation-param-second-pos` = + check( + """| + |object M { + | def hello(x: Int, Overr@@) + |} + |""".stripMargin, + "" + ) + + @Test def `no-annotation-param-second-list` = + check( + """| + |object M { + | def hello(x: Int)(Overr@@) + |} + |""".stripMargin, + "" + ) + + + @Test def `annotation-param-first-pos` = + check( + """| + |object M { + | def hello(@Overr@@) + |} + |""".stripMargin, + """|Override java.lang + |""".stripMargin, + filter = _ == "Override java.lang" + ) + + @Test def `annotation-param-second-pos` = + check( + """| + |object M { + | def hello(x: Int, @Overr@@) + |} + |""".stripMargin, + """|Override java.lang + |""".stripMargin, + filter = _ == "Override java.lang" + ) + + @Test def `annotation-param-second-list` = + check( + """| + |object M { + | def hello(x: Int)( @Overr@@) + |} + |""".stripMargin, + """|Override java.lang + |""".stripMargin, + filter = _ == "Override java.lang" + ) +