Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package scala.meta.internal.metals.codeactions

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

import scala.meta.internal.jpc.JavacDiagnostic
import scala.meta.internal.metals.Compilers
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.io.AbsolutePath
import scala.meta.pc.CancelToken
import scala.meta.pc.CodeActionId

import org.eclipse.{lsp4j => l}

class ChangeVariableType(
compilers: Compilers
) extends CodeAction {

override def kind: String = l.CodeActionKind.QuickFix
override def isScala: Boolean = false
override def isJava: Boolean = true
override def maybeCodeActionId: Option[String] =
Some(CodeActionId.ChangeVariableType)

override def contribute(
params: l.CodeActionParams,
token: CancelToken,
)(implicit ec: ExecutionContext): Future[Seq[l.CodeAction]] = {
val path = params.getTextDocument().getUri().toAbsolutePath
val requestRange = params.getRange()

val matchingDiagnostics =
params.getContext().getDiagnostics().asScala.toSeq.collect {
case diagnostic @ JavacDiagnostic.IncompatibleTypes()
if requestRange.overlapsWith(diagnostic.getRange()) =>
diagnostic
}

Future
.sequence {
matchingDiagnostics.map { diagnostic =>
val editParams = new l.TextDocumentPositionParams(
params.getTextDocument(),
diagnostic.getRange().getStart(),
)
compilers
.codeAction(
editParams,
token,
CodeActionId.ChangeVariableType,
Some(diagnostic.getRange()),
)
.map { edits =>
if (edits.isEmpty()) None
else Some(build(path, diagnostic, edits))
}
}
}
.map(_.flatten.distinctBy(_.getEdit()))
}

private def build(
path: AbsolutePath,
diagnostic: l.Diagnostic,
edits: java.util.List[l.TextEdit],
): l.CodeAction = {
CodeActionBuilder.build(
ChangeVariableType.title,
kind,
diagnostics = List(diagnostic),
changes = Seq(path -> edits.asScala.toSeq),
)
}
}

object ChangeVariableType {
val title = "Change variable type to match assigned value"
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class CodeActionProvider(
new GenerateGettersSetters(javaTrees, buffers),
new GenerateEqualsHashCodeToString(javaTrees, buffers),
new AddMissingReturnStatement(javaTrees, buffers),
new ChangeVariableType(compilers),
new JavaExtractMethodCodeAction(javaTrees, compilers),
)

Expand Down
31 changes: 3 additions & 28 deletions metals/src/main/scala/scala/meta/internal/parsing/JavaTrees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ class JavaTrees(buffers: Buffers) {
if (positionContains(targetOffset, nodeStart, nodeEnd)) {
val name = node.getName().toString()
val actualNodeStart =
findNameOffset(text, nodeStart, nodeEnd, name)
Positions
.findNameOffset(text, nodeStart, nodeEnd, name)
.getOrElse(nodeStart)
val actualNodeEnd = actualNodeStart + name.length()
if (positionContains(targetOffset, actualNodeStart, actualNodeEnd)) {
Expand Down Expand Up @@ -540,7 +541,7 @@ class JavaTrees(buffers: Buffers) {
endPos: Int,
name: String,
): Option[JavaRange] = {
findNameOffset(text, startPos, endPos, name).map { offset =>
Positions.findNameOffset(text, startPos, endPos, name).map { offset =>
val endOffset = offset + name.length()
JavaRange(
Positions.toLspRange(lineMap, offset, endOffset, text),
Expand All @@ -550,32 +551,6 @@ class JavaTrees(buffers: Buffers) {
}
}

private def findNameOffset(
text: String,
startPos: Int,
endPos: Int,
name: String,
): Option[Int] = {
if (startPos < 0 || endPos < 0) None
else {
val searchEnd = Math.min(endPos, text.length())
(startPos until searchEnd)
.find { offset =>
val endOffset = offset + name.length()
// Char at `offset` is a valid identifier start.
Character.isJavaIdentifierStart(text.charAt(offset)) &&
// Substring at `offset` matches the target name exactly.
text.startsWith(name, offset) &&
// Left boundary: previous char does not continue an identifier.
(offset == 0 ||
!Character.isJavaIdentifierPart(text.charAt(offset - 1))) &&
// Right boundary: next char is end-of-text or does not continue an identifier.
(endOffset >= text.length() ||
!Character.isJavaIdentifierPart(text.charAt(endOffset)))
}
}
}

private case class ParsedJavaCompilationUnit(
tree: CompilationUnitTree,
diagnostics: List[l.Diagnostic],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
public class CodeActionId {
public static final String ConvertToNamedArguments = "ConvertToNamedArguments";
public static final String ChangeVariableType = "ChangeVariableType";
public static final String ExtractMethod = "ExtractMethod";
public static final String ImplementAbstractMembers = "ImplementAbstractMembers";
public static final String ImportMissingSymbol = "ImportMissingSymbol";
Expand Down
Loading
Loading