Skip to content

Commit

Permalink
feat: implement 'convert to named lambda parameters' code action
Browse files Browse the repository at this point in the history
  • Loading branch information
KacperFKorban committed Aug 12, 2024
1 parent 057e7bc commit 601c8f7
Show file tree
Hide file tree
Showing 14 changed files with 572 additions and 1 deletion.
14 changes: 14 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Compilers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,20 @@ class Compilers(
}
}.getOrElse(Future.successful(Nil.asJava))

def convertToNamedLambdaParameters(
position: TextDocumentPositionParams,
token: CancelToken,
): Future[ju.List[TextEdit]] = {
withPCAndAdjustLsp(position) { (pc, pos, adjust) =>
pc.convertToNamedLambdaParameters(
CompilerOffsetParamsUtils.fromPos(pos, token)
).asScala
.map { edits =>
adjust.adjustTextEdits(edits)
}
}
}.getOrElse(Future.successful(Nil.asJava))

def implementAbstractMembers(
params: TextDocumentPositionParams,
token: CancelToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,20 @@ object ServerCommands {
|""".stripMargin,
)

final case class ConvertToNamedLambdaParametersRequest(
position: TextDocumentPositionParams
)
val ConvertToNamedLambdaParameters =
new ParametrizedCommand[ConvertToNamedLambdaParametersRequest](
"convert-to-named-lambda-parameters",
"Convert wildcard lambda parameters to named parameters",
"""|Whenever a user chooses code action to convert to named lambda parameters, this command is later run to
|rewrite the lambda to use named parameters.
|""".stripMargin,
"""|Object with [TextDocumentPositionParams](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams) of the target lambda
|""".stripMargin,
)

val GotoLog = new Command(
"goto-log",
"Check logs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class CodeActionProvider(
new MillifyDependencyCodeAction(buffers),
new MillifyScalaCliDependencyCodeAction(buffers),
new ConvertCommentCodeAction(buffers),
new ConvertToNamedLambdaParameters(trees, compilers, languageClient),
)

def codeActions(
Expand Down
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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ public CompletableFuture<List<TextEdit>> inlineValue(OffsetParams params) {
public abstract CompletableFuture<List<TextEdit>> convertToNamedArguments(OffsetParams params,
List<Integer> argIndices);

/**
* Return the text edits for converting a wildcard lambda to a named lambda.
*/
public CompletableFuture<List<TextEdit>> convertToNamedLambdaParameters(OffsetParams params) {
return CompletableFuture.supplyAsync(() -> {
throw new DisplayableException("Convert to named lambda parameters is not available in this version of Scala");
});
};

/**
* The text contents of the given file changed.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ case class JavaPresentationCompiler(
): CompletableFuture[util.List[TextEdit]] =
CompletableFuture.completedFuture(Nil.asJava)

override def convertToNamedLambdaParameters(
params: OffsetParams
): CompletableFuture[util.List[TextEdit]] =
CompletableFuture.completedFuture(Nil.asJava)

override def inlayHints(
params: InlayHintsParams
): CompletableFuture[util.List[lsp4j.InlayHint]] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scala.meta.internal.pc

import scala.meta.pc.OffsetParams

import org.eclipse.{lsp4j => l}

final class ConvertToNamedLambdaParametersProvider(
val compiler: MetalsGlobal,
offsetParam: OffsetParams
) {

def convertToNamedLambdaParameters: Either[String, List[l.TextEdit]] = {
Right(Nil)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,23 @@ case class ScalaPresentationCompiler(
}
}

override def convertToNamedLambdaParameters(
params: OffsetParams
): CompletableFuture[ju.List[TextEdit]] = {
val empty: Either[String, List[TextEdit]] = Right(List())
(compilerAccess
.withInterruptableCompiler(Some(params))(empty, params.token) { pc =>
new ConvertToNamedLambdaParametersProvider(
pc.compiler(),
params
).convertToNamedLambdaParameters
})
.thenApply {
case Left(error: String) => throw new DisplayableException(error)
case Right(edits: List[TextEdit]) => edits.asJava
}
}

override def autoImports(
name: String,
params: OffsetParams,
Expand Down
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
}
}
Loading

0 comments on commit 601c8f7

Please sign in to comment.