-
Notifications
You must be signed in to change notification settings - Fork 332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ConvertToNamedArguments code action #3971
Changes from 16 commits
dbe9541
6309677
2079c5e
5409484
0a22df1
b25f9b3
f108641
9013285
14fab12
123b5a7
64b9ef8
7deeec6
765f045
0002d11
484a7d4
4a82843
1244339
138fd7d
bf7aad5
e7facf1
83f6bea
2a2ffb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package scala.meta.internal.metals.codeactions | ||
|
||
import scala.concurrent.ExecutionContext | ||
import scala.concurrent.Future | ||
|
||
import scala.meta.Term | ||
import scala.meta.Tree | ||
import scala.meta.internal.metals.CodeAction | ||
import scala.meta.internal.metals.MetalsEnrichments._ | ||
import scala.meta.internal.metals.ServerCommands | ||
import scala.meta.internal.parsing.Trees | ||
import scala.meta.pc.CancelToken | ||
|
||
import org.eclipse.{lsp4j => l} | ||
|
||
class ConvertToNamedArguments(trees: Trees) extends CodeAction { | ||
|
||
import ConvertToNamedArguments._ | ||
override val kind: String = l.CodeActionKind.RefactorRewrite | ||
|
||
def firstApplyWithUnnamedArgs( | ||
term: Option[Tree] | ||
): Option[ApplyTermWithArgIndices] = { | ||
term match { | ||
case Some(apply: Term.Apply) => | ||
val argIndices = apply.args.zipWithIndex.collect { | ||
case (arg, index) | ||
if !arg.isInstanceOf[Term.Assign] && !arg | ||
.isInstanceOf[Term.Block] => | ||
index | ||
} | ||
if (argIndices.isEmpty) firstApplyWithUnnamedArgs(apply.parent) | ||
else Some(ApplyTermWithArgIndices(apply, argIndices)) | ||
case Some(t) => firstApplyWithUnnamedArgs(t.parent) | ||
case _ => None | ||
} | ||
} | ||
|
||
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 maybeApply = for { | ||
term <- trees.findLastEnclosingAt[Term.Apply](path, range.getStart()) | ||
apply <- firstApplyWithUnnamedArgs(Some(term)) | ||
} yield apply | ||
|
||
maybeApply | ||
.map { apply => | ||
{ | ||
val codeAction = new l.CodeAction(title(apply.app.fun.syntax)) | ||
codeAction.setKind(l.CodeActionKind.RefactorRewrite) | ||
val position = new l.TextDocumentPositionParams( | ||
params.getTextDocument(), | ||
new l.Position(apply.app.pos.endLine, apply.app.pos.endColumn) | ||
) | ||
codeAction.setCommand( | ||
ServerCommands.ConvertToNamedArguments.toLSP( | ||
ServerCommands | ||
.ConvertToNamedArgsRequest( | ||
position, | ||
apply.argIndices.map(new Integer(_)).asJava | ||
) | ||
) | ||
) | ||
Future.successful(Seq(codeAction)) | ||
} | ||
} | ||
.getOrElse(Future.successful(Nil)) | ||
} | ||
} | ||
|
||
object ConvertToNamedArguments { | ||
case class ApplyTermWithArgIndices(app: Term.Apply, argIndices: List[Int]) | ||
def title(funcName: String) = s"Convert $funcName to named arguments" | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||||||||||||||||||
package scala.meta.internal.pc | ||||||||||||||||||||||
|
||||||||||||||||||||||
import scala.meta.pc.OffsetParams | ||||||||||||||||||||||
|
||||||||||||||||||||||
import org.eclipse.{lsp4j => l} | ||||||||||||||||||||||
|
||||||||||||||||||||||
final class ConvertToNamedArgumentsProvider( | ||||||||||||||||||||||
val compiler: MetalsGlobal, | ||||||||||||||||||||||
params: OffsetParams, | ||||||||||||||||||||||
argIndices: Set[Int] | ||||||||||||||||||||||
) { | ||||||||||||||||||||||
|
||||||||||||||||||||||
import compiler._ | ||||||||||||||||||||||
def convertToNamedArguments: List[l.TextEdit] = { | ||||||||||||||||||||||
val unit = addCompilationUnit( | ||||||||||||||||||||||
code = params.text(), | ||||||||||||||||||||||
filename = params.uri().toString(), | ||||||||||||||||||||||
cursor = None | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
val typedTree = typedTreeAt(unit.position(params.offset)) | ||||||||||||||||||||||
tanishiking marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
typedTree match { | ||||||||||||||||||||||
case Apply(fun, args) => | ||||||||||||||||||||||
args.zipWithIndex | ||||||||||||||||||||||
.zip(fun.tpe.params) | ||||||||||||||||||||||
.collect { | ||||||||||||||||||||||
case ((arg, index), param) if argIndices.contains(index) => { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the compiler have any idea that there is a named parameter here? This might actually be the case, but just wanted to double check. We wouldn't need to calculate it before in that case. Otherwise, that's probably the right approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
metals/mtags/src/main/scala-2/scala/meta/internal/pc/completions/ArgCompletions.scala Lines 29 to 38 in 2cee762
edit: sorry, this is from Scala2 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had tried this originally, but it seems like the compiler doesn't pick up on addCompilationUnit(
params.text(),
filename = params.uri().toString(),
cursor = None
) and add some logging, it shows the args as:
|
||||||||||||||||||||||
val position = arg.pos.toLSP | ||||||||||||||||||||||
position.setEnd(position.getStart()) | ||||||||||||||||||||||
new l.TextEdit(position, s"${param.nameString} = ") | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
case _ => Nil | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package tests.pc | ||
|
||
import java.net.URI | ||
|
||
import scala.meta.internal.jdk.CollectionConverters._ | ||
import scala.meta.internal.metals.CompilerOffsetParams | ||
import scala.meta.internal.metals.TextEdits | ||
|
||
import munit.Location | ||
import munit.TestOptions | ||
import org.eclipse.{lsp4j => l} | ||
import tests.BaseCodeActionSuite | ||
|
||
class ConvertToNamedArgumentsSuite extends BaseCodeActionSuite { | ||
|
||
override protected def requiresScalaLibrarySources: Boolean = true | ||
|
||
checkEdit( | ||
"scala-std-lib", | ||
"""|object A{ | ||
| val a = <<scala.math.max(1, 2)>> | ||
|}""".stripMargin, | ||
List(0, 1), | ||
"""|object A{ | ||
| val a = scala.math.max(x = 1, y = 2) | ||
|}""".stripMargin | ||
) | ||
|
||
def checkEdit( | ||
name: TestOptions, | ||
original: String, | ||
argIndices: List[Int], | ||
expected: String, | ||
compat: Map[String, String] = Map.empty | ||
)(implicit location: Location): Unit = | ||
test(name) { | ||
val edits = convertToNamedArgs(original, argIndices) | ||
val (code, _, _) = params(original) | ||
val obtained = TextEdits.applyEdits(code, edits) | ||
assertNoDiff(obtained, getExpected(expected, compat, scalaVersion)) | ||
} | ||
|
||
def convertToNamedArgs( | ||
original: String, | ||
argIndices: List[Int], | ||
filename: String = "file:/A.scala" | ||
): List[l.TextEdit] = { | ||
val (code, _, offset) = params(original) | ||
val result = presentationCompiler | ||
.convertToNamedArguments( | ||
CompilerOffsetParams(URI.create(filename), code, offset, cancelToken), | ||
argIndices.map(new Integer(_)).asJava | ||
) | ||
.get() | ||
result.asScala.toList | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that we don't get the code action when at for example
F<<>>uture.succesfull(1)