Skip to content

Commit 6b4d8c9

Browse files
committed
feat: implement Convert to collect code action
1 parent 01974d7 commit 6b4d8c9

File tree

5 files changed

+432
-0
lines changed

5 files changed

+432
-0
lines changed

docs/features/code-actions.md

+6
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ It converts a chain of `map`, `flatMap`, `filter` and `filterNot` methods into a
5555

5656
![To For Comprehension](./gifs/FlatMapToForComprehension.gif)
5757

58+
## filter then map to collect
59+
60+
It converts a chain of `filter` and `map` methods into a `collect` method.
61+
62+
![To Collect](./gifs/FilterMapToCollect.gif)
63+
5864
## Implement Abstract Members of the Parent Type
5965

6066
Upon inheriting from a type, you also have to implement its abstract members. But manually looking them all up and copying their signature is time consuming, isn't it? You can just use this code action instead.
40.9 KB
Loading

metals/src/main/scala/scala/meta/internal/metals/codeactions/CodeActionProvider.scala

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ final class CodeActionProvider(
4444
new InlineValueCodeAction(trees, compilers, languageClient),
4545
new ConvertToNamedArguments(trees, compilers, languageClient),
4646
new FlatMapToForComprehensionCodeAction(trees, buffers),
47+
new FilterMapToCollectCodeAction(trees),
4748
new MillifyDependencyCodeAction(buffers),
4849
new MillifyScalaCliDependencyCodeAction(buffers),
4950
new ConvertCommentCodeAction(buffers),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package scala.meta.internal.metals.codeactions
2+
3+
import scala.concurrent.ExecutionContext
4+
import scala.concurrent.Future
5+
6+
import scala.meta._
7+
import scala.meta.internal.metals.MetalsEnrichments._
8+
import scala.meta.internal.metals.codeactions.CodeAction
9+
import scala.meta.internal.metals.codeactions.CodeActionBuilder
10+
import scala.meta.internal.parsing.Trees
11+
import scala.meta.pc.CancelToken
12+
13+
import org.eclipse.lsp4j.CodeActionParams
14+
import org.eclipse.{lsp4j => l}
15+
16+
class FilterMapToCollectCodeAction(trees: Trees) extends CodeAction {
17+
override def kind: String = l.CodeActionKind.RefactorRewrite
18+
19+
override def contribute(params: CodeActionParams, token: CancelToken)(implicit
20+
ec: ExecutionContext
21+
): Future[Seq[l.CodeAction]] = Future {
22+
val uri = params.getTextDocument().getUri()
23+
24+
val path = uri.toAbsolutePath
25+
val range = params.getRange()
26+
27+
trees
28+
.findLastEnclosingAt[Term.Apply](path, range.getStart())
29+
.flatMap(findFilterMapChain)
30+
.map(toTextEdit(_))
31+
.map(toCodeAction(uri, _))
32+
.toSeq
33+
}
34+
35+
private def toTextEdit(chain: FilterMapChain) = {
36+
val param = chain.filterFn.params.head
37+
val paramName = Term.Name(param.name.value)
38+
val paramPatWithType = param.decltpe match {
39+
case Some(tpe) => Pat.Typed(Pat.Var(paramName), tpe)
40+
case None => Pat.Var(paramName)
41+
}
42+
43+
val collectCall = Term.Apply(
44+
fun = Term.Select(chain.qual, Term.Name("collect")),
45+
argClause = Term.ArgClause(
46+
values = List(
47+
Term.PartialFunction(
48+
cases = List(
49+
Case(
50+
pat = paramPatWithType,
51+
cond = Some(chain.filterFn.renameParam(paramName)),
52+
body = chain.mapFn.renameParam(paramName),
53+
)
54+
)
55+
)
56+
)
57+
),
58+
)
59+
new l.TextEdit(chain.tree.pos.toLsp, collectCall.syntax)
60+
}
61+
62+
private def toCodeAction(uri: String, textEdit: l.TextEdit): l.CodeAction =
63+
CodeActionBuilder.build(
64+
title = FilterMapToCollectCodeAction.title,
65+
kind = this.kind,
66+
changes = List(uri.toAbsolutePath -> List(textEdit)),
67+
)
68+
69+
private implicit class FunctionOps(fn: Term.Function) {
70+
def renameParam(to: Term.Name): Term = {
71+
val fnParamName = fn.params.head.name.value
72+
fn.body
73+
.transform { case Term.Name(name) if name == fnParamName => to }
74+
.asInstanceOf[Term]
75+
}
76+
}
77+
78+
private def findFilterMapChain(tree: Term.Apply): Option[FilterMapChain] = {
79+
val x = Term.Name("x")
80+
def extractFunction(arg: Tree): Option[Term.Function] = arg match {
81+
case fn: Term.Function => Some(fn)
82+
case Term.Block(List(fn: Term.Function)) => extractFunction(fn)
83+
case ref: Term.Name => {
84+
Some(
85+
Term.Function(
86+
UnaryParameterList(x),
87+
Term.Apply(ref, Term.ArgClause(List(x))),
88+
)
89+
)
90+
}
91+
case _ => None
92+
}
93+
94+
def findChain(tree: Term.Apply): Option[FilterMapChain] =
95+
tree match {
96+
case MapFunctionApply(FilterFunctionApply(base, filterArg), mapArg) =>
97+
for {
98+
filterFn <- extractFunction(filterArg)
99+
mapFn <- extractFunction(mapArg)
100+
} yield FilterMapChain(tree, base, filterFn, mapFn)
101+
case _ => None
102+
}
103+
104+
findChain(tree).orElse {
105+
// If we're inside the chain, look at our parent
106+
tree.parent.flatMap {
107+
// We're in a method call or function, look at parent apply
108+
case Term.Select(_, Term.Name("map" | "filter")) | Term.Function(_) =>
109+
tree.parent
110+
.flatMap(_.parent)
111+
.collectFirst { case parent: Term.Apply => parent }
112+
.flatMap(findChain)
113+
case _ => None
114+
}
115+
}
116+
}
117+
118+
private object UnaryParameterList {
119+
def unapply(tree: Tree): Option[Name] = tree match {
120+
case Term.Param(_, name, _, _) => Some(name)
121+
case _ => None
122+
}
123+
def apply(name: Name): List[Term.Param] = List(
124+
Term.Param(Nil, name, None, None)
125+
)
126+
}
127+
128+
private case class FunctionApply(val name: String) {
129+
def unapply(tree: Tree): Option[(Term, Term)] = tree match {
130+
case Term.Apply(Term.Select(base, Term.Name(`name`)), List(args)) =>
131+
Some((base, args))
132+
case _ => None
133+
}
134+
}
135+
private val FilterFunctionApply = new FunctionApply("filter")
136+
private val MapFunctionApply = new FunctionApply("map")
137+
138+
private case class FilterMapChain(
139+
tree: Term.Apply,
140+
qual: Term,
141+
filterFn: Term.Function,
142+
mapFn: Term.Function,
143+
)
144+
}
145+
146+
object FilterMapToCollectCodeAction {
147+
val title = "Convert to collect"
148+
}

0 commit comments

Comments
 (0)