-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathPcInlineValueProvider.scala
309 lines (278 loc) · 10 KB
/
PcInlineValueProvider.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
package dotty.tools.pc
import scala.meta.internal.pc.Definition
import scala.meta.internal.pc.InlineValueProvider
import scala.meta.internal.pc.InlineValueProvider.Errors
import scala.meta.internal.pc.RangeOffset
import scala.meta.internal.pc.Reference
import scala.meta.pc.OffsetParams
import dotty.tools.dotc.ast.NavigateAST
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.StdNames
import dotty.tools.dotc.core.Symbols.Symbol
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 dotty.tools.pc.utils.InteractiveEnrichments.*
import dotty.tools.pc.IndexedContext.Result
import org.eclipse.lsp4j as l
final class PcInlineValueProvider(
driver: InteractiveDriver,
val params: OffsetParams
) extends WithSymbolSearchCollector[Option[Occurence]](driver, params):
// We return a result or an error
def getInlineTextEdits(): Either[String, List[l.TextEdit]] =
defAndRefs() match {
case Right((defn, refs)) =>
val edits =
if (defn.shouldBeRemoved) {
val defEdit = definitionTextEdit(defn)
val refsEdits = refs.map(referenceTextEdit(defn))
defEdit :: refsEdits
} else refs.map(referenceTextEdit(defn))
Right(edits)
case Left(error) => Left(error)
}
private def referenceTextEdit(
definition: Definition
)(ref: Reference): l.TextEdit =
if (definition.requiresBrackets && ref.requiresBrackets)
new l.TextEdit(
ref.range,
s"""(${ref.rhs})"""
)
else new l.TextEdit(ref.range, ref.rhs)
private def definitionTextEdit(definition: Definition): l.TextEdit =
new l.TextEdit(
extend(
definition.rangeOffsets.start,
definition.rangeOffsets.end,
definition.range
),
""
)
private def extend(
startOffset: Int,
endOffset: Int,
range: l.Range
): l.Range = {
val (startWithSpace, endWithSpace): (Int, Int) =
extendRangeToIncludeWhiteCharsAndTheFollowingNewLine(
text
)(startOffset, endOffset)
val startPos = new l.Position(
range.getStart.getLine,
range.getStart.getCharacter - (startOffset - startWithSpace)
)
val endPos =
if (endWithSpace - 1 >= 0 && text(endWithSpace - 1) == '\n')
new l.Position(range.getEnd.getLine + 1, 0)
else
new l.Position(
range.getEnd.getLine,
range.getEnd.getCharacter + endWithSpace - endOffset
)
new l.Range(startPos, endPos)
}
val position: l.Position = pos.toLsp.getStart().nn
override def collect(parent: Option[Tree])(
tree: Tree | EndMarker,
pos: SourcePosition,
sym: Option[Symbol]
): Option[Occurence] =
tree match
case tree: Tree =>
val (adjustedPos, _) = pos.adjust(text)
Some(Occurence(tree, parent, adjustedPos))
case _ => None
def defAndRefs(): Either[String, (Definition, List[Reference])] =
val newctx = driver.currentCtx.fresh.setCompilationUnit(unit)
val allOccurences = result().flatten
for
definition <- allOccurences
.collectFirst { case Occurence(defn: ValDef, _, pos) =>
DefinitionTree(defn, pos)
}
.toRight(Errors.didNotFindDefinition)
path = Interactive.pathTo(unit.tpdTree, definition.tree.rhs.span)(using newctx)
indexedContext = IndexedContext(Interactive.contextOfPath(path)(using newctx))
symbols = symbolsUsedInDefn(definition.tree.rhs).filter(indexedContext.lookupSym(_) == Result.InScope)
references <- getReferencesToInline(definition, allOccurences, symbols)
yield
val (deleteDefinition, refsEdits) = references
val defPos = definition.tree.sourcePos
val defEdit = Definition(
defPos.toLsp,
RangeOffset(defPos.start, defPos.end),
definitionRequiresBrackets(definition.tree.rhs)(using newctx),
deleteDefinition
)
(defEdit, refsEdits)
end for
end defAndRefs
private def stripIndentPrefix(rhs: String, refIndent: String, defIndent: String): String =
val rhsLines = rhs.split("\n").toList
rhsLines match
case h :: Nil => rhs
case h :: t =>
val header = if h.startsWith("{") then h else "\n" ++ refIndent ++ " " ++ h
header ++ t.map(refIndent ++ _.stripPrefix(defIndent)).mkString("\n", "\n", "")
case Nil => rhs
private def definitionRequiresBrackets(tree: Tree)(using Context): Boolean =
NavigateAST
.untypedPath(tree.span)
.headOption
.map {
case _: untpd.If => true
case _: untpd.Function => true
case _: untpd.Match => true
case _: untpd.ForYield => true
case _: untpd.InfixOp => true
case _: untpd.ParsedTry => true
case _: untpd.Try => true
case _: untpd.Block => true
case _: untpd.Typed => true
case _ => false
}
.getOrElse(false)
end definitionRequiresBrackets
private def referenceRequiresBrackets(tree: Tree)(using Context): Boolean =
NavigateAST.untypedPath(tree.span) match
case (_: untpd.InfixOp) :: _ => true
case _ =>
tree match
case _: Apply => StdNames.nme.raw.isUnary(tree.symbol.name)
case _: Select => true
case _: Ident => true
case _ => false
end referenceRequiresBrackets
private def extendWithSurroundingParens(pos: SourcePosition) =
/** Move `point` by `step` as long as the character at `point` is `acceptedChar` */
def extend(point: Int, acceptedChar: Char, step: Int): Int =
val newPoint = point + step
if newPoint > 0 && newPoint < text.length &&
text(newPoint) == acceptedChar
then extend(newPoint, acceptedChar, step)
else point
val adjustedStart = extend(pos.start, '(', -1)
val adjustedEnd = extend(pos.end - 1, ')', 1) + 1
text.slice(adjustedStart, adjustedEnd).mkString
private def symbolsUsedInDefn(rhs: Tree): Set[Symbol] =
def collectNames(
symbols: Set[Symbol],
tree: Tree
): Set[Symbol] =
tree match
case id: (Ident | Select)
if !id.symbol.is(Synthetic) && !id.symbol.is(Implicit) =>
symbols + tree.symbol
case _ => symbols
val traverser = new DeepFolder[Set[Symbol]](collectNames)
traverser(Set(), rhs)
end symbolsUsedInDefn
private def getReferencesToInline(
definition: DefinitionTree,
allOccurences: List[Occurence],
symbols: Set[Symbol]
): Either[String, (Boolean, List[Reference])] =
val defIsLocal = definition.tree.symbol.ownersIterator
.drop(1)
.exists(e => e.isTerm)
def allreferences = allOccurences.filterNot(_.isDefn)
def inlineAll() =
makeRefsEdits(allreferences, symbols, definition).map((true, _))
if definition.tree.sourcePos.toLsp.encloses(position)
then if defIsLocal then inlineAll() else Left(Errors.notLocal)
else
allreferences match
case ref :: Nil if defIsLocal => inlineAll()
case list =>
for
ref <- list
.find(_.pos.toLsp.encloses(position))
.toRight(Errors.didNotFindReference)
refEdits <- makeRefsEdits(List(ref), symbols, definition)
yield (false, refEdits)
end if
end getReferencesToInline
extension (pos: SourcePosition)
def startColumnIndentPadding: String = {
val source = pos.source
val offset = pos.start
var idx = source.startOfLine(offset)
val pad = new StringBuilder
while (idx != offset && idx < source.content().length && source.content()(idx).isWhitespace) {
pad.append(source.content()(idx))
idx += 1
}
pad.result()
}
private def makeRefsEdits(
refs: List[Occurence],
symbols: Set[Symbol],
definition: DefinitionTree
): Either[String, List[Reference]] =
val newctx = driver.currentCtx.fresh.setCompilationUnit(unit)
def buildRef(occurrence: Occurence): Either[String, Reference] =
val path =
Interactive.pathTo(unit.tpdTree, occurrence.pos.span)(using newctx)
val indexedContext = IndexedContext(
Interactive.contextOfPath(path)(using newctx)
)
import indexedContext.ctx
val conflictingSymbols = symbols
.withFilter {
indexedContext.lookupSym(_) match
case IndexedContext.Result.Conflict => true
case _ => false
}
.map(_.fullNameBackticked)
if conflictingSymbols.isEmpty then
Right(
Reference(
occurrence.pos.toLsp,
stripIndentPrefix(
extendWithSurroundingParens(definition.tree.rhs.sourcePos),
occurrence.tree.startPos.startColumnIndentPadding,
definition.tree.startPos.startColumnIndentPadding
),
occurrence.parent.map(p =>
RangeOffset(p.sourcePos.start, p.sourcePos.end)
),
occurrence.parent
.map(p => referenceRequiresBrackets(p)(using newctx))
.getOrElse(false)
)
)
else Left(Errors.variablesAreShadowed(conflictingSymbols.mkString(", ")))
end buildRef
refs.foldLeft((Right(List())): Either[String, List[Reference]])((acc, r) =>
for
collectedEdits <- acc
currentEdit <- buildRef(r)
yield currentEdit :: collectedEdits
)
end makeRefsEdits
end PcInlineValueProvider
case class Occurence(tree: Tree, parent: Option[Tree], pos: SourcePosition):
def isDefn =
tree match
case _: ValDef => true
case _ => false
case class DefinitionTree(tree: ValDef, pos: SourcePosition)
case class RangeOffset(start: Int, end: Int)
case class Definition(
range: l.Range,
rangeOffsets: RangeOffset,
requiresBrackets: Boolean,
shouldBeRemoved: Boolean
)
case class Reference(
range: l.Range,
rhs: String,
parentOffsets: Option[RangeOffset],
requiresBrackets: Boolean
)