-
Notifications
You must be signed in to change notification settings - Fork 337
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement 'convert to named lambda parameters' code action
- Loading branch information
1 parent
057e7bc
commit 0eabc2e
Showing
11 changed files
with
550 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
...rc/main/scala/scala/meta/internal/metals/codeactions/ConvertToNamedLambdaParameters.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package scala.meta.internal.metals.codeactions | ||
|
||
import scala.concurrent.ExecutionContext | ||
import scala.concurrent.Future | ||
|
||
import scala.meta.Term | ||
import scala.meta.internal.metals.Compilers | ||
import scala.meta.internal.metals.MetalsEnrichments._ | ||
import scala.meta.internal.metals.ServerCommands | ||
import scala.meta.internal.metals.clients.language.MetalsLanguageClient | ||
import scala.meta.internal.metals.codeactions.CodeAction | ||
import scala.meta.internal.metals.codeactions.CodeActionBuilder | ||
import scala.meta.internal.metals.logging | ||
import scala.meta.internal.parsing.Trees | ||
import scala.meta.pc.CancelToken | ||
|
||
import org.eclipse.{lsp4j => l} | ||
|
||
/** | ||
* Code action to convert a wildcard lambda to a lambda with named parameters | ||
* e.g. | ||
* | ||
* List(1, 2).map(<<_>> + 1) => List(1, 2).map(i => i + 1) | ||
*/ | ||
class ConvertToNamedLambdaParameters( | ||
trees: Trees, | ||
compilers: Compilers, | ||
languageClient: MetalsLanguageClient, | ||
) extends CodeAction { | ||
|
||
override val kind: String = l.CodeActionKind.RefactorRewrite | ||
|
||
override type CommandData = | ||
ServerCommands.ConvertToNamedLambdaParametersRequest | ||
|
||
override def command: Option[ActionCommand] = Some( | ||
ServerCommands.ConvertToNamedLambdaParameters | ||
) | ||
|
||
override def handleCommand( | ||
data: ServerCommands.ConvertToNamedLambdaParametersRequest, | ||
token: CancelToken, | ||
)(implicit ec: ExecutionContext): Future[Unit] = { | ||
val uri = data.position.getTextDocument().getUri() | ||
for { | ||
edits <- compilers.convertToNamedLambdaParameters( | ||
data.position, | ||
token, | ||
) | ||
_ = logging.logErrorWhen( | ||
edits.isEmpty(), | ||
s"Could not convert lambda at position ${data.position} to named lambda", | ||
) | ||
workspaceEdit = new l.WorkspaceEdit(Map(uri -> edits).asJava) | ||
_ <- languageClient | ||
.applyEdit(new l.ApplyWorkspaceEditParams(workspaceEdit)) | ||
.asScala | ||
} yield () | ||
} | ||
|
||
override def contribute( | ||
params: l.CodeActionParams, | ||
token: CancelToken, | ||
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = { | ||
val path = params.getTextDocument().getUri().toAbsolutePath | ||
val range = params.getRange() | ||
val maybeLambda = | ||
trees.findLastEnclosingAt[Term.AnonymousFunction](path, range.getStart()) | ||
maybeLambda | ||
.map { lambda => | ||
val position = new l.TextDocumentPositionParams( | ||
params.getTextDocument(), | ||
new l.Position(lambda.pos.startLine, lambda.pos.startColumn), | ||
) | ||
val command = | ||
ServerCommands.ConvertToNamedLambdaParameters.toLsp( | ||
ServerCommands.ConvertToNamedLambdaParametersRequest(position) | ||
) | ||
val codeAction = CodeActionBuilder.build( | ||
title = ConvertToNamedLambdaParameters.title, | ||
kind = kind, | ||
command = Some(command), | ||
) | ||
Future.successful(Seq(codeAction)) | ||
} | ||
.getOrElse(Future.successful(Nil)) | ||
} | ||
|
||
} | ||
|
||
object ConvertToNamedLambdaParameters { | ||
def title: String = "Convert to named lambda parameters" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
mtags/src/main/scala-3/scala/meta/internal/mtags/TermNameInference.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package scala.meta.internal.mtags | ||
|
||
/** | ||
* Helpers for generating variable names based on the desired types. | ||
*/ | ||
object TermNameInference { | ||
|
||
/** Single character names for types. (`Int` => `i`, `i1`, `i2`, ...) */ | ||
def singleLetterNameStream(typeName: String): LazyList[String] = { | ||
val typeName1 = sanitizeInput(typeName) | ||
val firstCharStr = typeName1.headOption.getOrElse('x').toLower.toString | ||
numberedStreamFromName(firstCharStr) | ||
} | ||
|
||
/** Names only from upper case letters (`OnDemandSymbolIndex` => `odsi`, `odsi1`, `odsi2`, ...) */ | ||
def shortNameStream(typeName: String): LazyList[String] = { | ||
val typeName1 = sanitizeInput(typeName) | ||
val upperCases = typeName1.filter(_.isUpper).map(_.toLower) | ||
val name = if (upperCases.isEmpty) typeName1 else upperCases | ||
numberedStreamFromName(name) | ||
} | ||
|
||
/** Names from lower case letters (`OnDemandSymbolIndex` => `onDemandSymbolIndex`, `onDemandSymbolIndex1`, ...) */ | ||
def fullNameStream(typeName: String): LazyList[String] = { | ||
val typeName1 = sanitizeInput(typeName) | ||
val withFirstLower = | ||
typeName1.headOption.map(_.toLower).getOrElse('x') + typeName1.drop(1) | ||
numberedStreamFromName(withFirstLower) | ||
} | ||
|
||
/** A lazy list of names: a, b, ..., z, aa, ab, ..., az, ba, bb, ... */ | ||
def saneNamesStream: LazyList[String] = { | ||
val letters = ('a' to 'z').map(_.toString) | ||
def computeNext(acc: String): String = { | ||
if (acc.last == 'z') | ||
computeNext(acc.init) + letters.head | ||
else | ||
acc.init + letters(letters.indexOf(acc.last) + 1) | ||
} | ||
def loop(acc: String): LazyList[String] = | ||
acc #:: loop(computeNext(acc)) | ||
loop("a") | ||
} | ||
|
||
private def sanitizeInput(typeName: String): String = | ||
typeName.filter(_.isLetterOrDigit) | ||
|
||
private def numberedStreamFromName(name: String): LazyList[String] = { | ||
val rest = LazyList.from(1).map(name + _) | ||
name #:: rest | ||
} | ||
} |
150 changes: 150 additions & 0 deletions
150
mtags/src/main/scala-3/scala/meta/internal/pc/ConvertToNamedLambdaParametersProvider.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package scala.meta.internal.pc | ||
|
||
import java.nio.file.Paths | ||
|
||
import scala.meta.internal.mtags.MtagsEnrichments.* | ||
import scala.meta.internal.mtags.TermNameInference.* | ||
import scala.meta.pc.OffsetParams | ||
|
||
import dotty.tools.dotc.ast.tpd | ||
import dotty.tools.dotc.core.Contexts.Context | ||
import dotty.tools.dotc.core.Flags | ||
import dotty.tools.dotc.interactive.Interactive | ||
import dotty.tools.dotc.interactive.InteractiveDriver | ||
import dotty.tools.dotc.util.SourceFile | ||
import dotty.tools.dotc.util.SourcePosition | ||
import org.eclipse.lsp4j as l | ||
|
||
/** | ||
* Facilitates the code action that converts a wildcard lambda to a lambda with named parameters | ||
* e.g. | ||
* | ||
* List(1, 2).map(<<_>> + 1) => List(1, 2).map(i => i + 1) | ||
*/ | ||
final class ConvertToNamedLambdaParametersProvider( | ||
driver: InteractiveDriver, | ||
params: OffsetParams | ||
): | ||
import ConvertToNamedLambdaParametersProvider._ | ||
|
||
def convertToNamedLambdaParameters: Either[String, List[l.TextEdit]] = { | ||
val uri = params.uri | ||
val filePath = Paths.get(uri) | ||
driver.run( | ||
uri, | ||
SourceFile.virtual(filePath.toString, params.text), | ||
) | ||
val unit = driver.latestRun | ||
given newctx: Context = driver.currentCtx.fresh.setCompilationUnit(unit) | ||
val pos = driver.sourcePosition(params) | ||
val trees = driver.openedTrees(uri) | ||
val treeList = Interactive.pathTo(trees, pos) | ||
// Extractor for a lambda function (needs context, so has to be defined here) | ||
val LambdaExtractor = Lambda(using newctx) | ||
// select the most inner wildcard lambda | ||
val firstLambda = treeList.collectFirst { | ||
case LambdaExtractor(params, rhsFn) if params.forall(isWildcardParam) => | ||
params -> rhsFn | ||
} | ||
|
||
firstLambda match { | ||
case Some((params, lambda)) => | ||
// avoid names that are either defined or referenced in the lambda | ||
val namesToAvoid = allDefAndRefNamesInTree(lambda) | ||
// compute parameter names based on the type of the parameter | ||
val computedParamNames: List[String] = | ||
params.foldLeft(List.empty[String]) { (acc, param) => | ||
val name = singleLetterNameStream(param.tpe.typeSymbol.name.toString()) | ||
.find(n => !namesToAvoid.contains(n) && !acc.contains(n)) | ||
acc ++ name.toList | ||
} | ||
if computedParamNames.size == params.size then | ||
val paramReferenceEdits = params.zip(computedParamNames).flatMap { (param, paramName) => | ||
val paramReferencePosition = findParamReferencePosition(param, lambda) | ||
paramReferencePosition.toList.map { pos => | ||
val position = pos.toLsp | ||
val range = new l.Range( | ||
position.getStart(), | ||
position.getEnd() | ||
) | ||
new l.TextEdit(range, paramName) | ||
} | ||
} | ||
val paramNamesStr = computedParamNames.mkString(", ") | ||
val paramDefsStr = | ||
if params.size == 1 then paramNamesStr | ||
else s"($paramNamesStr)" | ||
val defRange = new l.Range( | ||
lambda.sourcePos.toLsp.getStart(), | ||
lambda.sourcePos.toLsp.getStart() | ||
) | ||
val paramDefinitionEdits = List( | ||
new l.TextEdit(defRange, s"$paramDefsStr => ") | ||
) | ||
Right(paramDefinitionEdits ++ paramReferenceEdits) | ||
else | ||
Right(Nil) | ||
case _ => | ||
Right(Nil) | ||
} | ||
} | ||
|
||
end ConvertToNamedLambdaParametersProvider | ||
|
||
object ConvertToNamedLambdaParametersProvider: | ||
class Lambda(using Context): | ||
def unapply(tree: tpd.Block): Option[(List[tpd.ValDef], tpd.Tree)] = tree match { | ||
case tpd.Block((ddef @ tpd.DefDef(_, tpd.ValDefs(params) :: Nil, _, body: tpd.Tree)) :: Nil, tpd.Closure(_, meth, _)) | ||
if ddef.symbol == meth.symbol => | ||
params match { | ||
case List(param) => | ||
// lambdas with multiple wildcard parameters are represented as a single parameter function and a block with wildcard valdefs | ||
Some(multipleUnderscoresFromBody(param, body)) | ||
case _ => Some(params -> body) | ||
} | ||
case _ => None | ||
} | ||
end Lambda | ||
|
||
private def multipleUnderscoresFromBody(param: tpd.ValDef, body: tpd.Tree)(using Context): (List[tpd.ValDef], tpd.Tree) = body match { | ||
case tpd.Block(defs, expr) if param.symbol.is(Flags.Synthetic) => | ||
val wildcardParamDefs = defs.collect { | ||
case valdef: tpd.ValDef if isWildcardParam(valdef) => valdef | ||
} | ||
if wildcardParamDefs.size == defs.size then wildcardParamDefs -> expr | ||
else List(param) -> body | ||
case _ => List(param) -> body | ||
} | ||
|
||
def isWildcardParam(param: tpd.ValDef)(using Context): Boolean = | ||
param.name.toString.startsWith("_$") && param.symbol.is(Flags.Synthetic) | ||
|
||
def findParamReferencePosition(param: tpd.ValDef, lambda: tpd.Tree)(using Context): Option[SourcePosition] = | ||
var pos: Option[SourcePosition] = None | ||
object FindParamReference extends tpd.TreeTraverser: | ||
override def traverse(tree: tpd.Tree)(using Context): Unit = | ||
tree match | ||
case ident @ tpd.Ident(_) if ident.symbol == param.symbol => | ||
pos = Some(tree.sourcePos) | ||
case _ => | ||
traverseChildren(tree) | ||
FindParamReference.traverse(lambda) | ||
pos | ||
end findParamReferencePosition | ||
|
||
def allDefAndRefNamesInTree(tree: tpd.Tree)(using Context): List[String] = | ||
object FindDefinitionsAndRefs extends tpd.TreeAccumulator[List[String]]: | ||
override def apply(x: List[String], tree: tpd.Tree)(using Context): List[String] = | ||
tree match | ||
case tpd.DefDef(name, _, _, _) => | ||
super.foldOver(x :+ name.toString, tree) | ||
case tpd.ValDef(name, _, _) => | ||
super.foldOver(x :+ name.toString, tree) | ||
case tpd.Ident(name) => | ||
super.foldOver(x :+ name.toString, tree) | ||
case _ => | ||
super.foldOver(x, tree) | ||
FindDefinitionsAndRefs.foldOver(Nil, tree) | ||
end allDefAndRefNamesInTree | ||
|
||
end ConvertToNamedLambdaParametersProvider |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.