Skip to content

Commit 50be19d

Browse files
committed
worksheet/publishOutput returns a range instead of line
This command is sent from the server to the client when a statement in the current worksheet has finished running, but statements can be multi-line so returning just the line number loses some information. We use this extra information in vscode-dotty so that the hover pop-up containing the output of the statement is displayed when hovering over any part of the statement, and not just the last line. To avoid breaking this hover pop-up when using an old version of Dotty, some compatibiltiy code is necessary.
1 parent be8f708 commit 50be19d

File tree

9 files changed

+106
-79
lines changed

9 files changed

+106
-79
lines changed

docs/docs/usage/worksheet-mode-implementation-details.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ indicate that worksheet execution has produced some output.
6565
* The worksheet that produced this output.
6666
*/
6767
textDocument: VersionedTextDocumentIdentifier;
68-
68+
6969
/**
70-
* The line number of the expression that produced this output.
70+
* The range of the expression that produced this output.
7171
*/
72-
line: int;
73-
72+
range: Range;
73+
7474
/**
7575
* The output that has been produced.
7676
*/

language-server/src/dotty/tools/languageserver/worksheet/Worksheet.scala

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import dotty.tools.dotc.ast.tpd.{DefTree, Template, Tree, TypeDef}
44
import dotty.tools.dotc.core.Contexts.Context
55
import dotty.tools.dotc.interactive.SourceTree
66
import dotty.tools.dotc.util.Spans.Span
7-
import dotty.tools.dotc.util.SourceFile
7+
import dotty.tools.dotc.util.{ SourceFile, SourcePosition, NoSourcePosition }
88

99
import dotty.tools.dotc.core.Flags.Synthetic
1010

