/
CompletionPos.scala
157 lines (139 loc) Β· 4.39 KB
/
CompletionPos.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
package scala.meta.internal.pc
package completions
import java.net.URI
import scala.annotation.tailrec
import scala.meta.internal.mtags.MtagsEnrichments.*
import scala.meta.internal.tokenizers.Chars
import scala.meta.pc.OffsetParams
import dotty.tools.dotc.ast.tpd.*
import dotty.tools.dotc.ast.untpd.ImportSelector
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.StdNames.*
import dotty.tools.dotc.util.SourcePosition
import dotty.tools.dotc.util.Spans
import org.eclipse.lsp4j as l
enum CompletionKind:
case Empty, Scope, Members
case class CompletionPos(
kind: CompletionKind,
start: Int,
end: Int,
query: String,
cursorPos: SourcePosition,
sourceUri: URI,
):
def sourcePos: SourcePosition = cursorPos.withSpan(Spans.Span(start, end))
def stripSuffixEditRange: l.Range =
new l.Range(cursorPos.offsetToPos(start), cursorPos.offsetToPos(end))
def toEditRange: l.Range =
cursorPos.withStart(start).withEnd(cursorPos.point).toLsp
end CompletionPos
object CompletionPos:
def infer(
cursorPos: SourcePosition,
offsetParams: OffsetParams,
treePath: List[Tree],
)(using Context): CompletionPos =
infer(cursorPos, offsetParams.uri, offsetParams.text, treePath)
def infer(
cursorPos: SourcePosition,
uri: URI,
text: String,
treePath: List[Tree],
)(using Context): CompletionPos =
val start = inferIdentStart(cursorPos, text, treePath)
val end = inferIdentEnd(cursorPos, text)
val query = text.substring(start, end)
val prevIsDot =
if start - 1 >= 0 then text.charAt(start - 1) == '.' else false
val kind =
if prevIsDot then CompletionKind.Members
else if isImportOrExportSelect(cursorPos, treePath) then
CompletionKind.Members
else if query.isEmpty then CompletionKind.Empty
else CompletionKind.Scope
CompletionPos(
kind,
start,
end,
query,
cursorPos,
uri,
)
end infer
/**
* Infer the indentation by counting the number of spaces in the given line.
*
* @param lineOffset the offset position of the beginning of the line
*/
private[completions] def inferIndent(
lineOffset: Int,
text: String,
): (Int, Boolean) =
var i = 0
var tabIndented = false
while lineOffset + i < text.length && {
val char = text.charAt(lineOffset + i)
if char == '\t' then
tabIndented = true
true
else char == ' '
}
do i += 1
(i, tabIndented)
end inferIndent
private def isImportOrExportSelect(
pos: SourcePosition,
path: List[Tree],
)(using Context): Boolean =
@tailrec
def loop(enclosing: List[Tree]): Boolean =
enclosing match
case head :: tl if !head.sourcePos.contains(pos) => loop(tl)
case (tree: (Import | Export)) :: _ =>
tree.selectors.exists(_.imported.sourcePos.contains(pos))
case _ => false
loop(path)
/**
* Returns the start offset of the identifier starting as the given offset position.
*/
private def inferIdentStart(
pos: SourcePosition,
text: String,
path: List[Tree],
)(using Context): Int =
def fallback: Int =
var i = pos.point - 1
while i >= 0 && Chars.isIdentifierPart(text.charAt(i)) do i -= 1
i + 1
def loop(enclosing: List[Tree]): Int =
enclosing match
case Nil => fallback
case head :: tl =>
if !head.sourcePos.contains(pos) then loop(tl)
else
head match
case i: Ident => i.sourcePos.point
case s: Select =>
if s.name.toTermName == nme.ERROR || s.span.exists && pos.span.point < s.span.point
then fallback
else s.span.point
case Import(_, sel) =>
sel
.collectFirst {
case ImportSelector(imported, renamed, _)
if imported.sourcePos.contains(pos) =>
imported.sourcePos.point
}
.getOrElse(fallback)
case _ => fallback
loop(path)
end inferIdentStart
/**
* Returns the end offset of the identifier starting as the given offset position.
*/
private def inferIdentEnd(pos: SourcePosition, text: String): Int =
var i = pos.point
while i < text.length && Chars.isIdentifierPart(text.charAt(i)) do i += 1
i
end CompletionPos