diff --git a/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala new file mode 100644 index 000000000000..d5f7a66e105b --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala @@ -0,0 +1,66 @@ +package dotty.tools.pc + +import org.eclipse.lsp4j +import org.eclipse.lsp4j.DiagnosticSeverity +import dotty.tools.dotc.interactive.InteractiveDriver +import dotty.tools.dotc.interfaces.Diagnostic as DiagnosticInterfaces +import dotty.tools.dotc.reporting.Diagnostic +import dotty.tools.pc.utils.InteractiveEnrichments.toLsp + +import scala.meta.pc.VirtualFileParams +import ch.epfl.scala.bsp4j +import dotty.tools.dotc.reporting.CodeAction +import dotty.tools.dotc.rewrites.Rewrites.ActionPatch +import scala.jdk.CollectionConverters.* +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.reporting.ErrorMessageID +import org.eclipse.lsp4j.DiagnosticTag + +class DiagnosticProvider(driver: InteractiveDriver, params: VirtualFileParams): + + def diagnostics(): List[lsp4j.Diagnostic] = + val diags = driver.run(params.uri().nn, params.text().nn) + given Context = driver.currentCtx + diags.flatMap(toLsp) + + private def toLsp(diag: Diagnostic)(using Context): Option[lsp4j.Diagnostic] = + Option.when(diag.pos.exists): + val lspDiag = lsp4j.Diagnostic( + diag.pos.toLsp, + diag.msg.message, + toDiagnosticSeverity(diag.level), + "presentation compiler", + ) + lspDiag.setCode(diag.msg.errorId.errorNumber) + + val scalaDiagnostic = new bsp4j.ScalaDiagnostic() + val actions = diag.msg.actions.map(toBspScalaAction).asJava + scalaDiagnostic.setActions(actions) + // lspDiag.setRelatedInformation(???) Currently not emitted by the compiler + lspDiag.setData(scalaDiagnostic) + if diag.msg.errorId == ErrorMessageID.UnusedSymbolID then + lspDiag.setTags(List(DiagnosticTag.Unnecessary).asJava) + + lspDiag + + private def toBspScalaAction(action: CodeAction): bsp4j.ScalaAction = + val bspAction = bsp4j.ScalaAction(action.title) + action.description.foreach(bspAction.setDescription) + val workspaceEdit = bsp4j.ScalaWorkspaceEdit(action.patches.map(toBspTextEdit).asJava) + bspAction.setEdit(workspaceEdit) + bspAction + + private def toBspTextEdit(actionPatch: ActionPatch): bsp4j.ScalaTextEdit = + val startPos = bsp4j.Position(actionPatch.srcPos.startLine, actionPatch.srcPos.startColumn) + val endPos = bsp4j.Position(actionPatch.srcPos.endLine, actionPatch.srcPos.endColumn) + val range = bsp4j.Range(startPos, endPos) + bsp4j.ScalaTextEdit(range, actionPatch.replacement) + + + private def toDiagnosticSeverity(severity: Int): DiagnosticSeverity = + severity match + case DiagnosticInterfaces.ERROR => DiagnosticSeverity.Error + case DiagnosticInterfaces.WARNING => DiagnosticSeverity.Warning + case DiagnosticInterfaces.INFO => DiagnosticSeverity.Information + case _ => DiagnosticSeverity.Information + diff --git a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala index 218d92c38ffa..b3bb78e80385 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala @@ -7,6 +7,7 @@ import java.util.Optional import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutorService import java.util.concurrent.ScheduledExecutorService +import java.util.Collections import java.util as ju import scala.concurrent.ExecutionContext @@ -108,7 +109,9 @@ case class ScalaPresentationCompiler( params.token() ) { access => val driver = access.compiler() - new PcSemanticTokensProvider(driver, params).provide().asJava + new PcSemanticTokensProvider(driver, params) + .provide() + .asJava } override def inlayHints( @@ -175,7 +178,7 @@ case class ScalaPresentationCompiler( params: OffsetParams ): CompletableFuture[ju.List[DocumentHighlight]] = compilerAccess.withInterruptableCompiler(Some(params))( - List.empty[DocumentHighlight].asJava, + Collections.emptyList(), params.token() ) { access => val driver = access.compiler() @@ -186,7 +189,7 @@ case class ScalaPresentationCompiler( params: ReferencesRequest ): CompletableFuture[ju.List[ReferencesResult]] = compilerAccess.withNonInterruptableCompiler(Some(params.file()))( - List.empty[ReferencesResult].asJava, + Collections.emptyList(), params.file().token, ) { access => val driver = access.compiler() @@ -204,14 +207,11 @@ case class ScalaPresentationCompiler( new InferExpectedType(search, driver, params).infer().asJava } - def shutdown(): Unit = - compilerAccess.shutdown() + def shutdown(): Unit = compilerAccess.shutdown() - def restart(): Unit = - compilerAccess.shutdownCurrentCompiler() + def restart(): Unit = compilerAccess.shutdownCurrentCompiler() - def diagnosticsForDebuggingPurposes(): ju.List[String] = - List[String]().asJava + def diagnosticsForDebuggingPurposes(): ju.List[String] = Collections.emptyList() override def info( symbol: String @@ -264,7 +264,7 @@ case class ScalaPresentationCompiler( ju.List[scala.meta.pc.AutoImportsResult] ] = compilerAccess.withNonInterruptableCompiler(Some(params))( - List.empty[scala.meta.pc.AutoImportsResult].asJava, + Collections.emptyList(), params.token() ) { access => val driver = access.compiler() @@ -283,9 +283,8 @@ case class ScalaPresentationCompiler( def implementAbstractMembers( params: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = - val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() compilerAccess.withNonInterruptableCompiler(Some(params))( - empty, + Collections.emptyList(), params.token() ) { pc => val driver = pc.compiler() @@ -301,9 +300,8 @@ case class ScalaPresentationCompiler( override def insertInferredType( params: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = - val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() compilerAccess.withNonInterruptableCompiler(Some(params))( - empty, + Collections.emptyList(), params.token() ) { pc => new InferredTypeProvider(params, pc.compiler(), config, search) @@ -330,8 +328,10 @@ case class ScalaPresentationCompiler( range: RangeParams, extractionPos: OffsetParams ): CompletableFuture[ju.List[l.TextEdit]] = - val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]() - compilerAccess.withInterruptableCompiler(Some(range))(empty, range.token()) { + compilerAccess.withInterruptableCompiler(Some(range))( + Collections.emptyList(), + range.token() + ) { pc => new ExtractMethodProvider( range, @@ -368,7 +368,7 @@ case class ScalaPresentationCompiler( ): CompletableFuture[ju.List[l.SelectionRange]] = CompletableFuture.completedFuture { compilerAccess.withSharedCompiler(params.asScala.headOption)( - List.empty[l.SelectionRange].asJava + Collections.emptyList() ) { pc => new SelectionRangeProvider( pc.compiler(), @@ -438,7 +438,13 @@ case class ScalaPresentationCompiler( override def didChange( params: VirtualFileParams ): CompletableFuture[ju.List[l.Diagnostic]] = - CompletableFuture.completedFuture(Nil.asJava) + compilerAccess.withNonInterruptableCompiler(Some(params))( + Collections.emptyList(), + params.token() + ) { access => + val driver = access.compiler() + new DiagnosticProvider(driver, params).diagnostics().asJava + } override def didClose(uri: URI): Unit = compilerAccess.withNonInterruptableCompiler(None)( diff --git a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala new file mode 100644 index 000000000000..e15eed749800 --- /dev/null +++ b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala @@ -0,0 +1,59 @@ +package dotty.tools.pc.tests + +import scala.language.unsafeNulls + +import dotty.tools.pc.base.BasePCSuite +import dotty.tools.pc.utils.RangeReplace + +import java.net.URI +import scala.meta.internal.jdk.CollectionConverters.* +import scala.meta.internal.metals.CompilerVirtualFileParams +import scala.meta.internal.metals.EmptyCancelToken + +import org.junit.Test +import org.eclipse.lsp4j.DiagnosticSeverity +import dotty.tools.pc.utils.TestExtensions.getOffset + +class DiagnosticProviderSuite extends BasePCSuite with RangeReplace { + case class TestDiagnostic(startIndex: Int, endIndex: Int, msg: String, severity: DiagnosticSeverity) + + def check( + text: String, + expected: List[TestDiagnostic] + ): Unit = + val diagnostics = presentationCompiler + .didChange(CompilerVirtualFileParams(URI.create("file:/Diagnostic.scala"), text, EmptyCancelToken)) + .get() + .asScala + + val actual = diagnostics.map(d => TestDiagnostic(d.getRange().getStart().getOffset(text), d.getRange().getEnd().getOffset(text), d.getMessage(), d.getSeverity())) + assertEquals(expected, actual, s"Expected [${expected.mkString(", ")}] but got [${actual.mkString(", ")}]") + + @Test def error = + check( + """|class Bar(i: It) + |""".stripMargin, + List(TestDiagnostic(13, 15, "Not found: type It - did you mean Int.type? or perhaps Int?", DiagnosticSeverity.Error)) + ) + + @Test def warning = + check( + """|object M: + | 1 + 1 + |""".stripMargin, + List(TestDiagnostic(12, 17, "A pure expression does nothing in statement position", DiagnosticSeverity.Warning)) + ) + + @Test def mixed = + check( + """|class Bar(i: It) + |object M: + | 1 + 1 + |""".stripMargin, + List( + TestDiagnostic(13 ,15, "Not found: type It - did you mean Int.type? or perhaps Int?", DiagnosticSeverity.Error), + TestDiagnostic(29, 34, "A pure expression does nothing in statement position", DiagnosticSeverity.Warning) + ) + ) + +} diff --git a/project/Build.scala b/project/Build.scala index 806412931696..8ad3d9ede7a5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1487,6 +1487,7 @@ object Build { .exclude("org.eclipse.lsp4j","org.eclipse.lsp4j") .exclude("org.eclipse.lsp4j","org.eclipse.lsp4j.jsonrpc"), "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.20.1", + "ch.epfl.scala" % "bsp4j" % "2.1.1", ), libraryDependencies += ("org.scalameta" % "mtags-shared_2.13.15" % mtagsVersion % SourceDeps), ivyConfigurations += SourceDeps.hide,