Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package scala.meta.internal.metals

import java.net.URI
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Collections
Expand Down Expand Up @@ -335,11 +336,14 @@ class Compilers(
)

private def didChangeBSPDiagnostics(
path: AbsolutePath
path: AbsolutePath,
shouldReturnDiagnostics: Boolean,
content: Option[String] = None,
): Future[List[Diagnostic]] = {
def originInput =
path
.toInputFromBuffers(buffers)
content.map(Input.VirtualFile(path.toURI.toString(), _)).getOrElse {
path.toInputFromBuffers(buffers)
}

loadCompiler(path)
.map { pc =>
Expand All @@ -361,7 +365,21 @@ class Compilers(

outlineFilesProvider.didChange(pc.buildTargetId(), path)

Future.successful(Nil)
for {
ds <-
pc
.didChange(
Compilers.DidChangeCompilerFileParams(
path.toNIO.toUri(),
input.value,
shouldReturnDiagnostics,
)
)
.asScala
} yield {
ds.asScala.map(adjust.adjustDiagnostic).toList

}
}
.getOrElse(Future.successful(Nil))
}
Expand All @@ -371,7 +389,14 @@ class Compilers(
// Batch/debounce these requests since they can arrivein bursts
fileDidChange(Seq(path))
else
didChangeBSPDiagnostics(path).ignoreValue
didChangeBSPDiagnostics(path, shouldReturnDiagnostics = false).ignoreValue
}

def didChangeWithDiagnostics(
path: AbsolutePath,
content: Option[String] = None,
): Future[List[Diagnostic]] = {
didChangeBSPDiagnostics(path, shouldReturnDiagnostics = true, content)
}

def didCompile(report: CompileReport): Unit = {
Expand Down Expand Up @@ -1865,4 +1890,14 @@ object Compilers {
extends PresentationCompilerKey
}

// To be removed in the future after:
// - https://github.com/scalameta/metals/pull/7430
// - https://github.com/scala/scala3/pull/22259
case class DidChangeCompilerFileParams(
uri: URI,
text: String,
override val shouldReturnDiagnostics: Boolean,
token: CancelToken = EmptyCancelToken,
) extends VirtualFileParams

}
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,14 @@ abstract class MetalsLspService(
protected val syncStatusReporter: SyncStatusReporter =
new SyncStatusReporter(languageClient, buildTargets)

private val metalsPasteProvider: MetalsPasteProvider =
new MetalsPasteProvider(
compilers,
buildTargets,
definitionProvider,
trees,
)

def parseTreesAndPublishDiags(paths: Seq[AbsolutePath]): Future[Unit] = {
Future
.traverse(paths.distinct) { path =>
Expand Down Expand Up @@ -1499,6 +1507,16 @@ abstract class MetalsLspService(
.runRulesOrPrompt(uri.toAbsolutePath, rules)
.flatMap(applyEdits(uri, _))

def didPaste(
params: MetalsPasteParams
): Future[ApplyWorkspaceEditResponse] = {
metalsPasteProvider
.didPaste(params, EmptyCancelToken)
.flatMap(optEdit =>
applyEdits(params.textDocument.getUri(), optEdit.toList)
)
}

protected def applyEdits(
uri: String,
edits: List[TextEdit],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package scala.meta.internal.metals

import scala.concurrent.ExecutionContext
import scala.concurrent.Future

import scala.meta.Import
import scala.meta.Pkg
import scala.meta.Source
import scala.meta.Stat
import scala.meta.inputs.Input.VirtualFile
import scala.meta.inputs.Position
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.codeactions.MissingSymbolDiagnostic
import scala.meta.internal.parsing.Trees
import scala.meta.internal.pc.ScriptFirstImportPosition
import scala.meta.internal.semanticdb.Scala._
import scala.meta.io.AbsolutePath
import scala.meta.pc.CancelToken

import org.eclipse.lsp4j
import org.eclipse.lsp4j.TextDocumentPositionParams
import org.eclipse.lsp4j.TextEdit

class MetalsPasteProvider(
compilers: Compilers,
buildTargets: BuildTargets,
definitions: DefinitionProvider,
trees: Trees,
)(implicit ec: ExecutionContext) {

def didPaste(
params: MetalsPasteParams,
cancelToken: CancelToken,
): Future[Option[TextEdit]] = {
val path = params.textDocument.getUri.toAbsolutePath
val orginalPath = params.originDocument.getUri().toAbsolutePath
val isScala3 =
buildTargets.scalaVersion(path).exists(ScalaVersions.isScala3Version)
val MissingSymbol = new MissingSymbolDiagnostic(isScala3, path)

compilers
.didChangeWithDiagnostics(
path,
content = Some(params.text),
)
.flatMap { diagnostics =>
val imports = diagnostics.collect {
case d @ MissingSymbol(name, _)
if params.range.overlapsWith(d.getRange()) =>

val offset =
if (isScala3) d.getRange().getEnd()
else d.getRange().getStart()

val symbolsPositionParams =
new TextDocumentPositionParams(
params.originDocument,
adjustPositionToOrigin(params, offset),
)
for {
defnResult <- definitions.definition(
orginalPath,
symbolsPositionParams,
cancelToken,
)
} yield {
if (defnResult.isEmpty) None
else {
val symbolDesc = defnResult.symbol.desc
val symbolName = symbolDesc.name.value
lazy val owner = defnResult.symbol.owner
lazy val ownerName = owner.desc.name.value

val importText =
if (name != symbolName) {
if (
symbolDesc.isMethod &&
(symbolName == "apply" || symbolName == "unapply" || symbolName == "<init>")
)
if (ownerName == name) s"import ${symbolToFqcn(owner)}"
else
s"import ${symbolToFqcn(owner.owner)}.{${ownerName} => $name}"
else s"import ${symbolToFqcn(owner)}.{$symbolName => $name}"
} else s"import ${symbolToFqcn(defnResult.symbol)}"

Some(importText)
}
}
}
Future
.sequence(imports)
.map(_.flatten.distinct)
.map { imports =>
Option.when(imports.nonEmpty) {
val (prefix, suffix, pos) =
autoImportPosition(path, params.text, params.range.getStart())
new lsp4j.TextEdit(
new lsp4j.Range(pos, pos),
imports.mkString(prefix, "\n", suffix),
)
}
}
}
}

/**
* Convert a semanticdb symbol to an import path.
* E.g., "scala/collection/immutable/List#" -> "scala.collection.immutable.List"
*/
private def symbolToFqcn(symbol: String): String = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of this got rewritten and replaced on main. The logic was flaky unfortunately, could you check the newest version instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've applied newer changes from the main branch.

import scala.meta.internal.semanticdb.Scala.Symbols

// Build owner chain by recursively following .owner
@scala.annotation.tailrec
def buildOwnerChain(
sym: String,
acc: List[String],
depth: Int = 0,
): List[String] = {
if (depth > 100) {
// Prevent infinite recursion
acc
} else {
val desc = sym.desc
if (
desc.isNone || sym == Symbols.RootPackage || sym == Symbols.EmptyPackage
) {
acc
} else {
buildOwnerChain(sym.owner, sym :: acc, depth + 1)
}
}
}

// Find the term, type, or package (skip method descriptors, parameters, etc.)
@scala.annotation.tailrec
def findImportable(sym: String, depth: Int = 0): String = {
if (depth > 100) {
// Prevent infinite recursion
sym
} else {
val desc = sym.desc
if (desc.isTerm || desc.isType || desc.isPackage) sym
else findImportable(sym.owner, depth + 1)
}
}

val importable = findImportable(symbol)
val owners = buildOwnerChain(importable, Nil)

owners.map(_.desc.name.value).mkString(".")
}

private def adjustPositionToOrigin(
params: MetalsPasteParams,
pos: lsp4j.Position,
): lsp4j.Position = {
val lineDiff =
params.originOffset.getLine() - params.range.getStart().getLine()
if (pos.getLine() == params.range.getStart().getLine()) {
val charDiff = params.originOffset
.getCharacter() - params.range.getStart().getCharacter()
new lsp4j.Position(
pos.getLine() + lineDiff,
pos.getCharacter() + charDiff,
)
} else {
new lsp4j.Position(pos.getLine() + lineDiff, pos.getCharacter())
}
}

private def autoImportPosition(
path: AbsolutePath,
text: String,
pos: lsp4j.Position,
): (String, String, lsp4j.Position) = {

lazy val fallback = {
val inputFromText = new VirtualFile(path.toURI.toString, text)
val point = ScriptFirstImportPosition.infer(text)
val pos = Position.Range(inputFromText, point, point).toLsp.getStart()
("", "\n\n", pos)
}

def afterImportsPositon(stats: List[Stat]) =
stats
.takeWhile {
case _: Import => true
case _ => false
}
.lastOption
.map { imp =>
("\n", "\n", imp.pos.toLsp.getEnd())
}
trees.findLastEnclosingAt[Pkg](path, pos, _ => true) match {
case Some(pkg @ Pkg(_, stats)) =>
afterImportsPositon(stats).getOrElse(
(
"",
"\n\n",
stats.headOption
.map(_.pos.toLsp.getStart())
.getOrElse(pkg.pos.toLsp.getEnd()),
)
)
case _ =>
trees.get(path) match {
case Some(Source(stats)) =>
afterImportsPositon(stats).getOrElse(fallback)
case _ => fallback
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import scala.meta.internal.metals.newScalaFile.NewFileTypes

import ch.epfl.scala.{bsp4j => b}
import org.eclipse.lsp4j.Location
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.eclipse.lsp4j.TextDocumentPositionParams

Expand Down Expand Up @@ -456,6 +458,14 @@ object ServerCommands {
"[string], where the string is a stacktrace.",
)

val MetalsPaste = new ParametrizedCommand[MetalsPasteParams](
"metals-did-paste",
"Add needed import statements after paste",
"""|
|""".stripMargin,
"""|MetalsPasteParams""".stripMargin,
)

final case class ChooseClassRequest(
textDocument: TextDocumentIdentifier,
kind: String,
Expand Down Expand Up @@ -863,3 +873,16 @@ case class RunScalafixRulesParams(
textDocumentPositionParams: TextDocumentPositionParams,
@Nullable rules: java.util.List[String] = null,
)

case class MetalsPasteParams(
// The text document, where text was pasted.
textDocument: TextDocumentIdentifier,
// The range in the text document, where text was pasted.
range: Range,
// Content of the file after paste.
text: String,
// The origin document, where text was copied from.
originDocument: TextDocumentIdentifier,
// The origin start offset, where text was copied from.
originOffset: Position,
)
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,9 @@ class WorkspaceLspService(
.asJavaObject
case ServerCommands.CopyWorksheetOutput(path) =>
getServiceFor(path).copyWorksheetOutput(path.toAbsolutePath)
case ServerCommands.MetalsPaste(params) =>
val path = params.originDocument.getUri().toAbsolutePath
getServiceFor(path).didPaste(params).asJavaObject
case actionCommand
if currentOrHeadOrFallback.allActionCommandsIds(
actionCommand.getCommand()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class CodeActionProvider(

private val allActions: List[CodeAction] = List(
new ImplementAbstractMembers(compilers),
new ImportMissingSymbol(compilers, buildTargets),
new ImportMissingSymbolQuickFix(compilers, buildTargets),
new CreateNewSymbol(compilers, languageClient),
new ActionableDiagnostic(),
new StringActions(buffers),
Expand Down
Loading