Skip to content

Commit a8bc74b

Browse files
committed
Support go to def, rename, etc. in worksheets
1 parent a486832 commit a8bc74b

File tree

4 files changed

+163
-25
lines changed

4 files changed

+163
-25
lines changed

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class DottyLanguageServer extends LanguageServer
183183
val worksheetMode = isWorksheet(uri)
184184

185185
val (text, positionMapper) =
186-
if (worksheetMode) (wrapWorksheet(document.getText), Some(worksheetPositionMapper _))
186+
if (worksheetMode) (wrapWorksheet(document.getText), Some(toUnwrappedPosition _))
187187
else (document.getText, None)
188188

189189
val diags = driver.run(uri, text)
@@ -204,7 +204,7 @@ class DottyLanguageServer extends LanguageServer
204204
assert(change.getRange == null, "TextDocumentSyncKind.Incremental support is not implemented")
205205

206206
val (text, positionMapper) =
207-
if (worksheetMode) (wrapWorksheet(change.getText), Some(worksheetPositionMapper _))
207+
if (worksheetMode) (wrapWorksheet(change.getText), Some(toUnwrappedPosition _))
208208
else (change.getText, None)
209209

210210
val diags = driver.run(uri, text)
@@ -317,7 +317,7 @@ class DottyLanguageServer extends LanguageServer
317317
(Nil, Include.overriding)
318318
}
319319
val defs = Interactive.namedTrees(trees, include, sym)
320-
defs.flatMap(d => location(d.namePos)).asJava
320+
defs.flatMap(d => location(d.namePos, positionMapperFor(d.source))).asJava
321321
}
322322
}
323323

@@ -340,7 +340,7 @@ class DottyLanguageServer extends LanguageServer
340340
Include.references | Include.overriding | (if (includeDeclaration) Include.definitions else 0)
341341
val refs = Interactive.findTreesMatching(trees, includes, sym)
342342

343-
refs.flatMap(ref => location(ref.namePos)).asJava
343+
refs.flatMap(ref => location(ref.namePos, positionMapperFor(ref.source))).asJava
344344
}
345345
}
346346

@@ -363,7 +363,7 @@ class DottyLanguageServer extends LanguageServer
363363
val changes = refs.groupBy(ref => toUri(ref.source).toString)
364364
.mapValues(refs =>
365365
refs.flatMap(ref =>
366-
range(ref.namePos).map(nameRange => new TextEdit(nameRange, newName))).asJava)
366+
range(ref.namePos, positionMapperFor(ref.source)).map(nameRange => new TextEdit(nameRange, newName))).asJava)
367367

368368
new WorkspaceEdit(changes.asJava)
369369
}
@@ -383,7 +383,7 @@ class DottyLanguageServer extends LanguageServer
383383
val refs = Interactive.namedTrees(uriTrees, Include.references | Include.overriding, sym)
384384
(for {
385385
ref <- refs if !ref.tree.symbol.isConstructor
386-
nameRange <- range(ref.namePos)
386+
nameRange <- range(ref.namePos, positionMapperFor(ref.source))
387387
} yield new DocumentHighlight(nameRange, DocumentHighlightKind.Read)).asJava
388388
}
389389
}
@@ -416,8 +416,8 @@ class DottyLanguageServer extends LanguageServer
416416

417417
val defs = Interactive.namedTrees(uriTrees, includeReferences = false, _ => true)
418418
(for {
419-
d <- defs
420-
info <- symbolInfo(d.tree.symbol, d.namePos)
419+
d <- defs if !isWorksheetWrapper(d)
420+
info <- symbolInfo(d.tree.symbol, d.namePos, positionMapperFor(d.source))
421421
} yield JEither.forLeft(info)).asJava
422422
}
423423

@@ -429,7 +429,7 @@ class DottyLanguageServer extends LanguageServer
429429

430430
val trees = driver.allTrees
431431
val defs = Interactive.namedTrees(trees, includeReferences = false, nameSubstring = query)
432-
defs.flatMap(d => symbolInfo(d.tree.symbol, d.namePos))
432+
defs.flatMap(d => symbolInfo(d.tree.symbol, d.namePos, positionMapperFor(d.source)))
433433
}.asJava
434434
}
435435

