From 560081ce9dfa6ee85c7ab06bc29fccc267281314 Mon Sep 17 00:00:00 2001 From: Gabriele Petronella Date: Sun, 21 Feb 2021 19:01:06 +0100 Subject: [PATCH] WIP --- .../internal/metals/CodeActionProvider.scala | 3 +- .../internal/metals/ScalacDiagnostic.scala | 18 ++++ .../codeactions/ImportMissingGiven.scala | 86 +++++++++++++++++++ .../ImportMissingGivenLspSuite.scala | 37 ++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 metals/src/main/scala/scala/meta/internal/metals/codeactions/ImportMissingGiven.scala create mode 100644 tests/unit/src/test/scala/tests/codeactions/ImportMissingGivenLspSuite.scala diff --git a/metals/src/main/scala/scala/meta/internal/metals/CodeActionProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/CodeActionProvider.scala index 14e94661a16..b1606ae0f44 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/CodeActionProvider.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/CodeActionProvider.scala @@ -23,7 +23,8 @@ final class CodeActionProvider( new CreateNewSymbol(), new StringActions(buffers, trees), new OrganizeImports(scalafixProvider, buildTargets), - new InsertInferredType(trees, compilers) + new InsertInferredType(trees, compilers), + new ImportMissingGiven(compilers) ) def codeActions( diff --git a/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala b/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala index 56960f8b8c5..af4a142ebf6 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ScalacDiagnostic.scala @@ -30,4 +30,22 @@ object ScalacDiagnostic { case _ => None } } + + object SymbolNotFoundWithImportSuggestions { + // https://github.com/lampepfl/dotty/blob/3b741d67f8631487aa553c52e03ac21157e68563/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala#L311-L344 + private val regexes = for { + fix <- List("One of the following imports", "The following import") + help <- List("fix ", "make progress towards fixing") + } yield s"""(?s)$fix imports might $help the problem:(.*)""".r + def unapply(d: l.Diagnostic): Option[List[String]] = + regexes.collectFirst { + case regex if regex.findFirstIn(d.getMessage()).isDefined => + val suggestedImports = regex + .findFirstMatchIn(d.getMessage()) + .map( + _.group(1).linesIterator.filterNot(_.isEmpty()).map(_.trim).toList + ) + suggestedImports.getOrElse(Nil) + } + } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/codeactions/ImportMissingGiven.scala b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ImportMissingGiven.scala new file mode 100644 index 00000000000..1767dc50afd --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/codeactions/ImportMissingGiven.scala @@ -0,0 +1,86 @@ +package scala.meta.internal.metals.codeactions + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals._ +import scala.meta.pc.CancelToken + +import org.eclipse.{lsp4j => l} + +class ImportMissingGiven(compilers: Compilers) extends CodeAction { + override def kind: String = l.CodeActionKind.QuickFix + + override def contribute( + params: l.CodeActionParams, + token: CancelToken + )(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = { + + def addGivenImport( + diagnostic: l.Diagnostic, + packageName: String, + name: String + ): Future[Seq[l.CodeAction]] = + importMissingSymbol(diagnostic, packageName, name) + + def importMissingSymbol( + diagnostic: l.Diagnostic, + packageName: String, + 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.filter(_.packageName() == packageName).map { i => + val uri = params.getTextDocument().getUri() + val edit = new l.WorkspaceEdit(Map(uri -> i.edits).asJava) + + val codeAction = new l.CodeAction() + + codeAction.setTitle(ImportMissingGiven.title(name, i.packageName)) + codeAction.setKind(l.CodeActionKind.QuickFix) + codeAction.setDiagnostics(List(diagnostic).asJava) + codeAction.setEdit(edit) + + codeAction + } + } + } + + def parseImport(importSuggestion: String): (String, String) = { + val importMembers = importSuggestion.stripPrefix("import ").split(".") + val packageName = importMembers.dropRight(1).mkString(".") + val name = importMembers.last + (packageName -> name) + } + + val codeActions = params + .getContext() + .getDiagnostics() + .asScala + .collect { + case diag @ ScalacDiagnostic.SymbolNotFoundWithImportSuggestions( + rawSuggestions + ) if params.getRange().overlapsWith(diag.getRange()) => + Future + .traverse(rawSuggestions) { suggestion => + val (packageName, name) = parseImport(suggestion) + addGivenImport(diag, packageName, name) + } + .map(_.flatten) + } + .toSeq + + Future.sequence(codeActions).map(_.flatten) + } +} + +object ImportMissingGiven { + def title(name: String, packageName: String): String = + s"Import '$name' from package '$packageName'" +} diff --git a/tests/unit/src/test/scala/tests/codeactions/ImportMissingGivenLspSuite.scala b/tests/unit/src/test/scala/tests/codeactions/ImportMissingGivenLspSuite.scala new file mode 100644 index 00000000000..5fb2227c869 --- /dev/null +++ b/tests/unit/src/test/scala/tests/codeactions/ImportMissingGivenLspSuite.scala @@ -0,0 +1,37 @@ +package tests.codeactions + +import scala.meta.internal.metals.codeactions.CreateNewSymbol +import scala.meta.internal.metals.codeactions.ImportMissingGiven + +class ImportMissingGivenLspSuite + extends BaseCodeActionLspSuite("importMissingGiven") { + + check( + "basic", + """|trait X + |trait Y + |object test: + | def f(using x: X) = ??? + | locally { + | object instances { + | given xFromY(using Y) as X = ??? + | } + | <> // error + | } + """.stripMargin, + s"""|${ImportMissingGiven.title("instances", "xFromY")} + |""".stripMargin, + """|trait X + |trait Y + |object test: + | def f(using x: X) = ??? + | locally { + | object instances { + | given xFromY(using Y) as X = ??? + | } + | <> // error + | } + |""".stripMargin + ) + +}