diff --git a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala index b226eaec882..4018e32c346 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Compilers.scala @@ -15,15 +15,18 @@ import scala.util.control.NonFatal import scala.meta.inputs.Input import scala.meta.inputs.Position +import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.metals.CompilerOffsetParamsUtils import scala.meta.internal.metals.CompilerRangeParamsUtils import scala.meta.internal.metals.Compilers.PresentationCompilerKey import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.mtags.MD5 import scala.meta.internal.parsing.Trees import scala.meta.internal.pc.LogMessages import scala.meta.internal.pc.PcSymbolInformation import scala.meta.internal.worksheets.WorksheetPcData import scala.meta.internal.worksheets.WorksheetProvider +import scala.meta.internal.{semanticdb => s} import scala.meta.io.AbsolutePath import scala.meta.pc.AutoImportsResult import scala.meta.pc.CancelToken @@ -101,6 +104,9 @@ class Compilers( import compilerConfiguration._ + private val outlineFilesProvider = + new OutlineFilesProvider(buildTargets, buffers) + // Not a TrieMap because we want to avoid loading duplicate compilers for the same build target. // Not a `j.u.c.ConcurrentHashMap` because it can deadlock in `computeIfAbsent` when the absent // function is expensive, which is the case here. @@ -161,6 +167,7 @@ class Compilers( cache.clear() worksheetsCache.clear() worksheetsDigests.clear() + outlineFilesProvider.clear() } def restartAll(): Unit = { @@ -196,10 +203,7 @@ class Compilers( } def didClose(path: AbsolutePath): Unit = { - loadCompiler(path) - .map { pc => - pc.didClose(path.toNIO.toUri()) - } + loadCompiler(path).foreach(_.didClose(path.toNIO.toUri())) } def didChange(path: AbsolutePath): Future[List[Diagnostic]] = { @@ -225,6 +229,8 @@ class Compilers( AdjustedLspData.default, ) + outlineFilesProvider.didChange(pc.buildTargetId(), path) + for { ds <- pc @@ -241,13 +247,28 @@ class Compilers( } def didCompile(report: CompileReport): Unit = { - if (report.getErrors > 0) { - buildTargetPCFromCache(report.getTarget).foreach(_.restart()) - } else { + val isSuccessful = report.getErrors == 0 + buildTargetPCFromCache(report.getTarget).foreach { pc => + if ( + outlineFilesProvider.shouldRestartPc( + report.getTarget, + DidCompile(isSuccessful), + ) + ) { + pc.restart() + } + } + + outlineFilesProvider.onDidCompile(report.getTarget(), isSuccessful) + + if (isSuccessful) { // Restart PC for all build targets that depend on this target since the classfiles // may have changed. + for { target <- buildTargets.allInverseDependencies(report.getTarget) + if target != report.getTarget + if outlineFilesProvider.shouldRestartPc(target, InverseDependency) compiler <- buildTargetPCFromCache(target) } { compiler.restart() @@ -339,12 +360,14 @@ class Compilers( if (isZeroBased) i else i + 1 } - val offsetParams = CompilerOffsetParams( - path.toURI, - modified, - rangeEnd, - token, - ) + val offsetParams = + CompilerOffsetParams( + path.toURI, + modified, + rangeEnd, + token, + outlineFilesProvider.getOutlineFiles(compiler.buildTargetId()), + ) val previousLines = expression .getText() @@ -499,7 +522,12 @@ class Compilers( } val vFile = - CompilerVirtualFileParams(path.toNIO.toUri(), input.text, token) + CompilerVirtualFileParams( + path.toNIO.toUri(), + input.text, + token, + outlineFilesProvider.getOutlineFiles(compiler.buildTargetId()), + ) val isScala3 = ScalaVersions.isScala3Version(compiler.scalaVersion()) compiler @@ -578,7 +606,11 @@ class Compilers( } val rangeParams = - CompilerRangeParamsUtils.fromPos(pos, token) + CompilerRangeParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) val options = userConfig().inlayHintsOptions val pcParams = CompilerInlayHintsParams( rangeParams, @@ -609,8 +641,10 @@ class Compilers( token: CancelToken, ): Future[CompletionList] = withPCAndAdjustLsp(params) { (pc, pos, adjust) => + val outlineFiles = + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()) val offsetParams = - CompilerOffsetParamsUtils.fromPos(pos, token) + CompilerOffsetParamsUtils.fromPos(pos, token, outlineFiles) pc.complete(offsetParams) .asScala .map { list => @@ -628,7 +662,11 @@ class Compilers( withPCAndAdjustLsp(params) { (pc, pos, adjust) => pc.autoImports( name, - CompilerOffsetParamsUtils.fromPos(pos, token), + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ), findExtensionMethods, ).asScala .map { list => @@ -643,8 +681,13 @@ class Compilers( token: CancelToken, ): Future[ju.List[TextEdit]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => - pc.insertInferredType(CompilerOffsetParamsUtils.fromPos(pos, token)) - .asScala + pc.insertInferredType( + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ).asScala .map { edits => adjust.adjustTextEdits(edits) } @@ -656,8 +699,13 @@ class Compilers( token: CancelToken, ): Future[ju.List[TextEdit]] = withPCAndAdjustLsp(params) { (pc, pos, adjust) => - pc.inlineValue(CompilerOffsetParamsUtils.fromPos(pos, token)) - .asScala + pc.inlineValue( + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ).asScala .map(adjust.adjustTextEdits) }.getOrElse(Future.successful(Nil.asJava)) @@ -666,8 +714,13 @@ class Compilers( token: CancelToken, ): Future[ju.List[DocumentHighlight]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => - pc.documentHighlight(CompilerOffsetParamsUtils.fromPos(pos, token)) - .asScala + pc.documentHighlight( + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ).asScala .map { highlights => adjust.adjustDocumentHighlight(highlights) } @@ -683,7 +736,11 @@ class Compilers( withPCAndAdjustLsp(doc.getUri(), range, extractionPos) { (pc, metaRange, metaExtractionPos, adjust) => pc.extractMethod( - CompilerRangeParamsUtils.fromPos(metaRange, token), + CompilerRangeParamsUtils.fromPos( + metaRange, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ), CompilerOffsetParamsUtils.fromPos(metaExtractionPos, token), ).asScala .map { edits => @@ -699,7 +756,11 @@ class Compilers( ): Future[ju.List[TextEdit]] = { withPCAndAdjustLsp(position) { (pc, pos, adjust) => pc.convertToNamedArguments( - CompilerOffsetParamsUtils.fromPos(pos, token), + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ), argIndices, ).asScala .map { edits => @@ -713,8 +774,13 @@ class Compilers( token: CancelToken, ): Future[ju.List[TextEdit]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => - pc.implementAbstractMembers(CompilerOffsetParamsUtils.fromPos(pos, token)) - .asScala + pc.implementAbstractMembers( + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ).asScala .map { edits => adjust.adjustTextEdits(edits) } @@ -727,7 +793,11 @@ class Compilers( ): Future[Option[HoverSignature]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => pc.hover( - CompilerRangeParamsUtils.offsetOrRange(pos, token) + CompilerRangeParamsUtils.offsetOrRange( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) ).asScala .map(_.asScala.map { hover => adjust.adjustHoverResp(hover) }) } @@ -739,7 +809,11 @@ class Compilers( ): Future[ju.Optional[LspRange]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => pc.prepareRename( - CompilerRangeParamsUtils.offsetOrRange(pos, token) + CompilerRangeParamsUtils.offsetOrRange( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) ).asScala .map { range => range.map(adjust.adjustRange(_)) @@ -753,7 +827,11 @@ class Compilers( ): Future[ju.List[TextEdit]] = { withPCAndAdjustLsp(params) { (pc, pos, adjust) => pc.rename( - CompilerRangeParamsUtils.offsetOrRange(pos, token), + CompilerRangeParamsUtils.offsetOrRange( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ), params.getNewName(), ).asScala .map { edits => @@ -794,7 +872,11 @@ class Compilers( findTypeDef: Boolean, ): Future[DefinitionResult] = withPCAndAdjustLsp(params) { (pc, pos, adjust) => - val params = CompilerOffsetParamsUtils.fromPos(pos, token) + val params = CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) val defResult = if (findTypeDef) pc.typeDefinition(params) else @@ -830,7 +912,13 @@ class Compilers( token: CancelToken, ): Future[SignatureHelp] = withPCAndAdjustLsp(params) { (pc, pos, _) => - pc.signatureHelp(CompilerOffsetParamsUtils.fromPos(pos, token)).asScala + pc.signatureHelp( + CompilerOffsetParamsUtils.fromPos( + pos, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ).asScala }.getOrElse(Future.successful(new SignatureHelp())) def selectionRange( @@ -839,7 +927,13 @@ class Compilers( ): Future[ju.List[SelectionRange]] = { withPCAndAdjustLsp(params) { (pc, positions) => val offsetPositions: ju.List[OffsetParams] = - positions.map(CompilerOffsetParamsUtils.fromPos(_, token)) + positions.map( + CompilerOffsetParamsUtils.fromPos( + _, + token, + outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + ) pc.selectionRange(offsetPositions).asScala }.getOrElse(Future.successful(Nil.asJava)) } @@ -885,7 +979,8 @@ class Compilers( case Some(value) => if (path.isScalaFilename) loadCompiler(value) else if (path.isJavaFilename && forceScala) - loadCompiler(value).orElse(Some(loadJavaCompiler(value))) + loadCompiler(value) + .orElse(Some(loadJavaCompiler(value))) else if (path.isJavaFilename) Some(loadJavaCompiler(value)) else None } @@ -1019,7 +1114,9 @@ class Compilers( private def withPCAndAdjustLsp[T]( params: SelectionRangeParams - )(fn: (PresentationCompiler, ju.List[Position]) => T): Option[T] = { + )( + fn: (PresentationCompiler, ju.List[Position]) => T + ): Option[T] = { val path = params.getTextDocument.getUri.toAbsolutePath loadCompiler(path).map { compiler => val input = path @@ -1035,7 +1132,9 @@ class Compilers( private def withPCAndAdjustLsp[T]( params: TextDocumentPositionParams - )(fn: (PresentationCompiler, Position, AdjustLspData) => T): Option[T] = { + )( + fn: (PresentationCompiler, Position, AdjustLspData) => T + ): Option[T] = { val path = params.getTextDocument.getUri.toAbsolutePath loadCompiler(path).flatMap { compiler => val (input, pos, adjust) = @@ -1043,21 +1142,27 @@ class Compilers( params, compiler.scalaVersion(), ) - pos.toMeta(input).map(metaPos => fn(compiler, metaPos, adjust)) + pos + .toMeta(input) + .map(metaPos => fn(compiler, metaPos, adjust)) } } private def withPCAndAdjustLsp[T]( params: InlayHintParams - )(fn: (PresentationCompiler, Position, AdjustLspData) => T): Option[T] = { + )( + fn: (PresentationCompiler, Position, AdjustLspData) => T + ): Option[T] = { val path = params.getTextDocument.getUri.toAbsolutePath - loadCompiler(path).flatMap { compiler => + loadCompiler(path).flatMap { case compiler => val (input, pos, adjust) = sourceAdjustments( params, compiler.scalaVersion(), ) - pos.toMeta(input).map(metaPos => fn(compiler, metaPos, adjust)) + pos + .toMeta(input) + .map(metaPos => fn(compiler, metaPos, adjust)) } } @@ -1086,13 +1191,24 @@ class Compilers( adjustRequest(range.getEnd()), ).toMeta(input) metaExtractionPos <- adjustRequest(extractionPos).toMeta(input) - } yield fn(compiler, metaRange, metaExtractionPos, adjustResponse) + } yield fn( + compiler, + metaRange, + metaExtractionPos, + adjustResponse, + ) } } private def withPCAndAdjustLsp[T]( params: HoverExtParams - )(fn: (PresentationCompiler, Position, AdjustLspData) => T): Option[T] = { + )( + fn: ( + PresentationCompiler, + Position, + AdjustLspData, + ) => T + ): Option[T] = { val path = params.textDocument.getUri.toAbsolutePath loadCompiler(path).flatMap { compiler => @@ -1242,6 +1358,100 @@ class Compilers( debugItem } + def semanticdbTextDocument( + source: AbsolutePath, + text: String, + ): s.TextDocument = { + val pc = loadCompiler(source).getOrElse(fallbackCompiler) + + val (prependedLinesSize, modifiedText) = + Option + .when(source.isSbt)( + buildTargets + .sbtAutoImports(source) + ) + .flatten + .fold((0, text))(imports => + (imports.size, SbtBuildTool.prependAutoImports(text, imports)) + ) + + // NOTE(olafur): it's unfortunate that we block on `semanticdbTextDocument` + // here but to avoid it we would need to refactor the `Semanticdbs` trait, + // which requires more effort than it's worth. + val params = new CompilerVirtualFileParams( + source.toURI, + modifiedText, + token = EmptyCancelToken, + outlineFiles = outlineFilesProvider.getOutlineFiles(pc.buildTargetId()), + ) + val bytes = pc + .semanticdbTextDocument(params) + .get( + config.initialConfig.compilers.timeoutDelay, + config.initialConfig.compilers.timeoutUnit, + ) + val textDocument = { + val doc = s.TextDocument.parseFrom(bytes) + if (doc.text.isEmpty()) doc.withText(text) + else doc + } + if (prependedLinesSize > 0) + cleanupAutoImports(textDocument, text, prependedLinesSize) + else textDocument + } + + private def cleanupAutoImports( + document: s.TextDocument, + originalText: String, + linesSize: Int, + ): s.TextDocument = { + + def adjustRange(range: s.Range): Option[s.Range] = { + val nextStartLine = range.startLine - linesSize + val nextEndLine = range.endLine - linesSize + if (nextEndLine >= 0) { + val nextRange = range.copy( + startLine = nextStartLine, + endLine = nextEndLine, + ) + Some(nextRange) + } else None + } + + val adjustedOccurences = + document.occurrences.flatMap { occurence => + occurence.range + .flatMap(adjustRange) + .map(r => occurence.copy(range = Some(r))) + } + + val adjustedDiagnostic = + document.diagnostics.flatMap { diagnostic => + diagnostic.range + .flatMap(adjustRange) + .map(r => diagnostic.copy(range = Some(r))) + } + + val adjustedSynthetic = + document.synthetics.flatMap { synthetic => + synthetic.range + .flatMap(adjustRange) + .map(r => synthetic.copy(range = Some(r))) + } + + s.TextDocument( + schema = document.schema, + uri = document.uri, + text = originalText, + md5 = MD5.compute(originalText), + language = document.language, + symbols = document.symbols, + occurrences = adjustedOccurences, + diagnostics = adjustedDiagnostic, + synthetics = adjustedSynthetic, + ) + } + } object Compilers { diff --git a/metals/src/main/scala/scala/meta/internal/metals/InteractiveSemanticdbs.scala b/metals/src/main/scala/scala/meta/internal/metals/InteractiveSemanticdbs.scala index 247a6ff14d0..2395178d7e9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/InteractiveSemanticdbs.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/InteractiveSemanticdbs.scala @@ -3,12 +3,9 @@ package scala.meta.internal.metals import java.nio.charset.Charset import java.util.Collections -import scala.concurrent.Await -import scala.concurrent.duration.Duration import scala.util.Success import scala.util.Try -import scala.meta.internal.builds.SbtBuildTool import scala.meta.internal.metals.MetalsEnrichments._ import scala.meta.internal.metals.scalacli.ScalaCliServers import scala.meta.internal.mtags.MD5 @@ -145,111 +142,7 @@ final class InteractiveSemanticdbs( javaInteractiveSemanticdb.fold(s.TextDocument())( _.textDocument(source, text) ) - else scalaCompile(source, text) + else compilers().semanticdbTextDocument(source, text) } - private def scalaCompile( - source: AbsolutePath, - text: String, - ): s.TextDocument = { - - val pc = compilers() - .loadCompiler(source) - .orElse { - // load presentation compiler for sources that were create by a worksheet definition request - tables.worksheetSources - .getWorksheet(source) - .flatMap(compilers().loadWorksheetCompiler) - } - .getOrElse { - // this is highly unlikely since None is only returned for non scala/java files - throw new RuntimeException( - s"No presentation compiler found for $source" - ) - } - - val (prependedLinesSize, modifiedText) = - Option - .when(source.isSbt)( - buildTargets - .sbtAutoImports(source) - ) - .flatten - .fold((0, text))(imports => - (imports.size, SbtBuildTool.prependAutoImports(text, imports)) - ) - - // NOTE(olafur): it's unfortunate that we block on `semanticdbTextDocument` - // here but to avoid it we would need to refactor the `Semanticdbs` trait, - // which requires more effort than it's worth. - val bytes = Await - .result( - pc.semanticdbTextDocument(source.toURI, modifiedText).asScala, - Duration( - clientConfig.initialConfig.compilers.timeoutDelay, - clientConfig.initialConfig.compilers.timeoutUnit, - ), - ) - val textDocument = { - val doc = s.TextDocument.parseFrom(bytes) - if (doc.text.isEmpty()) doc.withText(text) - else doc - } - if (prependedLinesSize > 0) - cleanupAutoImports(textDocument, text, prependedLinesSize) - else textDocument - } - - private def cleanupAutoImports( - document: s.TextDocument, - originalText: String, - linesSize: Int, - ): s.TextDocument = { - - def adjustRange(range: s.Range): Option[s.Range] = { - val nextStartLine = range.startLine - linesSize - val nextEndLine = range.endLine - linesSize - if (nextEndLine >= 0) { - val nextRange = range.copy( - startLine = nextStartLine, - endLine = nextEndLine, - ) - Some(nextRange) - } else None - } - - val adjustedOccurences = - document.occurrences.flatMap { occurence => - occurence.range - .flatMap(adjustRange) - .map(r => occurence.copy(range = Some(r))) - } - - val adjustedDiagnostic = - document.diagnostics.flatMap { diagnostic => - diagnostic.range - .flatMap(adjustRange) - .map(r => diagnostic.copy(range = Some(r))) - } - - val adjustedSynthetic = - document.synthetics.flatMap { synthetic => - synthetic.range - .flatMap(adjustRange) - .map(r => synthetic.copy(range = Some(r))) - } - - s.TextDocument( - schema = document.schema, - uri = document.uri, - text = originalText, - md5 = MD5.compute(originalText), - language = document.language, - symbols = document.symbols, - occurrences = adjustedOccurences, - diagnostics = adjustedDiagnostic, - synthetics = adjustedSynthetic, - ) - } - } diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 126a8e3dcf3..9e13818e2cc 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -403,11 +403,16 @@ abstract class MetalsLspService( scalaVersionSelector, clientConfig.icons, onCreate = path => { - buildTargets.onCreate(path) + onCreate(path) onChange(List(path)) }, ) + protected def onCreate(path: AbsolutePath): Unit = { + buildTargets.onCreate(path) + compilers.didChange(path) + } + protected val interactiveSemanticdbs: InteractiveSemanticdbs = { val javaInteractiveSemanticdb = maybeJdkVersion.map(jdkVersion => JavaInteractiveSemanticdb.create(folder, buildTargets, jdkVersion) @@ -866,7 +871,7 @@ abstract class MetalsLspService( val path = params.getTextDocument.getUri.toAbsolutePath buffers.put(path, change.getText) diagnostics.didChange(path) - + compilers.didChange(path) parseTrees(path).asJava } } diff --git a/metals/src/main/scala/scala/meta/internal/metals/OutlineFilesProvider.scala b/metals/src/main/scala/scala/meta/internal/metals/OutlineFilesProvider.scala new file mode 100644 index 00000000000..af7876d197e --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/OutlineFilesProvider.scala @@ -0,0 +1,198 @@ +package scala.meta.internal.metals + +import java.util.Optional +import java.util.concurrent.atomic.AtomicBoolean +import java.{util => ju} + +import scala.collection.concurrent.TrieMap + +import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.io.AbsolutePath +import scala.meta.pc.VirtualFileParams +import scala.meta.pc.{OutlineFiles => JOutlineFiles} + +import ch.epfl.scala.bsp4j.BuildTargetIdentifier + +class OutlineFilesProvider( + buildTargets: BuildTargets, + buffers: Buffers, +) { + private val outlineFiles = + new TrieMap[BuildTargetIdentifier, BuildTargetOutlineFilesProvider]() + + def shouldRestartPc( + id: BuildTargetIdentifier, + reason: PcRestartReason, + ): Boolean = { + reason match { + case DidCompile(true) => true + case _ => + outlineFiles.get(id) match { + case Some(provider) => + // if it was never compiled successfully by the build server + // we don't restart pc not to lose information from outline compile + provider.wasSuccessfullyCompiledByBuildServer + case None => true + } + } + } + + def onDidCompile(id: BuildTargetIdentifier, wasSuccessful: Boolean): Unit = { + outlineFiles.get(id) match { + case Some(provider) => + if (wasSuccessful) provider.successfulCompilation() + case None => + for { + scalaTarget <- buildTargets.scalaTarget(id) + // we don't perform outline compilation for Scala 3 + if (!ScalaVersions.isScala3Version(scalaTarget.scalaVersion)) + } outlineFiles.putIfAbsent( + id, + new BuildTargetOutlineFilesProvider( + buildTargets, + buffers, + id, + wasSuccessful, + ), + ) + } + } + + def didChange(id: String, path: AbsolutePath): Unit = + buildTargetId(id).foreach(didChange(_, path)) + + def didChange(id: BuildTargetIdentifier, path: AbsolutePath): Unit = { + for { + provider <- outlineFiles.get(id) + } provider.didChange(path) + } + + def getOutlineFiles(id: String): Optional[JOutlineFiles] = + getOutlineFiles(buildTargetId(id)) + + def getOutlineFiles( + buildTargetId: Option[BuildTargetIdentifier] + ): Optional[JOutlineFiles] = { + val res: Option[JOutlineFiles] = + for { + id <- buildTargetId + provider <- outlineFiles.get(id) + outlineFiles <- provider.outlineFiles() + } yield outlineFiles + res.asJava + } + + def enrichWithOutlineFiles( + buildTargetId: Option[BuildTargetIdentifier] + )(vFile: CompilerVirtualFileParams): CompilerVirtualFileParams = { + val optOutlineFiles = + for { + id <- buildTargetId + provider <- outlineFiles.get(id) + outlineFiles <- provider.outlineFiles() + } yield outlineFiles + + optOutlineFiles + .map(outlineFiles => vFile.copy(outlineFiles = Optional.of(outlineFiles))) + .getOrElse(vFile) + } + + def enrichWithOutlineFiles( + path: AbsolutePath + )(vFile: CompilerVirtualFileParams): CompilerVirtualFileParams = + enrichWithOutlineFiles(buildTargets.inferBuildTarget(path))(vFile) + + def clear(): Unit = { + outlineFiles.clear() + } + + private def buildTargetId(id: String): Option[BuildTargetIdentifier] = + Option(id).filter(_.nonEmpty).map(new BuildTargetIdentifier(_)) +} + +class BuildTargetOutlineFilesProvider( + buildTargets: BuildTargets, + buffers: Buffers, + id: BuildTargetIdentifier, + wasCompilationSuccessful: Boolean, +) { + private val changedDocuments = + ConcurrentHashSet.empty[AbsolutePath] + + private val wasAllOutlined: AtomicBoolean = + new AtomicBoolean(false) + + private val wasSuccessfullyCompiled: AtomicBoolean = + new AtomicBoolean(wasCompilationSuccessful) + + def wasSuccessfullyCompiledByBuildServer: Boolean = + wasSuccessfullyCompiled.get() + + def successfulCompilation(): Unit = { + wasSuccessfullyCompiled.set(true) + changedDocuments.clear() + } + + def didChange(path: AbsolutePath): Boolean = + changedDocuments.add(path) + + def outlineFiles(): Option[OutlineFiles] = { + if (!wasSuccessfullyCompiled.get() && !wasAllOutlined.getAndSet(true)) { + // initial outline compilation that is a substitute for build server compilation + val allFiles = + buildTargets + .buildTargetSources(id) + .flatMap(_.listRecursive.toList) + + if (allFiles.size > OutlineFilesProvider.maxOutlineFiles) { + // too many files to outline using pc + None + } else { + val inputs = allFiles.flatMap(toVirtualFileParams(_)).toList.asJava + Some( + OutlineFiles( + inputs, + isFirstCompileSubstitute = true, + ) + ) + } + } else { + changedDocuments.asScala.toList.flatMap( + toVirtualFileParams + ) match { + case Nil => None + case files => + Some( + OutlineFiles( + files.asJava, + isFirstCompileSubstitute = false, + ) + ) + } + } + } + + private def toVirtualFileParams( + path: AbsolutePath + ): Option[VirtualFileParams] = + buffers.get(path).orElse(path.readTextOpt).map { text => + CompilerVirtualFileParams( + path.toURI, + text, + ) + } + +} + +sealed trait PcRestartReason +case object InverseDependency extends PcRestartReason +case class DidCompile(wasSuccess: Boolean) extends PcRestartReason + +case class OutlineFiles( + files: ju.List[VirtualFileParams], + isFirstCompileSubstitute: Boolean = false, +) extends JOutlineFiles + +object OutlineFilesProvider { + val maxOutlineFiles = 300 +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala index 72e3f72f665..4f82ee779b8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/ProjectMetalsLspService.scala @@ -574,8 +574,7 @@ class ProjectMetalsLspService( !buffers.contains(path) ) { event.eventType match { - case EventType.CreateOrModify => - buildTargets.onCreate(path) + case EventType.CreateOrModify => onCreate(path) case _ => } onChange(List(path)).asJava diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/DefinitionResult.java b/mtags-interfaces/src/main/java/scala/meta/pc/DefinitionResult.java index a8026e8a62d..bab22cac17f 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/DefinitionResult.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/DefinitionResult.java @@ -6,4 +6,4 @@ public interface DefinitionResult { String symbol(); List locations(); -} \ No newline at end of file +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/OutlineFiles.java b/mtags-interfaces/src/main/java/scala/meta/pc/OutlineFiles.java new file mode 100644 index 00000000000..28cd919872b --- /dev/null +++ b/mtags-interfaces/src/main/java/scala/meta/pc/OutlineFiles.java @@ -0,0 +1,16 @@ +package scala.meta.pc; +import java.util.List; + +public interface OutlineFiles { + + /** + * Will this outline compilation be substitute for build server's compilation. + * Used if the first compilation using build server is unsuccessful. + */ + boolean isFirstCompileSubstitute(); + + /** + * Files that should be outline compiled before calculating result. + */ + List files(); +} diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java index 095f681b537..4b7eebdbf03 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/PresentationCompiler.java @@ -161,7 +161,7 @@ public abstract CompletableFuture> convertToNamedArguments(Offset List argIndices); /** - * The text contents of the fiven file changed. + * The text contents of the given file changed. */ public abstract CompletableFuture> didChange(VirtualFileParams params); @@ -196,6 +196,14 @@ public CompletableFuture> info(String symbol) { */ public abstract CompletableFuture semanticdbTextDocument(URI filename, String code); + /** + * Returns the Protobuf byte array representation of a SemanticDB + * TextDocument for the given source. + */ + public CompletableFuture semanticdbTextDocument(VirtualFileParams params) { + return semanticdbTextDocument(params.uri(), params.text()); + } + /** * Return the selections ranges for the given positions. */ @@ -324,4 +332,8 @@ public abstract PresentationCompiler newInstance(String buildTargetIdentifier, L */ public abstract String scalaVersion(); + public String buildTargetId() { + return ""; + } + } diff --git a/mtags-interfaces/src/main/java/scala/meta/pc/VirtualFileParams.java b/mtags-interfaces/src/main/java/scala/meta/pc/VirtualFileParams.java index 904cb7da0fd..5a534e82203 100644 --- a/mtags-interfaces/src/main/java/scala/meta/pc/VirtualFileParams.java +++ b/mtags-interfaces/src/main/java/scala/meta/pc/VirtualFileParams.java @@ -1,6 +1,7 @@ package scala.meta.pc; import java.net.URI; +import java.util.Optional; /** * Parameters for a presentation compiler request at a given offset in a single source file. @@ -21,6 +22,14 @@ public interface VirtualFileParams { */ CancelToken token(); + /** + * Information about files that changed since last compilation + * and should be outline compiled. + */ + default Optional outlineFiles() { + return Optional.empty(); + } + default void checkCanceled() { token().checkCanceled(); } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerInlayHintsParams.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerInlayHintsParams.scala index cebaf448c70..a6b29dde488 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerInlayHintsParams.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerInlayHintsParams.scala @@ -1,9 +1,11 @@ package scala.meta.internal.metals import java.net.URI +import java.util.Optional import scala.meta.pc.CancelToken import scala.meta.pc.InlayHintsParams +import scala.meta.pc.OutlineFiles case class CompilerInlayHintsParams( rangeParams: CompilerRangeParams, @@ -28,4 +30,6 @@ case class CompilerInlayHintsParams( implicitParameters = implicitParameters ) } + + override def outlineFiles(): Optional[OutlineFiles] = rangeParams.outlineFiles } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala index 71b3223d226..7bdf4b34558 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerOffsetParams.scala @@ -1,30 +1,57 @@ package scala.meta.internal.metals import java.net.URI +import java.util.Optional import scala.meta.pc.CancelToken import scala.meta.pc.OffsetParams +import scala.meta.pc.OutlineFiles import scala.meta.pc.RangeParams case class CompilerOffsetParams( uri: URI, text: String, offset: Int, - token: CancelToken = EmptyCancelToken + token: CancelToken, + override val outlineFiles: Optional[OutlineFiles] ) extends OffsetParams +object CompilerOffsetParams { + def apply( + uri: URI, + text: String, + offset: Int, + token: CancelToken = EmptyCancelToken + ): CompilerOffsetParams = + CompilerOffsetParams(uri, text, offset, token, Optional.empty()) +} + case class CompilerRangeParams( uri: URI, text: String, offset: Int, endOffset: Int, - token: CancelToken = EmptyCancelToken + token: CancelToken, + override val outlineFiles: Optional[OutlineFiles] ) extends RangeParams { + def toCompilerOffsetParams: CompilerOffsetParams = CompilerOffsetParams( uri, text, offset, - token + token, + outlineFiles ) } + +object CompilerRangeParams { + def apply( + uri: URI, + text: String, + offset: Int, + endOffset: Int, + token: CancelToken = EmptyCancelToken + ): CompilerRangeParams = + CompilerRangeParams(uri, text, offset, endOffset, token, Optional.empty()) +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerVirtualFileParams.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerVirtualFileParams.scala index 954d3b0514a..ea4512f40e3 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerVirtualFileParams.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/CompilerVirtualFileParams.scala @@ -1,12 +1,24 @@ package scala.meta.internal.metals import java.net.URI +import java.util.Optional import scala.meta.pc.CancelToken +import scala.meta.pc.OutlineFiles import scala.meta.pc.VirtualFileParams case class CompilerVirtualFileParams( uri: URI, text: String, - token: CancelToken = EmptyCancelToken + token: CancelToken, + override val outlineFiles: Optional[OutlineFiles] ) extends VirtualFileParams + +object CompilerVirtualFileParams { + def apply( + uri: URI, + text: String, + token: CancelToken = EmptyCancelToken + ): CompilerVirtualFileParams = + CompilerVirtualFileParams(uri, text, token, Optional.empty()) +} diff --git a/mtags-shared/src/main/scala/scala/meta/internal/metals/Compression.scala b/mtags-shared/src/main/scala/scala/meta/internal/metals/Compression.scala index cc48fe816d4..136f2d3f9c9 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/metals/Compression.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/metals/Compression.scala @@ -55,7 +55,7 @@ object Compression { pkg = in.readString() case 0 => isDone = true - case _ => + case tag => in.skipField(tag) } } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerWrapper.scala b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerWrapper.scala index 20dbeb0631f..04246adf40e 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerWrapper.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/pc/CompilerWrapper.scala @@ -1,5 +1,7 @@ package scala.meta.internal.pc +import scala.meta.pc.VirtualFileParams + trait CompilerWrapper[Reporter, Compiler] { def resetReporter(): Unit @@ -12,7 +14,10 @@ trait CompilerWrapper[Reporter, Compiler] { def stop(): Unit + def compiler(params: VirtualFileParams): Compiler = compiler() + def compiler(): Compiler def presentationCompilerThread: Option[Thread] + } diff --git a/mtags/src/main/scala-2.11/scala/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.11/scala/meta/internal/pc/Compat.scala index b3b68c9534e..e4e177109fc 100644 --- a/mtags/src/main/scala-2.11/scala/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.11/scala/meta/internal/pc/Compat.scala @@ -3,6 +3,8 @@ package scala.meta.internal.pc import scala.tools.nsc.reporters.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.pc.OutlineFiles + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = { val dealiased = tpe.dealiasWiden @@ -19,4 +21,8 @@ trait Compat { this: MetalsGlobal => def isAliasCompletion(m: Member): Boolean = false def constantType(c: ConstantType): ConstantType = c + + def runOutline(files: OutlineFiles): Unit = { + // no outline compilation for 2.11 + } } diff --git a/mtags/src/main/scala-2.12/scala/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.12/scala/meta/internal/pc/Compat.scala index da02d75b0f9..8532ab9e00d 100644 --- a/mtags/src/main/scala-2.12/scala/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.12/scala/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc -import scala.tools.nsc.reporters.Reporter +import java.{util => ju} + +import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrSamArgTypes(tpe) @@ -16,4 +22,33 @@ trait Compat { this: MetalsGlobal => def isAliasCompletion(m: Member): Boolean = false def constantType(c: ConstantType): ConstantType = c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } } diff --git a/mtags/src/main/scala-2.13.5/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.13.5/meta/internal/pc/Compat.scala index 988bd8f2db8..5f2527b504d 100644 --- a/mtags/src/main/scala-2.13.5/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.13.5/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc +import java.{util => ju} + import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrPfOrSamArgTypes(tpe) @@ -19,4 +25,33 @@ trait Compat { this: MetalsGlobal => def constantType(c: ConstantType): ConstantType = if (c.value.isSuitableLiteralType) LiteralType(c.value) else c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } } diff --git a/mtags/src/main/scala-2.13.6/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.13.6/meta/internal/pc/Compat.scala index 988bd8f2db8..5f2527b504d 100644 --- a/mtags/src/main/scala-2.13.6/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.13.6/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc +import java.{util => ju} + import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrPfOrSamArgTypes(tpe) @@ -19,4 +25,33 @@ trait Compat { this: MetalsGlobal => def constantType(c: ConstantType): ConstantType = if (c.value.isSuitableLiteralType) LiteralType(c.value) else c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } } diff --git a/mtags/src/main/scala-2.13.7/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.13.7/meta/internal/pc/Compat.scala index 988bd8f2db8..1d941c7ebc2 100644 --- a/mtags/src/main/scala-2.13.7/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.13.7/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc +import java.{util => ju} + import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrPfOrSamArgTypes(tpe) @@ -19,4 +25,34 @@ trait Compat { this: MetalsGlobal => def constantType(c: ConstantType): ConstantType = if (c.value.isSuitableLiteralType) LiteralType(c.value) else c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } + } diff --git a/mtags/src/main/scala-2.13.8/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.13.8/meta/internal/pc/Compat.scala index 988bd8f2db8..5f2527b504d 100644 --- a/mtags/src/main/scala-2.13.8/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.13.8/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc +import java.{util => ju} + import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrPfOrSamArgTypes(tpe) @@ -19,4 +25,33 @@ trait Compat { this: MetalsGlobal => def constantType(c: ConstantType): ConstantType = if (c.value.isSuitableLiteralType) LiteralType(c.value) else c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } } diff --git a/mtags/src/main/scala-2.13/scala/meta/internal/pc/Compat.scala b/mtags/src/main/scala-2.13/scala/meta/internal/pc/Compat.scala index a4eeed4b00c..0b4fe757b10 100644 --- a/mtags/src/main/scala-2.13/scala/meta/internal/pc/Compat.scala +++ b/mtags/src/main/scala-2.13/scala/meta/internal/pc/Compat.scala @@ -1,8 +1,14 @@ package scala.meta.internal.pc +import java.{util => ju} + import scala.reflect.internal.Reporter import scala.tools.nsc.reporters.StoreReporter +import scala.meta.internal.jdk.CollectionConverters._ +import scala.meta.pc.OutlineFiles +import scala.meta.pc.VirtualFileParams + trait Compat { this: MetalsGlobal => def metalsFunctionArgTypes(tpe: Type): List[Type] = definitions.functionOrPfOrSamArgTypes(tpe) @@ -23,4 +29,33 @@ trait Compat { this: MetalsGlobal => def constantType(c: ConstantType): ConstantType = if (c.value.isSuitableLiteralType) LiteralType(c.value) else c + + def runOutline(files: OutlineFiles): Unit = { + this.settings.Youtline.value = true + runOutline(files.files) + if (files.isFirstCompileSubstitute()) { + // if first compilation substitute we compile all files twice + // first to emit symbols, second so signatures have information about those symbols + // this isn't a perfect strategy but much better than single compile + runOutline(files.files, forceNewUnit = true) + } + this.settings.Youtline.value = false + } + + private def runOutline( + files: ju.List[VirtualFileParams], + forceNewUnit: Boolean = false + ): Unit = { + files.asScala.foreach { params => + val unit = this.addCompilationUnit( + params.text(), + params.uri.toString(), + cursor = None, + isOutline = true, + forceNew = forceNewUnit + ) + this.typeCheck(unit) + this.richCompilationCache.put(params.uri().toString(), unit) + } + } } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/AutoImportsProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/AutoImportsProvider.scala index d891c06f88b..4ef9095584c 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/AutoImportsProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/AutoImportsProvider.scala @@ -40,8 +40,10 @@ final class AutoImportsProvider( false } - val visitor = new CompilerSearchVisitor(context, visit) + compiler.searchOutline(visit, name) + val visitor = + new CompilerSearchVisitor(context, visit) search.search(name, buildTargetIdentifier, visitor) def isInImportTree: Boolean = lastVisitedParentTrees match { diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala index 8171bab24c3..2f9a043f83e 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/MetalsGlobal.scala @@ -5,6 +5,7 @@ import java.util import java.util.logging.Logger import java.{util => ju} +import scala.collection.concurrent.TrieMap import scala.collection.mutable import scala.language.implicitConversions import scala.reflect.internal.util.Position @@ -66,6 +67,12 @@ class MetalsGlobal( val logger: Logger = Logger.getLogger(classOf[MetalsGlobal].getName) + val richCompilationCache: TrieMap[String, RichCompilationUnit] = + TrieMap.empty[String, RichCompilationUnit] + + // for those paths units were fully compiled (not just outlined) + val fullyCompiled: mutable.Set[String] = mutable.Set.empty[String] + class MetalsInteractiveAnalyzer(val global: compiler.type) extends InteractiveAnalyzer { @@ -100,6 +107,7 @@ class MetalsGlobal( else super.pluginsMacroExpand(typer, expandee, mode, pt) } } + override lazy val analyzer: this.MetalsInteractiveAnalyzer = new MetalsInteractiveAnalyzer(compiler) @@ -165,6 +173,16 @@ class MetalsGlobal( } } + /** + * If we run outline compilation on a file this means it's fresher + * and we don't want to get older data from last compilation. + * + * @param symbol symbol to check + */ + def isOutlinedFile(path: Path): Boolean = { + richCompilationCache.contains(path.toUri().toString()) + } + def workspaceSymbolListMembers( query: String, pos: Position, @@ -172,7 +190,7 @@ class MetalsGlobal( ): SymbolSearch.Result = { def isRelevantWorkspaceSymbol(sym: Symbol): Boolean = - sym.isStatic + sym.isStatic && !sym.isStale lazy val isInStringInterpolation = { lastVisitedParentTrees match { @@ -185,26 +203,30 @@ class MetalsGlobal( } } + def visitMember(sym: Symbol) = { + if (isRelevantWorkspaceSymbol(sym)) + visit { + if (isInStringInterpolation) + new WorkspaceInterpolationMember( + sym, + Nil, + edit => s"{$edit}", + None + ) + else + new WorkspaceMember(sym) + } + else false + } + if (query.isEmpty) SymbolSearch.Result.INCOMPLETE else { val context = doLocateContext(pos) val visitor = new CompilerSearchVisitor( context, - sym => - if (isRelevantWorkspaceSymbol(sym)) - visit { - if (isInStringInterpolation) - new WorkspaceInterpolationMember( - sym, - Nil, - edit => s"{$edit}", - None - ) - else - new WorkspaceMember(sym) - } - else false + visitMember ) + searchOutline(visitMember, query) search.search(query, buildTargetIdentifier, visitor) } } @@ -214,12 +236,17 @@ class MetalsGlobal( pos: Position ): List[Member] = { val buffer = mutable.ListBuffer.empty[Member] + val isSeen = mutable.Set.empty[String] workspaceSymbolListMembers( query, pos, mem => { - buffer.append(mem) - true + val id = mem.sym.fullName + if (!isSeen(id)) { + isSeen += id + buffer.append(mem) + true + } else true } ) buffer.toList @@ -637,7 +664,9 @@ class MetalsGlobal( code: String, filename: String, cursor: Option[Int], - cursorName: String = CURSOR + cursorName: String = CURSOR, + isOutline: Boolean = false, + forceNew: Boolean = false ): RichCompilationUnit = { val codeWithCursor = cursor match { case Some(offset) => @@ -656,10 +685,16 @@ class MetalsGlobal( if util.Arrays.equals( value.source.content, richUnit.source.content - ) => + ) && (isOutline || fullyCompiled(filename)) && !forceNew => value case _ => unitOfFile(richUnit.source.file) = richUnit + if (!isOutline) { + fullyCompiled += filename + } else { + fullyCompiled -= filename + } + richUnit } } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/PcSemanticTokensProvider.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/PcSemanticTokensProvider.scala index b7cd7c1ef7d..28b2cdfd1cf 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/PcSemanticTokensProvider.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/PcSemanticTokensProvider.scala @@ -69,7 +69,7 @@ final class PcSemanticTokensProvider( } } } - def provide(): List[Node] = + def provide(): List[Node] = { Collector .result() .flatten @@ -77,6 +77,7 @@ final class PcSemanticTokensProvider( if (n1.start() == n2.start()) n1.end() < n2.end() else n1.start() < n2.start() ) + } def makeNode( sym: Collector.compiler.Symbol, diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala index 3d82a6783c1..4184176b534 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaCompilerAccess.scala @@ -9,10 +9,17 @@ import scala.util.control.NonFatal import scala.meta.internal.metals.ReportContext import scala.meta.pc.PresentationCompilerConfig +import scala.meta.pc.VirtualFileParams class ScalaCompilerWrapper(global: MetalsGlobal) extends CompilerWrapper[StoreReporter, MetalsGlobal] { + override def compiler(params: VirtualFileParams): MetalsGlobal = { + if (params.outlineFiles().isPresent()) { + global.runOutline(params.outlineFiles().get()) + } + global + } override def compiler(): MetalsGlobal = global override def resetReporter(): Unit = global.reporter.reset() diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala index d6417afc858..8659ca01df8 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -131,7 +131,7 @@ case class ScalaPresentationCompiler( compilerAccess.shutdown() } - override def restart(): Unit = { + def restart(): Unit = { compilerAccess.shutdownCurrentCompiler() } @@ -155,7 +155,14 @@ case class ScalaPresentationCompiler( CompletableFuture.completedFuture(Nil.asJava) } - def didClose(uri: URI): Unit = {} + def didClose(uri: URI): Unit = { + compilerAccess.withNonInterruptableCompiler(None)( + (), + EmptyCancelToken + ) { pc => + pc.compiler().richCompilationCache.remove(uri.toString()) + } + } override def semanticTokens( params: VirtualFileParams @@ -166,7 +173,7 @@ case class ScalaPresentationCompiler( params.token ) { pc => new PcSemanticTokensProvider( - pc.compiler(), + pc.compiler(params), params ).provide().asJava } @@ -190,13 +197,15 @@ case class ScalaPresentationCompiler( override def complete( params: OffsetParams - ): CompletableFuture[CompletionList] = + ): CompletableFuture[CompletionList] = { compilerAccess.withInterruptableCompiler(Some(params))( EmptyCompletionList(), params.token ) { pc => - new CompletionProvider(pc.compiler(), params).completions() + new CompletionProvider(pc.compiler(params), params) + .completions() } + } override def implementAbstractMembers( params: OffsetParams @@ -206,7 +215,8 @@ case class ScalaPresentationCompiler( empty, params.token ) { pc => - new CompletionProvider(pc.compiler(), params).implementAll() + new CompletionProvider(pc.compiler(params), params) + .implementAll() } } @@ -218,7 +228,9 @@ case class ScalaPresentationCompiler( empty, params.token ) { pc => - new InferredTypeProvider(pc.compiler(), params).inferredTypeEdits().asJava + new InferredTypeProvider(pc.compiler(params), params) + .inferredTypeEdits() + .asJava } } @@ -228,7 +240,10 @@ case class ScalaPresentationCompiler( val empty: Either[String, List[TextEdit]] = Right(List()) (compilerAccess .withInterruptableCompiler(Some(params))(empty, params.token) { pc => - new PcInlineValueProviderImpl(pc.compiler(), params).getInlineTextEdits + new PcInlineValueProviderImpl( + pc.compiler(params), + params + ).getInlineTextEdits }) .thenApply { case Right(edits: List[TextEdit]) => edits.asJava @@ -244,7 +259,7 @@ case class ScalaPresentationCompiler( compilerAccess.withInterruptableCompiler(Some(range))(empty, range.token) { pc => new ExtractMethodProvider( - pc.compiler(), + pc.compiler(range), range, extractionPos ).extractMethod.asJava @@ -259,7 +274,7 @@ case class ScalaPresentationCompiler( (compilerAccess .withInterruptableCompiler(Some(params))(empty, params.token) { pc => new ConvertToNamedArgumentsProvider( - pc.compiler(), + pc.compiler(params), params, argIndices.asScala.map(_.toInt).toSet ).convertToNamedArguments @@ -279,7 +294,9 @@ case class ScalaPresentationCompiler( List.empty[AutoImportsResult].asJava, params.token ) { pc => - new AutoImportsProvider(pc.compiler(), name, params).autoImports().asJava + new AutoImportsProvider(pc.compiler(params), name, params) + .autoImports() + .asJava } override def getTasty( @@ -308,7 +325,10 @@ case class ScalaPresentationCompiler( compilerAccess.withNonInterruptableCompiler(Some(params))( new SignatureHelp(), params.token - ) { pc => new SignatureHelpProvider(pc.compiler()).signatureHelp(params) } + ) { pc => + new SignatureHelpProvider(pc.compiler(params)) + .signatureHelp(params) + } override def prepareRename( params: OffsetParams @@ -317,7 +337,9 @@ case class ScalaPresentationCompiler( Optional.empty[Range](), params.token ) { pc => - new PcRenameProvider(pc.compiler(), params, None).prepareRename().asJava + new PcRenameProvider(pc.compiler(params), params, None) + .prepareRename() + .asJava } override def rename( @@ -328,7 +350,11 @@ case class ScalaPresentationCompiler( List[TextEdit]().asJava, params.token ) { pc => - new PcRenameProvider(pc.compiler(), params, Some(name)).rename().asJava + new PcRenameProvider( + pc.compiler(params), + params, + Some(name) + ).rename().asJava } override def hover( @@ -339,7 +365,11 @@ case class ScalaPresentationCompiler( params.token ) { pc => Optional.ofNullable( - new HoverProvider(pc.compiler(), params, config.hoverContentType()) + new HoverProvider( + pc.compiler(params), + params, + config.hoverContentType() + ) .hover() .orNull ) @@ -350,7 +380,10 @@ case class ScalaPresentationCompiler( compilerAccess.withNonInterruptableCompiler(Some(params))( DefinitionResultImpl.empty, params.token - ) { pc => new PcDefinitionProvider(pc.compiler(), params).definition() } + ) { pc => + new PcDefinitionProvider(pc.compiler(params), params) + .definition() + } } override def info( @@ -374,7 +407,10 @@ case class ScalaPresentationCompiler( compilerAccess.withNonInterruptableCompiler(Some(params))( DefinitionResultImpl.empty, params.token - ) { pc => new PcDefinitionProvider(pc.compiler(), params).typeDefinition() } + ) { pc => + new PcDefinitionProvider(pc.compiler(params), params) + .typeDefinition() + } } override def documentHighlight( @@ -384,7 +420,9 @@ case class ScalaPresentationCompiler( List.empty[DocumentHighlight].asJava, params.token() ) { pc => - new PcDocumentHighlightProvider(pc.compiler(), params).highlights().asJava + new PcDocumentHighlightProvider(pc.compiler(params), params) + .highlights() + .asJava } override def semanticdbTextDocument( @@ -392,15 +430,21 @@ case class ScalaPresentationCompiler( code: String ): CompletableFuture[Array[Byte]] = { val virtualFile = CompilerVirtualFileParams(fileUri, code) + semanticdbTextDocument(virtualFile) + } + + override def semanticdbTextDocument( + virtualFile: VirtualFileParams + ): CompletableFuture[Array[Byte]] = { compilerAccess.withInterruptableCompiler(Some(virtualFile))( Array.emptyByteArray, EmptyCancelToken ) { pc => new SemanticdbTextDocumentProvider( - pc.compiler(), + pc.compiler(virtualFile), config.semanticdbCompilerOptions().asScala.toList ) - .textDocument(fileUri, code) + .textDocument(virtualFile.uri(), virtualFile.text()) .toByteArray } } @@ -419,6 +463,8 @@ case class ScalaPresentationCompiler( } } + override def buildTargetId(): String = buildTargetIdentifier + def newCompiler(): MetalsGlobal = { val classpath = this.classpath.mkString(File.pathSeparator) val vd = new VirtualDirectory("(memory)", None) @@ -477,4 +523,5 @@ case class ScalaPresentationCompiler( .toList .asJava } + } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/SymbolSearchCandidate.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/SymbolSearchCandidate.scala index d377bf65599..70e321f7173 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/SymbolSearchCandidate.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/SymbolSearchCandidate.scala @@ -1,5 +1,7 @@ package scala.meta.internal.pc +import java.nio.file.Path + import scala.meta.internal.metals.Fuzzy import scala.meta.internal.semanticdb.Scala._ @@ -18,7 +20,8 @@ object SymbolSearchCandidate { override def packageString: String = pkg override def termCharacter: Char = '$' } - final case class Workspace(symbol: String) extends SymbolSearchCandidate { + final case class Workspace(symbol: String, path: Path) + extends SymbolSearchCandidate { def nameString: String = symbol override def packageString: String = { def loop(s: String): String = { diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/WorkspaceSymbolSearch.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/WorkspaceSymbolSearch.scala index 9f4ab5e53bb..8e42984fe5b 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/WorkspaceSymbolSearch.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/WorkspaceSymbolSearch.scala @@ -2,6 +2,7 @@ package scala.meta.internal.pc import java.nio.file.Path +import scala.annotation.tailrec import scala.util.control.NonFatal import scala.meta.pc.PcSymbolKind @@ -10,7 +11,40 @@ import scala.meta.pc.SymbolSearchVisitor import org.eclipse.{lsp4j => l} -trait WorkspaceSymbolSearch { this: MetalsGlobal => +trait WorkspaceSymbolSearch { compiler: MetalsGlobal => + + def searchOutline( + visitMember: Symbol => Boolean, + query: String + ): Unit = { + + def traverseUnit(unit: RichCompilationUnit) = { + @tailrec + def loop(trees: List[Tree]): Unit = { + trees match { + case Nil => + case (tree: MemberDef) :: tail => + val sym = tree.symbol + def matches = if (sym.isType) + CompletionFuzzy.matchesSubCharacters(query, sym.name.toString()) + else CompletionFuzzy.matches(query, sym.name.toString()) + if (sym != null && sym.exists && matches) { + try { + visitMember(sym) + } catch { + case _: Throwable => + // with outline compiler there might be situations when things fail + } + } + loop(tree.children ++ tail) + case tree :: tail => + loop(tree.children ++ tail) + } + } + loop(List(unit.body)) + } + compiler.richCompilationCache.values.foreach(traverseUnit) + } def info(symbol: String): Option[PcSymbolInformation] = { val index = symbol.lastIndexOf("/") @@ -97,6 +131,7 @@ trait WorkspaceSymbolSearch { this: MetalsGlobal => ) extends SymbolSearchVisitor { def visit(top: SymbolSearchCandidate): Int = { + var added = 0 for { sym <- loadSymbolFromClassfile(top) @@ -117,7 +152,7 @@ trait WorkspaceSymbolSearch { this: MetalsGlobal => kind: l.SymbolKind, range: l.Range ): Int = { - visit(SymbolSearchCandidate.Workspace(symbol)) + visit(SymbolSearchCandidate.Workspace(symbol, path)) } def shouldVisitPackage(pkg: String): Boolean = @@ -161,10 +196,13 @@ trait WorkspaceSymbolSearch { this: MetalsGlobal => } } members.filter(sym => isAccessible(sym)) - case SymbolSearchCandidate.Workspace(symbol) => + case SymbolSearchCandidate.Workspace(symbol, path) + if !compiler.isOutlinedFile(path) => val gsym = inverseSemanticdbSymbol(symbol) if (isAccessible(gsym)) gsym :: Nil else Nil + case _ => + Nil } } catch { case NonFatal(_) => Nil diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala index e1fe6415fb4..9119270d371 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/ScalaPresentationCompiler.scala @@ -446,4 +446,6 @@ case class ScalaPresentationCompiler( override def isLoaded() = compilerAccess.isLoaded() + override def buildTargetId(): String = buildTargetIdentifier + end ScalaPresentationCompiler diff --git a/mtags/src/main/scala/scala/meta/internal/metals/OffsetParamsUtils.scala b/mtags/src/main/scala/scala/meta/internal/metals/OffsetParamsUtils.scala index 8c449b181df..842e1e71a89 100644 --- a/mtags/src/main/scala/scala/meta/internal/metals/OffsetParamsUtils.scala +++ b/mtags/src/main/scala/scala/meta/internal/metals/OffsetParamsUtils.scala @@ -3,11 +3,13 @@ package scala.meta.internal.metals import java.net.URI import java.net.URISyntaxException import java.nio.file.Paths +import java.util.Optional import scala.meta.inputs.Position import scala.meta.internal.inputs.XtensionInputSyntaxStructure import scala.meta.pc.CancelToken import scala.meta.pc.OffsetParams +import scala.meta.pc.OutlineFiles trait OffsetParamsUtils { protected def syntaxURI(pos: Position): URI = { @@ -25,34 +27,48 @@ trait OffsetParamsUtils { object CompilerRangeParamsUtils extends OffsetParamsUtils { - def offsetOrRange(pos: Position, token: CancelToken): OffsetParams = { + def offsetOrRange( + pos: Position, + token: CancelToken, + outlineFiles: Optional[OutlineFiles] = Optional.empty() + ): OffsetParams = { if (pos.start == pos.end) - CompilerOffsetParamsUtils.fromPos(pos, token) + CompilerOffsetParamsUtils.fromPos(pos, token, outlineFiles) else - CompilerRangeParamsUtils.fromPos(pos, token) + CompilerRangeParamsUtils.fromPos(pos, token, outlineFiles) } - def fromPos(pos: Position, token: CancelToken): CompilerRangeParams = { + def fromPos( + pos: Position, + token: CancelToken, + outlineFiles: Optional[OutlineFiles] = Optional.empty() + ): CompilerRangeParams = { val uri = syntaxURI(pos) CompilerRangeParams( uri, pos.input.text, pos.start, pos.end, - token + token, + outlineFiles ) } } object CompilerOffsetParamsUtils extends OffsetParamsUtils { - def fromPos(pos: Position, token: CancelToken): CompilerOffsetParams = { + def fromPos( + pos: Position, + token: CancelToken, + outlineFiles: Optional[OutlineFiles] = Optional.empty() + ): CompilerOffsetParams = { val uri = syntaxURI(pos) CompilerOffsetParams( uri, pos.input.text, pos.start, - token + token, + outlineFiles ) } } diff --git a/tests/unit/src/test/scala/tests/CompilersLspSuite.scala b/tests/unit/src/test/scala/tests/CompilersLspSuite.scala index 3d54d056a42..416781d961e 100644 --- a/tests/unit/src/test/scala/tests/CompilersLspSuite.scala +++ b/tests/unit/src/test/scala/tests/CompilersLspSuite.scala @@ -2,6 +2,9 @@ package tests import scala.concurrent.Future +import scala.meta.internal.metals.codeactions.CreateNewSymbol +import scala.meta.internal.metals.codeactions.ImportMissingSymbol + class CompilersLspSuite extends BaseCompletionLspSuite("compilers") { test("reset-pc") { cleanWorkspace() @@ -55,4 +58,347 @@ class CompilersLspSuite extends BaseCompletionLspSuite("compilers") { ) } yield () } + + test("non-compiling") { + cleanWorkspace() + for { + _ <- initialize( + """/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + |package a + |class A { + | // @@ + | def completeThisUniqueName() = 42 + |} + |/a/src/main/scala/b/B.scala + |package b + |object UniqueObject { + | def completeThisUniqueName() = 42 + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/A.scala") + _ <- server.didOpen("a/src/main/scala/b/B.scala") + _ = assertNoDiagnostics() + // break the file and add a new method, should show methods + _ <- + server.didChange("a/src/main/scala/b/B.scala") { _ => + """|package b + |object UniqueObject{ + | def completeThisUniqueName() = 42 + | def completeThisUniqueName2(): String = 42 + |}""".stripMargin + } + _ <- assertCompletion( + "b.UniqueObject.completeThisUniqueNa@@", + """|completeThisUniqueName(): Int + |completeThisUniqueName2(): String""".stripMargin, + ) + // make sure autoimports are properly suggested + _ <- assertCompletionEdit( + "UniqueObject@@", + """|package a + | + |import b.UniqueObject + |class A { + | UniqueObject + | def completeThisUniqueName() = 42 + |} + |""".stripMargin, + ) + // change the name of the object and test again + _ <- + server.didChange("a/src/main/scala/b/B.scala") { _ => + """|package b + |object UniqueObjectOther{ + | def completeThisUniqueName() = 42 + | def completeThisUniqueName2(): String = 42 + |}""".stripMargin + } + _ <- assertCompletion( + "b.UniqueObjectOther.completeThisUniqueNa@@", + """|completeThisUniqueName(): Int + |completeThisUniqueName2(): String""".stripMargin, + ) + // make sure old name is not suggested + _ <- assertCompletionEdit( + "UniqueObject@@", + """|package a + | + |import b.UniqueObjectOther + |class A { + | UniqueObjectOther + | def completeThisUniqueName() = 42 + |} + |""".stripMargin, + ) + // check if the change name is picked up despite the file not compiling + newText = """|package a + | + |class A { + | <> + | def completeThisUniqueName() = 42 + |} + |""".stripMargin + input = newText.replace("<<", "").replace(">>", "") + _ <- server.didSave("a/src/main/scala/a/A.scala") { _ => + newText.replace("<<", "").replace(">>", "") + } + codeActions <- + server + .assertCodeAction( + "a/src/main/scala/a/A.scala", + newText, + s"""|${ImportMissingSymbol.title("UniqueObjectOther", "b")} + |${CreateNewSymbol.title("UniqueObjectOther")} + |""".stripMargin, + kind = Nil, + ) + // make sure that the now change UniqueObject is not suggested + _ <- server.didSave("a/src/main/scala/a/A.scala") { _ => + input.replace("UniqueObjectOther", "UniqueObject") + } + codeActions <- + server + .assertCodeAction( + "a/src/main/scala/a/A.scala", + newText.replace("UniqueObjectOther", "UniqueObject"), + s"""|${CreateNewSymbol.title("UniqueObject")} + |""".stripMargin, + kind = Nil, + ) + } yield () + } + + test("never-compiling") { + cleanWorkspace() + for { + _ <- initialize( + """/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + |package a + |class A { + | // @@ + | def completeThisUniqueName(): String = 42 + |} + |/a/src/main/scala/b/B.scala + |package b + |object UniqueObject { + | def completeThisUniqueName() = 42 + | def completeThisUniqueName2(): String = 42 + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/A.scala") + _ <- server.didOpen("a/src/main/scala/b/B.scala") + _ = assertNoDiff( + server.client.workspaceDiagnostics, + """|a/src/main/scala/a/A.scala:4:42: error: type mismatch; + | found : Int(42) + | required: String + | def completeThisUniqueName(): String = 42 + | ^^ + |a/src/main/scala/b/B.scala:4:43: error: type mismatch; + | found : Int(42) + | required: String + | def completeThisUniqueName2(): String = 42 + | ^^ + |""".stripMargin, + ) + _ <- assertCompletion( + "b.UniqueObject.completeThisUniqueNa@@", + """|completeThisUniqueName(): Int + |completeThisUniqueName2(): String""".stripMargin, + ) + _ <- assertCompletionEdit( + "UniqueObject@@", + """|package a + | + |import b.UniqueObject + |class A { + | UniqueObject + | def completeThisUniqueName(): String = 42 + |} + |""".stripMargin, + ) + } yield () + } + + test("never-compiling2") { + cleanWorkspace() + for { + _ <- initialize( + """/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + |package a + |class A { + | // @@ + | def completeThisUniqueName(): String = 42 + |} + |/a/src/main/scala/b/B.scala + |package b + |trait BTrait { + | def completeThisUniqueName3() = 42 + |} + |class B + |/a/src/main/scala/c/C.scala + |package c + |import b.BTrait + |import b.B + | + |object UniqueObject extends BTrait { + | def completeThisUniqueName() = 42 + | def completeThisUniqueName2(b: B): String = 42 + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/c/C.scala") + _ = assertNoDiff( + server.client.workspaceDiagnostics, + """|a/src/main/scala/a/A.scala:4:42: error: type mismatch; + | found : Int(42) + | required: String + | def completeThisUniqueName(): String = 42 + | ^^ + |a/src/main/scala/c/C.scala:7:47: error: type mismatch; + | found : Int(42) + | required: String + | def completeThisUniqueName2(b: B): String = 42 + | ^^ + |""".stripMargin, + ) + _ <- assertCompletion( + "c.UniqueObject.completeThisUniqueNa@@", + """|completeThisUniqueName(): Int + |completeThisUniqueName2(b: B): String + |completeThisUniqueName3(): Int""".stripMargin, + ) + } yield () + } + + test("never-compiling-reverse-order") { + cleanWorkspace() + for { + _ <- initialize( + """/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + |package a + |case class A(bar: String) + |object O { + | type T = A + |} + |/a/src/main/scala/b/B.scala + |package b + |import a.O.T + | + |object B { + | def getT: T = ??? + |} + |/a/src/main/scala/c/C.scala + |package c + |import b.B + | + |object C { + | val i: Int = "aaa" + | def foo = B.getT + | def bar = foo.bar + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/c/C.scala") + _ = assertNoDiff( + server.client.workspaceDiagnostics, + """|a/src/main/scala/c/C.scala:5:16: error: type mismatch; + | found : String("aaa") + | required: Int + | val i: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + _ <- assertCompletion( + " def bar = foo.bar@@", + "bar: String", + filename = Some("a/src/main/scala/c/C.scala"), + ) + } yield () + } + + test("imports-for-non-compiling") { + cleanWorkspace() + for { + _ <- initialize( + """/metals.json + |{ + | "a": {} + |} + |/a/src/main/scala/a/A.scala + |package a + | + |/a/src/main/scala/b/B.scala + |package b + | + |object O { + | class UniqueObject { + | } + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/b/B.scala") + _ <- server.didChange("a/src/main/scala/b/B.scala") { _ => + """|package b + |object W { + | class UniqueObject { + | val i: Int = "aaa" + | } + |} + |""".stripMargin + } + _ <- server.didSave("a/src/main/scala/b/B.scala")(identity) + // check if the change name is picked up despite the file not compiling + newText = """|package a + | + |object A { + | // @@ + | val k: <> = ??? + |} + |""".stripMargin + input = newText.replace("<<", "").replace(">>", "") + _ <- server.didChange("a/src/main/scala/a/A.scala")(_ => input) + _ <- server.didSave("a/src/main/scala/a/A.scala")(identity) + codeActions <- + server + .assertCodeAction( + "a/src/main/scala/a/A.scala", + newText, + s"""|${ImportMissingSymbol.title("UniqueObject", "b.W")} + |${CreateNewSymbol.title("UniqueObject")} + |""".stripMargin, + kind = Nil, + ) + _ <- assertCompletionEdit( + "UniqueObject@@", + """|package a + | + |import b.W.UniqueObject + | + |object A { + | UniqueObject + | val k: UniqueObject = ??? + |} + |""".stripMargin, + ) + } yield () + } } diff --git a/tests/unit/src/test/scala/tests/SemanticTokensLspSuite.scala b/tests/unit/src/test/scala/tests/SemanticTokensLspSuite.scala index e30699ba51b..4a67f1b84d1 100644 --- a/tests/unit/src/test/scala/tests/SemanticTokensLspSuite.scala +++ b/tests/unit/src/test/scala/tests/SemanticTokensLspSuite.scala @@ -254,6 +254,57 @@ class SemanticTokensLspSuite extends BaseLspSuite("SemanticTokens") { |""".stripMargin, ) + test("new-changes") { + val expected = + """|<>/*keyword*/ <>/*namespace*/ + |<>/*keyword*/ <
>/*class*/ { + | <>/*keyword*/ <>/*method,definition*/: <>/*type*/ = { + | <>/*keyword*/ <>/*variable,definition,readonly*/ = <<3>>/*number*/ + | <>/*variable,readonly*/ <<+>>/*method,deprecated,abstract*/ <<4>>/*number*/ + |""".stripMargin + val fileContent = expected.replaceAll(raw"<<|>>|/\*.*?\*/", "") + for { + _ <- initialize( + s"""/metals.json + |{"a":{}} + |/a/src/main/scala/a/Main.scala + | + |/a/src/main/scala/a/OtherFile.scala + |package a + |object A + |""".stripMargin, + expectError = true, + ) + _ <- server.didChangeConfiguration( + """{ + | "enable-semantic-highlighting": true + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/Main.scala") + _ <- server.didChange("a/src/main/scala/a/Main.scala")(_ => fileContent) + _ <- server.didSave("a/src/main/scala/a/Main.scala")(identity) + _ <- server.didOpen("a/src/main/scala/a/OtherFile.scala") + // triggers outline compile on `Main.scala` + _ <- server.assertSemanticHighlight( + "a/src/main/scala/a/OtherFile.scala", + """|<>/*keyword*/ <>/*namespace*/ + |<>/*keyword*/ <>/*class*/ + |""".stripMargin, + """|package a + |object A + |""".stripMargin, + ) + _ <- server.didOpen("a/src/main/scala/a/Main.scala") + // tests if we do full compile after outline compile + _ <- server.assertSemanticHighlight( + "a/src/main/scala/a/Main.scala", + expected, + fileContent, + ) + } yield () + } + def check( name: TestOptions, expected: String, diff --git a/tests/unit/src/test/scala/tests/codeactions/OrganizeImportsLspSuite.scala b/tests/unit/src/test/scala/tests/codeactions/OrganizeImportsLspSuite.scala index d778eed7b77..399671d1397 100644 --- a/tests/unit/src/test/scala/tests/codeactions/OrganizeImportsLspSuite.scala +++ b/tests/unit/src/test/scala/tests/codeactions/OrganizeImportsLspSuite.scala @@ -96,7 +96,7 @@ class OrganizeImportsLspSuite ) check( - "basic-unsaved-error", + "basic-unsaved-2", """ |package a |import scala.concurrent.duration._ @@ -110,8 +110,6 @@ class OrganizeImportsLspSuite |""".stripMargin, s"${SourceOrganizeImports.title}", """|package a - |import java.nio.file.ClassDoNotExist - |import scala.concurrent.ExecutionContext.global |import scala.concurrent.Future |import scala.concurrent.duration._ |// comment