-
Notifications
You must be signed in to change notification settings - Fork 326
/
ClientConfigurationAdapter.scala
139 lines (127 loc) Β· 4.88 KB
/
ClientConfigurationAdapter.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
package scala.meta.internal.metals.debug
import java.nio.file.Paths
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.SourceMapper
import scala.meta.io.AbsolutePath
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.debug.InitializeRequestArguments
import org.eclipse.lsp4j.debug.InitializeRequestArgumentsPathFormat
import org.eclipse.lsp4j.debug.SourceBreakpoint
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
/**
* The [[ClientConfigurationAdapter]] uses the client configuration coming from the initialize request
* to normalize the source path and line numbers.
*
* @param pathFormat either "path" or "uri"
* @param linesStartAt1 true if client line numbers start at 1
*/
private[debug] final case class ClientConfigurationAdapter(
clientId: Option[String],
pathFormat: String,
val linesStartAt1: Boolean,
sourceMapper: SourceMapper,
) {
// The scala-debug-adapter uses the JVM class file format
// in which lines start at 1
def normalizeLineForServer(path: AbsolutePath, line: Int): Int = {
val normalizedLine = if (linesStartAt1) line else line + 1
sourceMapper.mappedLineForServer(path, normalizedLine)
}
def adaptLineForClient(path: AbsolutePath, line: Int): Int = {
val adaptedLine = if (linesStartAt1) line else line - 1
sourceMapper.mappedLineForClient(path, adaptedLine)
}
/**
* In the DAP specification, the presentationHint of a StackFrame can be
* 'normal', 'label' or 'subtle'. Most DAP implementations use 'subtle' to
* indicate that a frame is skipped by the debugger. The problem is that
* VSCode does not collapse 'subtle' frames, as other DAP clients do.
* Instead it collapses 'deemphasize' frames, even if it is not part of the
* spec.
*
* See https://github.com/microsoft/vscode/issues/206801
*/
def adaptStackTraceResponse(result: JsonObject): JsonObject = {
if (clientId.contains("vscode")) {
try {
// For VSCode only, we hack the json result of the stack trace response
// to replace all occurrences of 'subtle' by 'deemphasize'.
val frames = result.get("stackFrames").getAsJsonArray()
for (i <- 0.until(frames.size)) {
val frame = frames.get(i).getAsJsonObject()
val presentationHint = Option(frame.get("presentationHint"))
.map(_.getAsJsonPrimitive.getAsString)
if (presentationHint.contains("subtle")) {
frame.add("presentationHint", new JsonPrimitive("deemphasize"))
}
}
} catch {
case NonFatal(t) => scribe.warn("unexpected error when adapting stack trace response", t)
}
}
result
}
def toLspPosition(breakpoint: SourceBreakpoint): Position = {
val line = breakpoint.getLine
// LSP Position is 0-based
val lspLine =
if (linesStartAt1) line - 1
else line
new Position(lspLine, breakpoint.getColumn())
}
def toMetalsPath(path: String, mappedFrom: Boolean = false): AbsolutePath = {
pathFormat match {
// VS Code normally sends in path, which doesn't encode files from jars properly
// so URIs are actually sent in this case instead
case InitializeRequestArgumentsPathFormat.PATH
if !path.startsWith("file:") && !path.startsWith("jar:") =>
val uriPath = Paths.get(path).toUri.toString.toAbsolutePath
val mappedPath =
if (mappedFrom) sourceMapper.mappedFrom(uriPath)
else sourceMapper.mappedTo(uriPath)
mappedPath.getOrElse(uriPath)
case _ =>
val absolutePath = path.toAbsolutePath
val mappedPath =
if (mappedFrom) sourceMapper.mappedFrom(absolutePath)
else sourceMapper.mappedTo(absolutePath)
mappedPath.getOrElse(absolutePath)
}
}
def adaptPathForClient(path: AbsolutePath): String = {
pathFormat match {
case InitializeRequestArgumentsPathFormat.PATH =>
if (path.isJarFileSystem) path.toURI.toString else path.toString
case InitializeRequestArgumentsPathFormat.URI => path.toURI.toString
}
}
}
private[debug] object ClientConfigurationAdapter {
private val defautlPathFormat = InitializeRequestArgumentsPathFormat.URI
private val defaultLinesStartAt1 = false
def default(sourceMapper: SourceMapper): ClientConfigurationAdapter = {
ClientConfigurationAdapter(
None,
defautlPathFormat,
defaultLinesStartAt1,
sourceMapper,
)
}
def initialize(
initRequest: InitializeRequestArguments,
sourceMapper: SourceMapper,
): ClientConfigurationAdapter = {
val pathFormat =
Option(initRequest.getPathFormat).getOrElse(defautlPathFormat)
val linesStartAt1 = Option(initRequest.getLinesStartAt1)
.map(_.booleanValue)
.getOrElse(defaultLinesStartAt1)
ClientConfigurationAdapter(
Option(initRequest.getClientID),
pathFormat,
linesStartAt1,
sourceMapper,
)
}
}