Skip to content

Commit 0770ad6

Browse files
rochalatgodzik
authored andcommitted
Add unsafe RawPresentationCompiler implementation (scala#24133)
`RawPresentationCompiler` is a direct, unsafe way to access presentation compiler interface provided in scalameta/metals#7841 It is up to the consumer to guarantee thread safety and sequential request processing. This solves many issues with the current `PresentationCompiler` interface but most important are: - We now have control where and how each presentation compiler method is run. Before it was always run on a `SingleThreadExecutor` created by the `PresentationCompiler`. The original behavior will stay with the standard `ScalaPresentationCompiler` interface. (There is another PR that refactors that part), - If there was bug in scheduler all versions were affected, On top of that this PR implements all things that will work out of the box: - Diagnostic provider - trivial implementation, previously not possible to LIFO task used by `Scala3CompilerAccess`. Will be super useful e.g for Scastie which will become a consumer of this new raw API. - `InteractiveDriver` compilation cancellation by checking Thread.interrupted(). Future changes are required to make it work with safe `ScalaPresentationCompiler` and `CancelTokens`. Here we can omit the cancel token support for now, because we would check for `Thread.interrupted()` along the `cancelToken.isCancelled` anyway, and we can now easily interrupt it from e.g `IO.interruptible` Those changes were adapted from my other PR's that required adjustment of `Scala3CompilerAccess` but I don't think it is ready / I'm not happy with current state of that refactor. In the future we should think of replacing the implementation of `ScalaPresentationCompiler` to use `RawPresentationCompiler` as underlying impl and just call it in safe way. The implementation was tested on the actual LSP server and it works fine.
1 parent 1b2769f commit 0770ad6

File tree

10 files changed

+535
-32
lines changed

10 files changed

+535
-32
lines changed

compiler/src/dotty/tools/dotc/core/Contexts.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ object Contexts {
179179
val local = incCallback
180180
local != null && local.enabled || forceRun
181181

182-
/** The Zinc compile progress callback implementation if we are run from Zinc, null otherwise */
182+
/** The Zinc compile progress callback implementation if we are run from Zinc or used by presentation compiler, null otherwise */
183183
def progressCallback: ProgressCallback | Null = store(progressCallbackLoc)
184184

185185
/** Run `op` if there exists a Zinc progress callback */

compiler/src/dotty/tools/dotc/interactive/InteractiveDriver.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import java.util.zip.*
1414
import scala.collection.*
1515
import scala.io.Codec
1616

17+
import dotty.tools.dotc.sbt.interfaces.ProgressCallback
1718
import dotty.tools.io.AbstractFile
1819

1920
import ast.{Trees, tpd}
@@ -30,6 +31,9 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
3031

3132
override def sourcesRequired: Boolean = false
3233

34+
private var myProgressCallback: ProgressCallback = new ProgressCallback:
35+
override def isCancelled(): Boolean = Thread.interrupted()
36+
3337
private val myInitCtx: Context = {
3438
val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive)
3539
rootCtx.setSetting(rootCtx.settings.YretainTrees, true)
@@ -151,7 +155,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
151155
val reporter =
152156
new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages
153157

154-
val run = compiler.newRun(using myInitCtx.fresh.setReporter(reporter))
158+
val run = compiler.newRun(using myInitCtx.fresh.setReporter(reporter).setProgressCallback(myProgressCallback))
155159
myCtx = run.runContext.withRootImports
156160

157161
given Context = myCtx
@@ -169,8 +173,7 @@ class InteractiveDriver(val settings: List[String]) extends Driver {
169173
myCtx = myCtx.fresh.setPhase(myInitCtx.base.typerPhase)
170174

171175
reporter.removeBufferedMessages
172-
}
173-
catch {
176+
} catch {
174177
case ex: FatalError =>
175178
myCtx = previousCtx
176179
close(uri)

presentation-compiler/src/main/dotty/tools/pc/CachingDriver.scala

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import scala.compiletime.uninitialized
2929
*/
3030
class CachingDriver(override val settings: List[String]) extends InteractiveDriver(settings):
3131

32-
@volatile private var lastCompiledURI: URI = uninitialized
32+
private var lastCompiledURI: URI = uninitialized
33+
private var previousDiags = List.empty[Diagnostic]
3334

3435
private def alreadyCompiled(uri: URI, content: Array[Char]): Boolean =
3536
compilationUnits.get(uri) match
@@ -40,17 +41,8 @@ class CachingDriver(override val settings: List[String]) extends InteractiveDriv
4041
case _ => false
4142

4243
override def run(uri: URI, source: SourceFile): List[Diagnostic] =
43-
val diags =
44-
if alreadyCompiled(uri, source.content) then Nil
45-
else super.run(uri, source)
44+
if !alreadyCompiled(uri, source.content) then previousDiags = super.run(uri, source)
4645
lastCompiledURI = uri
47-
diags
48-
49-
override def run(uri: URI, sourceCode: String): List[Diagnostic] =
50-
val diags =
51-
if alreadyCompiled(uri, sourceCode.toCharArray().nn) then Nil
52-
else super.run(uri, sourceCode)
53-
lastCompiledURI = uri
54-
diags
46+
previousDiags
5547

5648
end CachingDriver
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package dotty.tools.pc
2+
3+
import dotty.tools.dotc.core.Contexts.Context
4+
import dotty.tools.dotc.interactive.InteractiveDriver
5+
import dotty.tools.dotc.interfaces.Diagnostic as DiagnosticInterfaces
6+
import dotty.tools.dotc.reporting.ErrorMessageID
7+
import dotty.tools.dotc.reporting.Diagnostic
8+
import dotty.tools.dotc.reporting.CodeAction
9+
import dotty.tools.dotc.rewrites.Rewrites.ActionPatch
10+
import dotty.tools.pc.utils.InteractiveEnrichments.toLsp
11+
12+
import org.eclipse.lsp4j
13+
14+
import scala.jdk.CollectionConverters.*
15+
import scala.meta.pc.VirtualFileParams
16+
17+
class DiagnosticProvider(driver: InteractiveDriver, params: VirtualFileParams):
18+
19+
def diagnostics(): List[lsp4j.Diagnostic] =
20+
if params.shouldReturnDiagnostics then
21+
val diags = driver.run(params.uri().nn, params.text().nn)
22+
given Context = driver.currentCtx
23+
diags.flatMap(toLsp)
24+
else Nil
25+
26+
private def toLsp(diag: Diagnostic)(using Context): Option[lsp4j.Diagnostic] =
27+
Option.when(diag.pos.exists):
28+
val lspDiag = lsp4j.Diagnostic(
29+
diag.pos.toLsp,
30+
diag.msg.message,
31+
toDiagnosticSeverity(diag.level),
32+
"presentation compiler",
33+
)
34+
lspDiag.setCode(diag.msg.errorId.errorNumber)
35+
36+
val actions = diag.msg.actions.map(toLspScalaAction).asJava
37+
38+
lspDiag.setData(actions)
39+
if diag.msg.errorId == ErrorMessageID.UnusedSymbolID then
40+
lspDiag.setTags(List(lsp4j.DiagnosticTag.Unnecessary).asJava)
41+
42+
lspDiag
43+
44+
private def toLspScalaAction(action: CodeAction): lsp4j.CodeAction =
45+
val lspAction = lsp4j.CodeAction(action.title)
46+
lspAction.setKind(lsp4j.CodeActionKind.QuickFix)
47+
lspAction.setIsPreferred(true)
48+
val edits = action.patches.groupBy(_.srcPos.source.path)
49+
.map((path, actions) => path -> (actions.map(toLspTextEdit).asJava))
50+
.asJava
51+
52+
val workspaceEdit = lsp4j.WorkspaceEdit(edits)
53+
lspAction.setEdit(workspaceEdit)
54+
lspAction
55+
56+
private def toLspTextEdit(actionPatch: ActionPatch): lsp4j.TextEdit =
57+
val startPos = lsp4j.Position(actionPatch.srcPos.startLine, actionPatch.srcPos.startColumn)
58+
val endPos = lsp4j.Position(actionPatch.srcPos.endLine, actionPatch.srcPos.endColumn)
59+
val range = lsp4j.Range(startPos, endPos)
60+
lsp4j.TextEdit(range, actionPatch.replacement)
61+
62+
private def toDiagnosticSeverity(severity: Int): lsp4j.DiagnosticSeverity =
63+
severity match
64+
case DiagnosticInterfaces.ERROR => lsp4j.DiagnosticSeverity.Error
65+
case DiagnosticInterfaces.WARNING => lsp4j.DiagnosticSeverity.Warning
66+
case DiagnosticInterfaces.INFO => lsp4j.DiagnosticSeverity.Information
67+
case _ => lsp4j.DiagnosticSeverity.Information

presentation-compiler/src/main/dotty/tools/pc/PcInlayHintsProvider.scala

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import dotty.tools.dotc.util.Spans.Span
3232
import org.eclipse.lsp4j.InlayHint
3333
import org.eclipse.lsp4j.InlayHintKind
3434
import org.eclipse.{lsp4j as l}
35+
import scala.meta.internal.pc.InlayHintOrigin
3536

3637
class PcInlayHintsProvider(
3738
driver: InteractiveDriver,
@@ -80,7 +81,7 @@ class PcInlayHintsProvider(
8081
)
8182
case _ => inlayHints
8283
}
83-
84+
8485
tree match
8586
case ImplicitConversion(symbol, range) =>
8687
val adjusted = adjustPos(range)
@@ -89,11 +90,13 @@ class PcInlayHintsProvider(
8990
adjusted.startPos.toLsp,
9091
labelPart(symbol, symbol.decodedName) :: LabelPart("(") :: Nil,
9192
InlayHintKind.Parameter,
93+
InlayHintOrigin.ImplicitConversion
9294
)
9395
.add(
9496
adjusted.endPos.toLsp,
9597
LabelPart(")") :: Nil,
9698
InlayHintKind.Parameter,
99+
InlayHintOrigin.ImplicitConversion
97100
)
98101
case ImplicitParameters(trees, pos) =>
99102
firstPassHints.add(
@@ -104,12 +107,14 @@ class PcInlayHintsProvider(
104107
case None => LabelPart(label)
105108
),
106109
InlayHintKind.Parameter,
110+
InlayHintOrigin.ImplicitParameters
107111
)
108112
case ValueOf(label, pos) =>
109113
firstPassHints.add(
110114
adjustPos(pos).toLsp,
111115
LabelPart("(") :: LabelPart(label) :: List(LabelPart(")")),
112116
InlayHintKind.Parameter,
117+
InlayHintOrigin.ImplicitParameters
113118
)
114119
case TypeParameters(tpes, pos, sel)
115120
if !syntheticTupleApply(sel) =>
@@ -118,20 +123,19 @@ class PcInlayHintsProvider(
118123
adjustPos(pos).endPos.toLsp,
119124
label,
120125
InlayHintKind.Type,
126+
InlayHintOrigin.TypeParameters
121127
)
122128
case InferredType(tpe, pos, defTree)
123129
if !isErrorTpe(tpe) =>
124130
val adjustedPos = adjustPos(pos).endPos
125-
if firstPassHints.containsDef(adjustedPos.start) then firstPassHints
126-
else
127-
firstPassHints
128-
.add(
129-
adjustedPos.toLsp,
130-
LabelPart(": ") :: toLabelParts(tpe, pos),
131-
InlayHintKind.Type,
132-
)
133-
.addDefinition(adjustedPos.start)
134-
case Parameters(isInfixFun, args) =>
131+
firstPassHints
132+
.add(
133+
adjustedPos.toLsp,
134+
LabelPart(": ") :: toLabelParts(tpe, pos),
135+
InlayHintKind.Type,
136+
InlayHintOrigin.InferredType
137+
)
138+
case Parameters(isInfixFun, args) =>
135139
def isNamedParam(pos: SourcePosition): Boolean =
136140
val start = text.indexWhere(!_.isWhitespace, pos.start)
137141
val end = text.lastIndexWhere(!_.isWhitespace, pos.end - 1)
@@ -167,6 +171,7 @@ class PcInlayHintsProvider(
167171
hintPos.startPos.toLsp,
168172
List(LabelPart(labelStr)),
169173
InlayHintKind.Parameter,
174+
if params.byNameParameters then InlayHintOrigin.ByNameParameters else InlayHintOrigin.NamedParameters
170175
)
171176
else ih
172177
}
@@ -515,7 +520,7 @@ object XRayModeHint:
515520
case Some(sel: Select) if sel.isForComprehensionMethod => false
516521
case Some(par) if par.sourcePos.exists && par.sourcePos.line == tree.sourcePos.line => true
517522
case _ => false
518-
523+
519524
tree match
520525
/*
521526
anotherTree
@@ -530,7 +535,7 @@ object XRayModeHint:
530535
.select
531536
*/
532537
case select @ Select(innerTree, _)
533-
if innerTree.sourcePos.exists && !isParentOnSameLine && !isParentApply &&
538+
if innerTree.sourcePos.exists && !isParentOnSameLine && !isParentApply &&
534539
isEndOfLine(tree.sourcePos) =>
535540
Some((select.tpe.widen.deepDealiasAndSimplify, tree.sourcePos))
536541
case _ => None

0 commit comments

Comments
 (0)