@@ -22,15 +22,15 @@ object Worksheet {
2222
*/
2323
def run(tree: SourceTree,
2424
treeLock: Object,
25-
sendMessage: (Int, String) => Unit,
25+
sendMessage: (SourcePosition, String) => Unit,
2626
cancelChecker: CancelChecker)(
2727
implicit ctx: Context): Unit = {
2828
// For now, don't try to run multiple evaluators in parallel, this would require
2929
// changes to the logic of Evaluator.get among other things.
3030
Evaluator.synchronized {
3131
Evaluator.get(cancelChecker) match {
3232
case None =>
33-
sendMessage(1, "Couldn't start the JVM.")
33+
sendMessage(NoSourcePosition, "Couldn't start the JVM.")
3434
case Some(evaluator) =>
3535
val queries = treeLock.synchronized {
3636
tree.tree match {
@@ -59,16 +59,15 @@ object Worksheet {
5959
}
6060

6161
/**
62-
* Extract the line number and source code corresponding to this tree
62+
* Extract the position and source code corresponding to this tree
6363
*
6464
* @param evaluator The JVM that runs the REPL.
6565
* @param tree The compiled tree to evaluate.
6666
* @param sourcefile The sourcefile of the worksheet.
6767
*/
68-
private def query(tree: Tree, sourcefile: SourceFile): (Int, String) = {
69-
val line = sourcefile.offsetToLine(tree.span.end)
68+
private def query(tree: Tree, sourcefile: SourceFile)(implicit ctx: Context): (SourcePosition, String) = {
7069
val source = sourcefile.content.slice(tree.span.start, tree.span.end).mkString
71-
(line, source)
70+
(tree.sourcePos, source)
7271
}
7372

7473
private def bounds(span: Span): (Int, Int) = (span.start, span.end)

language-server/src/dotty/tools/languageserver/worksheet/WorksheetMessages.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package dotty.tools.languageserver.worksheet
22

3-
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier
3+
import org.eclipse.lsp4j.{ Range, VersionedTextDocumentIdentifier }
44

55
// All case classes in this file should have zero-parameters secondary
66
// constructors to allow Gson to reflectively create instances on
@@ -17,6 +17,6 @@ case class WorksheetRunResult(success: Boolean) {
1717
}
1818

1919
/** The parameters to the `worksheet/publishOutput` notification. */
20-
case class WorksheetRunOutput(textDocument: VersionedTextDocumentIdentifier, line: Int, content: String) {
21-
def this() = this(null, 0, null)
20+
case class WorksheetRunOutput(textDocument: VersionedTextDocumentIdentifier, range: Range, content: String) {
21+
def this() = this(null, null, null)
2222
}

language-server/src/dotty/tools/languageserver/worksheet/WorksheetService.scala

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

33
import dotty.tools.dotc.core.Contexts.Context
4+
import dotty.tools.dotc.util.SourcePosition
45
import dotty.tools.dotc.interactive.InteractiveDriver
56
import dotty.tools.languageserver.DottyLanguageServer
7+
import dotty.tools.languageserver.DottyLanguageServer.range
68

79
import org.eclipse.lsp4j.jsonrpc._//{CancelChecker, CompletableFutures}
810
import org.eclipse.lsp4j.jsonrpc.services._//{JsonSegment, JsonRequest}
@@ -19,8 +21,8 @@ trait WorksheetService { thisServer: DottyLanguageServer =>
1921
val uri = new URI(params.textDocument.getUri)
2022
try {
2123
val driver = driverFor(uri)
22-
val sendMessage =
23-
(line: Int, msg: String) => client.publishOutput(WorksheetRunOutput(params.textDocument, line, msg))
24+
val sendMessage = (pos: SourcePosition, msg: String) =>
25+
client.publishOutput(WorksheetRunOutput(params.textDocument, range(pos).get, msg))
2426

2527
runWorksheet(driver, uri, sendMessage, cancelChecker)(driver.currentCtx)
2628
cancelChecker.checkCanceled()
@@ -41,7 +43,7 @@ trait WorksheetService { thisServer: DottyLanguageServer =>
4143
*/
4244
private def runWorksheet(driver: InteractiveDriver,
4345
uri: URI,
44-
sendMessage: (Int, String) => Unit,
46+
sendMessage: (SourcePosition, String) => Unit,
4547
cancelChecker: CancelChecker)(
4648
implicit ctx: Context): Unit = {
4749
val treeOpt = thisServer.synchronized {

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

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,86 +13,98 @@ import java.lang.System.{lineSeparator => nl}
1313
class WorksheetTest {
1414

1515
@Test def runExpression: Unit = {
16-
ws"${m1}2 + 2".withSource
17-
.run(m1, "1:val res0: Int = 4")
16+
ws"${m1}2 + 2${m2}".withSource
17+
.run(m1,
18+
((m1 to m2), "val res0: Int = 4"))
1819
}
1920

2021
@Test def runSimpleVal: Unit = {
21-
ws"${m1}val foo = 123".withSource
22-
.run(m1, "1:val foo: Int = 123")
22+
ws"${m1}val foo = 123${m2}".withSource
23+
.run(m1,
24+
((m1 to m2), "val foo: Int = 123"))
2325
}
2426

2527
@Test def usePreviousDefinition: Unit = {
26-
ws"""${m1}val foo = 123
27-
val bar = foo + 1""".withSource
28-
.run(m1, "1:val foo: Int = 123",
29-
"2:val bar: Int = 124")
28+
ws"""${m1}val foo = 123${m2}
29+
${m3}val bar = foo + 1${m4}""".withSource
30+
.run(m1,
31+
((m1 to m2), "val foo: Int = 123"),
32+
((m3 to m4), "val bar: Int = 124"))
3033
}
3134

3235
@Test def defineObject: Unit = {
33-
ws"""${m1}def foo(x: Int) = x + 1
34-
foo(1)""".withSource
35-
.run(m1, "1:def foo(x: Int): Int",
36-
"2:val res0: Int = 2")
36+
ws"""${m1}def foo(x: Int) = x + 1${m2}
37+
${m3}foo(1)${m4}""".withSource
38+
.run(m1,
39+
((m1 to m2), "def foo(x: Int): Int"),
40+
((m3 to m4), "val res0: Int = 2"))
3741
}
3842

3943
@Test def defineCaseClass: Unit = {
40-
ws"""${m1} case class Foo(x: Int)
41-
Foo(1)""".withSource
42-
.run(m1, "1:// defined case class Foo",
43-
"2:val res0: Foo = Foo(1)")
44+
ws"""${m1}case class Foo(x: Int)${m2}
45+
${m3}Foo(1)${m4}""".withSource
46+
.run(m1,
47+
((m1 to m2), "// defined case class Foo"),
48+
((m3 to m4), "val res0: Foo = Foo(1)"))
4449
}
4550

4651
@Test def defineClass: Unit = {
4752
ws"""${m1}class Foo(x: Int) {
4853
override def toString: String = "Foo"
49-
}
50-
new Foo(1)""".withSource
51-
.run(m1, "3:// defined class Foo",
52-
"4:val res0: Foo = Foo")
54+
}${m2}
55+
${m3}new Foo(1)${m4}""".withSource
56+
.run(m1,
57+
((m1 to m2), "// defined class Foo"),
58+
((m3 to m4), "val res0: Foo = Foo"))
5359
}
5460

5561
@Test def defineAnonymousClass0: Unit = {
5662
ws"""${m1}new {
5763
override def toString: String = "Foo"
58-
}""".withSource
59-
.run(m1, "3:val res0: Object = Foo")
64+
}${m2}""".withSource
65+
.run(m1,
66+
((m1 to m2), "val res0: Object = Foo"))
6067
}
6168

6269
@Test def defineAnonymousClass1: Unit = {
63-
ws"""${m1}class Foo
64-
trait Bar
65-
new Foo with Bar {
70+
ws"""${m1}class Foo${m2}
71+
${m3}trait Bar${m4}
72+
${m5}new Foo with Bar {
6673
override def toString: String = "Foo"
67-
}""".withSource
68-
.run(m1, "1:// defined class Foo",
69-
"2:// defined trait Bar",
70-
"5:val res0: Foo & Bar = Foo")
74+
}${m6}""".withSource
75+
.run(m1,
76+
((m1 to m2), "// defined class Foo"),
77+
((m3 to m4), "// defined trait Bar"),
78+
((m5 to m6), "val res0: Foo & Bar = Foo"))
7179
}
7280

7381
@Test def produceMultilineOutput: Unit = {
74-
ws"""${m1}1 to 3 foreach println""".withSource
75-
.run(m1, s"1:1${nl}2${nl}3")
82+
ws"""${m1}1 to 3 foreach println${m2}""".withSource
83+
.run(m1,
84+
((m1 to m2), s"1${nl}2${nl}3"))
7685
}
7786

7887
@Test def patternMatching0: Unit = {
7988
ws"""${m1}1 + 2 match {
8089
case x if x % 2 == 0 => "even"
8190
case _ => "odd"
82-
}""".withSource
83-
.run(m1, "4:val res0: String = odd")
91+
}${m2}""".withSource
92+
.run(m1,
93+
((m1 to m2), "val res0: String = odd"))
8494
}
8595

8696
@Test def patternMatching1: Unit = {
87-
ws"""${m1}val (foo, bar) = (1, 2)""".withSource
88-
.run(m1, s"1:val bar: Int = 2${nl}val foo: Int = 1")
97+
ws"""${m1}val (foo, bar) = (1, 2)${m2}""".withSource
98+
.run(m1,
99+
((m1 to m2), s"val bar: Int = 2${nl}val foo: Int = 1"))
89100
}
90101

91102
@Test def evaluationException: Unit = {
92-
ws"""${m1}val foo = 1 / 0
93-
val bar = 2""".withSource
94-
.runNonStrict(m1, "1:java.lang.ArithmeticException: / by zero",
95-
"2:val bar: Int = 2")
103+
ws"""${m1}val foo = 1 / 0${m2}
104+
${m3}val bar = 2${m4}""".withSource
105+
.runNonStrict(m1,
106+
((m1 to m2), "java.lang.ArithmeticException: / by zero"),
107+
((m3 to m4), "val bar: Int = 2"))
96108
}
97109

98110
@Test def worksheetCompletion(): Unit = {
@@ -204,15 +216,17 @@ class WorksheetTest {
204216
}
205217

206218
@Test def systemExit(): Unit = {
207-
ws"""${m1}println("Hello, world!")
219+
ws"""${m1}println("Hello, world!")${m2}
208220
System.exit(0)
209221
println("Goodbye!")""".withSource
210-
.run(m1, "1:Hello, world!")
222+
.run(m1,
223+
((m1 to m2), "Hello, world!"))
211224
}
212225

213226
@Test def outputOnStdErr(): Unit = {
214-
ws"""${m1}System.err.println("Oh no")""".withSource
215-
.run(m1, "1:Oh no")
227+
ws"""${m1}System.err.println("Oh no")${m2}""".withSource
228+
.run(m1,
229+
((m1 to m2), "Oh no"))
216230
}
217231

218232
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ class CodeTester(projects: List[Project]) {
182182
*
183183
* @see dotty.tools.languageserver.util.actions.WorksheetRun
184184
*/
185-
def run(marker: CodeMarker, expected: String*): this.type =
185+
def run(marker: CodeMarker, expected: (CodeRange, String)*): this.type =
186186
doAction(new WorksheetRun(marker, expected, strict = true))
187187

188188
/**
@@ -194,7 +194,7 @@ class CodeTester(projects: List[Project]) {
194194
*
195195
* @see dotty.tools.languageserver.util.actions.WorksheetRun
196196
*/
197-
def runNonStrict(marker: CodeMarker, expected: String*): this.type =
197+
def runNonStrict(marker: CodeMarker, expected: (CodeRange, String)*): this.type =
198198
doAction(new WorksheetRun(marker, expected, strict = false))
199199

200200
/**

language-server/test/dotty/tools/languageserver/util/actions/WorksheetRun.scala

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
package dotty.tools.languageserver.util.actions
22

3-
import dotty.tools.languageserver.util.PositionContext
3+
import dotty.tools.languageserver.util.{ CodeRange, PositionContext }
44
import dotty.tools.languageserver.util.embedded.CodeMarker
55

66
import java.util.concurrent.TimeUnit
77

88
import org.junit.Assert.{assertEquals, assertTrue, fail}
99

10-
class WorksheetRun(marker: CodeMarker, expected: Seq[String], strict: Boolean) extends WorksheetAction {
10+
class WorksheetRun(marker: CodeMarker, expected: Seq[(CodeRange, String)], strict: Boolean) extends WorksheetAction {
1111

1212
override def execute(): Exec[Unit] = {
1313
val result = triggerRun(marker).get(30, TimeUnit.SECONDS)
1414
assertTrue(result.success)
1515

16-
val logs = worksheetOutput(marker).map(out => s"${out.line}:${out.content}")
16+
val logs = worksheetOutput(marker).map(out => (out.range, out.content))
1717

18-
if (strict) {
19-
assertEquals(expected, logs)
20-
} else {
21-
expected.zip(logs).foreach {
22-
case (expected, message) => assertTrue(s"'$message' didn't start with '$expected'", message.startsWith(expected))
23-
}
18+
expected.zip(logs).foreach {
19+
case ((expectedCodeRange, expectedOutput), (actualRange, actualOutput)) =>
20+
assertEquals(expectedCodeRange.toRange, actualRange)
21+
if (strict)
22+
assertEquals(expectedOutput, actualOutput)
23+
else
24+
assertTrue(s"'$actualOutput' didn't start with '$expectedOutput'", actualOutput.startsWith(expectedOutput))
2425
}
2526
client.worksheetOutput.clear()
2627
}

vscode-dotty/src/protocol.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as vscode from 'vscode'
22
import { RequestType, NotificationType } from 'vscode-jsonrpc'
3-
import { VersionedTextDocumentIdentifier, TextDocumentIdentifier } from 'vscode-languageserver-protocol'
3+
import { Range, VersionedTextDocumentIdentifier, TextDocumentIdentifier } from 'vscode-languageserver-protocol'
44

55
import { client } from './extension'
66

@@ -17,7 +17,13 @@ export interface WorksheetRunResult {
1717
/** The parameters for the `worksheet/publishOutput` notification. */
1818
export interface WorksheetPublishOutputParams {
1919
textDocument: VersionedTextDocumentIdentifier
20-
line: number
20+
// TODO: remove this property and make `range` non-optional once we
21+
// stop supporting Dotty < 0.13.0-RC1
22+
/**
23+
* @deprecated Use range instead.
24+
*/
25+
line?: number
26+
range?: Range
2127
content: string
2228
}
2329

vscode-dotty/src/worksheet.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ class Worksheet implements Disposable {
9999

100100
/** Display the output in the worksheet's editor. */
101101
handleMessage(output: WorksheetPublishOutputParams, editor: vscode.TextEditor) {
102-
this.displayAndSaveResult(output.line - 1, output.content, editor)
102+
let range = output.range ?
103+
this.client.protocol2CodeConverter.asRange(output.range) :
104+
(output.line ?
105+
new Range(output.line - 1, 0, output.line - 1, 0) :
106+
new Range(0, 0, 0, 0))
107+
this.displayAndSaveResult(range, output.content, editor)
103108
}
104109

105110
/**
@@ -140,22 +145,22 @@ class Worksheet implements Disposable {
140145
* Parse and display the result of running part of this worksheet. The result is saved so that it
141146
* can be restored if this buffer is closed.
142147
*
143-
* @param lineNumber The number of the line in the source that produced the result.
148+
* @param range The range in the source that produced the result.
144149
* @param runResult The result itself.
145150
* @param editor The editor where to display the result.
146151
*/
147-
private displayAndSaveResult(lineNumber: number, runResult: string, editor: vscode.TextEditor): void {
152+
private displayAndSaveResult(range: Range, runResult: string, editor: vscode.TextEditor): void {
148153
const resultLines = runResult.split(/\r\n|\r|\n/g)
149154

150155
if (resultLines.length == 0)
151156
return
152157

153-
const line = editor.document.lineAt(lineNumber)
158+
const lastLine = editor.document.lineAt(range.end.line)
154159
const decorationOptions = {
155-
range: line.range,
160+
range: range,
156161
hoverMessage: new vscode.MarkdownString().appendCodeblock(runResult)
157162
}
158-
const decorationMargin = this.margin - line.text.length
163+
const decorationMargin = this.margin - lastLine.text.length
159164
const decorationText = resultLines[0] + (resultLines.length > 1 ? `<${resultLines.length - 1} lines hidden, hover to see full output>` : "")
160165
const decorationType = this.createDecoration(decorationMargin, decorationText)
161166
const decoration = new Decoration(decorationType, decorationOptions)

0 commit comments

Comments
 (0)