@@ -473,9 +473,12 @@ object DottyLanguageServer {
473473

474474
/** Convert an lsp4j.Position to a SourcePosition */
475475
def sourcePosition(driver: InteractiveDriver, uri: URI, pos: lsp4j.Position): SourcePosition = {
476+
val actualPosition =
477+
if (isWorksheet(uri)) toWrappedPosition(pos)
478+
else pos
476479
val source = driver.openedFiles(uri)
477480
if (source.exists) {
478-
val p = Positions.Position(source.lineToOffset(pos.getLine) + pos.getCharacter)
481+
val p = Positions.Position(source.lineToOffset(actualPosition.getLine) + actualPosition.getCharacter)
479482
new SourcePosition(source, p)
480483
}
481484
else NoSourcePosition
@@ -528,6 +531,10 @@ object DottyLanguageServer {
528531
private def isWorksheet(uri: URI): Boolean =
529532
uri.toString.endsWith(".sc")
530533

534+
/** Does this sourcefile represent a worksheet? */
535+
private def isWorksheet(sourcefile: SourceFile): Boolean =
536+
sourcefile.file.extension == "sc"
537+
531538
/** Wrap the source of a worksheet inside an `object`. */
532539
private def wrapWorksheet(source: String): String =
533540
s"""object Worksheet {
@@ -544,13 +551,48 @@ object DottyLanguageServer {
544551
* @param position The position as seen by the compiler (after wrapping)
545552
* @return The position in the actual source file (before wrapping).
546553
*/
547-
private def worksheetPositionMapper(position: SourcePosition): SourcePosition = {
554+
private def toUnwrappedPosition(position: SourcePosition): SourcePosition = {
548555
new SourcePosition(position.source, position.pos, position.outer) {
549556
override def startLine: Int = position.startLine - 1
550557
override def endLine: Int = position.endLine - 1
551558
}
552559
}
553560

561+
/**
562+
* Map `position` in an unwrapped worksheet to the same position in the wrapped source.
563+
*
564+
* Because worksheet are wrapped in an `object`, the positions in the source are one line
565+
* above from what the compiler sees.
566+
*
567+
* @see wrapWorksheet
568+
* @param position The position as seen by VSCode (before wrapping)
569+
* @return The position as seen by the compiler (after wrapping)
570+
*/
571+
private def toWrappedPosition(position: lsp4j.Position): lsp4j.Position = {
572+
new lsp4j.Position(position.getLine + 1, position.getCharacter)
573+
}
574+
575+
/**
576+
* Returns the position mapper necessary to unwrap positions for `sourcefile`. If `sourcefile` is
577+
* not a worksheet, no mapper is necessary. Otherwise, return `toUnwrappedPosition`.
578+
*/
579+
private def positionMapperFor(sourcefile: SourceFile): Option[SourcePosition => SourcePosition] = {
580+
if (isWorksheet(sourcefile)) Some(toUnwrappedPosition _)
581+
else None
582+
}
583+
584+
/**
585+
* Is `sourceTree` the wrapper object that we put around worksheet sources?
586+
*
587+
* @see wrapWorksheet
588+
*/
589+
def isWorksheetWrapper(sourceTree: SourceTree)(implicit ctx: Context): Boolean = {
590+
val symbol = sourceTree.tree.symbol
591+
isWorksheet(sourceTree.source) &&
592+
symbol.name.toString == "Worksheet$" &&
593+
symbol.owner == ctx.definitions.EmptyPackageClass
594+
}
595+
554596
/** Create an lsp4j.CompletionItem from a Symbol */
555597
def completionItem(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItem = {
556598
def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = {
@@ -596,7 +638,7 @@ object DottyLanguageServer {
596638
}
597639

598640
/** Create an lsp4j.SymbolInfo from a Symbol and a SourcePosition */
599-
def symbolInfo(sym: Symbol, pos: SourcePosition)(implicit ctx: Context): Option[lsp4j.SymbolInformation] = {
641+
def symbolInfo(sym: Symbol, pos: SourcePosition, positionMapper: Option[SourcePosition => SourcePosition])(implicit ctx: Context): Option[lsp4j.SymbolInformation] = {
600642
def symbolKind(sym: Symbol)(implicit ctx: Context): lsp4j.SymbolKind = {
601643
import lsp4j.{SymbolKind => SK}
602644

@@ -621,6 +663,6 @@ object DottyLanguageServer {
621663
else
622664
null
623665

624-
location(pos).map(l => new lsp4j.SymbolInformation(name, symbolKind(sym), l, containerName))
666+
location(pos, positionMapper).map(l => new lsp4j.SymbolInformation(name, symbolKind(sym), l, containerName))
625667
}
626668
}

language-server/test/dotty/tools/languageserver/DefinitionTest.scala

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,6 @@ class DefinitionTest {
5555
}
5656

5757
@Test def goToDefNamedArgOverload: Unit = {
58-
val m9 = new CodeMarker("m9")
59-
val m10 = new CodeMarker("m10")
60-
val m11 = new CodeMarker("m11")
61-
val m12 = new CodeMarker("m12")
62-
val m13 = new CodeMarker("m13")
63-
val m14 = new CodeMarker("m14")
6458

6559
code"""object Foo {
6660
def foo(${m1}x${m2}: String): String = ${m3}x${m4}
@@ -88,10 +82,6 @@ class DefinitionTest {
8882
}
8983

9084
@Test def goToConstructorNamedArgOverload: Unit = {
91-
val m9 = new CodeMarker("m9")
92-
val m10 = new CodeMarker("m10")
93-
val m11 = new CodeMarker("m11")
94-
val m12 = new CodeMarker("m12")
9585

9686
withSources(
9787
code"""class Foo(${m1}x${m2}: String) {
@@ -110,8 +100,6 @@ class DefinitionTest {
110100
}
111101

112102
@Test def goToParamCopyMethod: Unit = {
113-
val m9 = new CodeMarker("m9")
114-
val m10 = new CodeMarker("m10")
115103

116104
withSources(
117105
code"""case class Foo(${m1}x${m2}: Int, ${m3}y${m4}: String)""",

language-server/test/dotty/tools/languageserver/WorksheetTest.scala

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dotty.tools.languageserver
22

33
import org.junit.Test
4+
import org.eclipse.lsp4j.{CompletionItemKind, DocumentHighlightKind, SymbolKind}
45

56
import dotty.tools.languageserver.util.Code._
7+
import dotty.tools.languageserver.util.embedded.CodeMarker
68

79
class WorksheetTest {
810

@@ -82,4 +84,104 @@ class WorksheetTest {
8284
.evaluate(m1, "1:val foo: Int = 1\nval bar: Int = 2")
8385
}
8486

87+
@Test def worksheetCompletion(): Unit = {
88+
ws"""class Foo { def bar = 123 }
89+
val x = new Foo
90+
x.b${m1}""".withSource
91+
.completion(m1, Set(("bar", CompletionItemKind.Method, "=> Int")))
92+
}
93+
94+
@Test def worksheetGoToDefinition(): Unit = {
95+
96+
withSources(
97+
code"""class ${m11}Baz${m12}""",
98+
ws"""class ${m1}Foo${m2} { def ${m3}bar${m4} = new ${m5}Baz${m6} }
99+
val x = new ${m7}Foo${m8}
100+
x.${m9}bar${m10}"""
101+
).definition(m1 to m2, List(m1 to m2))
102+
.definition(m3 to m4, List(m3 to m4))
103+
.definition(m5 to m6, List(m11 to m12))
104+
.definition(m7 to m8, List(m1 to m2))
105+
.definition(m9 to m10, List(m3 to m4))
106+
.definition(m11 to m12, List(m11 to m12))
107+
}
108+
109+
@Test def worksheetReferences(): Unit = {
110+
111+
withSources(
112+
code"""class ${m11}Baz${m12}""",
113+
ws"""class ${m1}Foo${m2} { def ${m3}bar${m4} = new ${m9}Baz${m10} }
114+
val x = new ${m5}Foo${m6}
115+
x.${m7}bar${m8}"""
116+
).references(m1 to m2, List(m5 to m6))
117+
.references(m3 to m4, List(m7 to m8))
118+
.references(m11 to m12, List(m9 to m10))
119+
}
120+
121+
@Test def worksheetRename(): Unit = {
122+
123+
def sources =
124+
withSources(
125+
code"""class ${m9}Baz${m10}""",
126+
ws"""class ${m1}Foo${m2}(baz: ${m3}Baz${m4})
127+
val x = new ${m5}Foo${m6}(new ${m7}Baz${m8})"""
128+
)
129+
130+
def testRenameFooFrom(m: CodeMarker) =
131+
sources.rename(m, "Bar", Set(m1 to m2, m5 to m6))
132+
133+
def testRenameBazFrom(m: CodeMarker) =
134+
sources.rename(m, "Bar", Set(m3 to m4, m7 to m8, m9 to m10))
135+
136+
testRenameFooFrom(m1)
137+
testRenameBazFrom(m3)
138+
testRenameFooFrom(m5)
139+
testRenameBazFrom(m7)
140+
testRenameBazFrom(m9)
141+
}
142+
143+
@Test def worksheetHighlight(): Unit = {
144+
ws"""class ${m1}Foo${m2} { def ${m3}bar${m4} = 123 }
145+
val x = new ${m5}Foo${m6}
146+
x.${m7}bar${m8}""".withSource
147+
.highlight(m1 to m2, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
148+
.highlight(m3 to m4, (m3 to m4, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
149+
.highlight(m5 to m6, (m1 to m2, DocumentHighlightKind.Read), (m5 to m6, DocumentHighlightKind.Read))
150+
.highlight(m7 to m8, (m3 to m4, DocumentHighlightKind.Read), (m7 to m8, DocumentHighlightKind.Read))
151+
}
152+
153+
def hoverContent(typeInfo: String, comment: String): Option[String] =
154+
Some(s"""```scala
155+
|$typeInfo
156+
|$comment
157+
|```""".stripMargin)
158+
@Test def worksheetHover(): Unit = {
159+
ws"""/** A class */ class ${m1}Foo${m2} { /** A method */ def ${m3}bar${m4} = 123 }
160+
val x = new ${m5}Foo${m6}
161+
x.${m7}bar${m8}""".withSource
162+
.hover(m1 to m2, hoverContent("Worksheet.Foo", "/** A class */"))
163+
.hover(m3 to m4, hoverContent("Int", "/** A method */"))
164+
.hover(m5 to m6, hoverContent("Worksheet.Foo", "/** A class */"))
165+
.hover(m7 to m8, hoverContent("Int", "/** A method */"))
166+
}
167+
168+
@Test def worksheetDocumentSymbol(): Unit = {
169+
ws"""class ${m1}Foo${m2} {
170+
def ${m3}bar${m4} = 123
171+
}""".withSource
172+
.documentSymbol(m1, (m1 to m2).symInfo("Foo", SymbolKind.Class, "Worksheet$"),
173+
(m3 to m4).symInfo("bar", SymbolKind.Method, "Foo"))
174+
}
175+
176+
@Test def worksheetSymbol(): Unit = {
177+
withSources(
178+
ws"""class ${m1}Foo${m2} {
179+
def ${m3}bar${m4} = 123
180+
}""",
181+
code"""class ${m5}Baz${m6}"""
182+
).symbol("Foo", (m1 to m2).symInfo("Foo", SymbolKind.Class, "Worksheet$"))
183+
.symbol("bar", (m3 to m4).symInfo("bar", SymbolKind.Method, "Foo"))
184+
.symbol("Baz", (m5 to m6).symInfo("Baz", SymbolKind.Class))
185+
}
186+
85187
}

language-server/test/dotty/tools/languageserver/util/Code.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ object Code {
2020
val m6 = new CodeMarker("m6")
2121
val m7 = new CodeMarker("m7")
2222
val m8 = new CodeMarker("m8")
23+
val m9 = new CodeMarker("m9")
24+
val m10 = new CodeMarker("m10")
25+
val m11 = new CodeMarker("m11")
26+
val m12 = new CodeMarker("m12")
27+
val m13 = new CodeMarker("m13")
28+
val m14 = new CodeMarker("m14")
2329

2430
implicit class CodeHelper(val sc: StringContext) extends AnyVal {
2531

0 commit comments

Comments
 (0)