Skip to content

Commit 0e0163c

Browse files
committed
First prototype of all-in-one multi-span rendering
1 parent 8859048 commit 0e0163c

File tree

1 file changed

+165
-42
lines changed

1 file changed

+165
-42
lines changed

compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala

Lines changed: 165 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -246,65 +246,113 @@ trait MessageRendering {
246246
else
247247
pos
248248

249-
/** The whole message rendered from `dia.msg`.
250-
*
251-
* For a position in an inline expansion, choose `pos1`
252-
* which is the most specific position in the call written
253-
* by the user. For a diagnostic at EOF, where the last char
254-
* of source text is a newline, adjust the position to render
255-
* before the newline, at the end of the last line of text.
256-
*
257-
* The rendering begins with a label and position (`posString`).
258-
* Then `sourceLines` with embedded caret `positionMarker`
259-
* and rendered message.
260-
*
261-
* Then an `Inline stack trace` showing context for inlined code.
262-
* Inlined positions are taken which don't contain `pos1`.
263-
* (That should probably be positions not contained by outermost.)
264-
* Note that position equality includes `outer` position;
265-
* usually we intend to test `contains` or `coincidesWith`.
266-
*
267-
*/
268-
def messageAndPos(dia: Diagnostic)(using Context): String =
249+
/** Render diagnostics with positions in different files separately */
250+
private def renderSeparateSpans(dia: Diagnostic)(using Context): String =
269251
val msg = dia.msg
270252
val pos = dia.pos
271-
val pos1 = adjust(pos.nonInlined) // innermost pos contained by call.pos
272-
val outermost = pos.outermost // call.pos
273-
val inlineStack = pos.inlinePosStack.filterNot(outermost.contains(_))
253+
val pos1 = adjust(pos.nonInlined)
274254
given Level = Level(dia.level)
275255
given Offset =
276-
val maxLineNumber =
277-
if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1
278-
else 0
256+
val maxLineNumber = if pos.exists then pos1.endLine + 1 else 0
279257
Offset(maxLineNumber.toString.length + 2)
258+
280259
val sb = StringBuilder()
281260
val posString = posStr(pos1, msg, diagnosticLevel(dia))
282261
if posString.nonEmpty then sb.append(posString).append(EOL)
262+
283263
if pos.exists && pos1.exists && pos1.source.file.exists then
284264
val (srcBefore, srcAfter, offset) = sourceLines(pos1)
285265
val marker = positionMarker(pos1)
286266
val err = errorMsg(pos1, msg.message)
287267
sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL))
288-
289-
if inlineStack.nonEmpty then
290-
sb.append(EOL).append(newBox())
291-
sb.append(EOL).append(offsetBox).append(i"Inline stack trace")
292-
for inlinedPos <- inlineStack do
293-
sb.append(EOL).append(newBox(soft = true))
294-
sb.append(EOL).append(offsetBox).append(i"This location contains code that was inlined from $pos")
295-
if inlinedPos.source.file.exists then
296-
val (srcBefore, srcAfter, _) = sourceLines(inlinedPos)
297-
val marker = positionMarker(inlinedPos)
298-
sb.append(EOL).append((srcBefore ::: marker :: srcAfter).mkString(EOL))
299-
sb.append(EOL).append(endBox)
300-
end if
301-
else sb.append(msg.message)
268+
else
269+
sb.append(msg.message)
302270

303271
dia.getSubdiags.foreach(addSubdiagnostic(sb, _))
272+
sb.toString
273+
274+
def messageAndPosMultiSpan(dia: Diagnostic)(using Context): String =
275+
val msg = dia.msg
276+
val pos = dia.pos
277+
val pos1 = adjust(pos.nonInlined)
278+
val subdiags = dia.getSubdiags
279+
280+
// Collect all positions with their associated messages
281+
case class PosAndMsg(pos: SourcePosition, msg: Message, isPrimary: Boolean)
282+
val allPosAndMsg = PosAndMsg(pos1, msg, true) :: subdiags.map(s => PosAndMsg(adjust(s.pos), s.msg, false))
283+
val validPosAndMsg = allPosAndMsg.filter(_.pos.exists)
284+
285+
if validPosAndMsg.isEmpty then
286+
return msg.message
287+
288+
// Check all positions are in the same source file
289+
val source = validPosAndMsg.head.pos.source
290+
if !validPosAndMsg.forall(_.pos.source == source) || !source.file.exists then
291+
// Cannot render multi-span if positions are in different files
292+
// Fall back to showing them separately
293+
return renderSeparateSpans(dia)
294+
295+
// Find the line range covering all positions
296+
val minLine = validPosAndMsg.map(_.pos.startLine).min
297+
val maxLine = validPosAndMsg.map(_.pos.endLine).max
298+
val maxLineNumber = maxLine + 1
299+
300+
given Level = Level(dia.level)
301+
given Offset = Offset(maxLineNumber.toString.length + 2)
302+
303+
val sb = StringBuilder()
304+
305+
// Title using the primary position
306+
val posString = posStr(pos1, msg, diagnosticLevel(dia))
307+
if posString.nonEmpty then sb.append(posString).append(EOL)
308+
309+
// Render the unified code snippet
310+
// Get syntax-highlighted content for the entire range
311+
val startOffset = source.lineToOffset(minLine)
312+
val endOffset = source.nextLine(source.lineToOffset(maxLine))
313+
val content = source.content.slice(startOffset, endOffset)
314+
val syntax =
315+
if (summon[Context].settings.color.value != "never" && !summon[Context].isJava)
316+
SyntaxHighlighting.highlight(new String(content)).toCharArray
317+
else content
318+
319+
// Split syntax-highlighted content into lines
320+
def linesFrom(arr: Array[Char]): List[String] = {
321+
def pred(c: Char) = (c: @switch) match {
322+
case LF | CR | FF | SU => true
323+
case _ => false
324+
}
325+
val (line, rest0) = arr.span(!pred(_))
326+
val (_, rest) = rest0.span(pred)
327+
new String(line) :: { if (rest.isEmpty) Nil else linesFrom(rest) }
328+
}
304329

305-
if dia.isVerbose then
306-
appendFilterHelp(dia, sb)
330+
val lines = linesFrom(syntax)
331+
val lineNumberWidth = maxLineNumber.toString.length
332+
333+
// Render each line with its markers and messages
334+
for (lineNum <- minLine to maxLine) {
335+
val lineIdx = lineNum - minLine
336+
if lineIdx < lines.length then
337+
val lineContent = lines(lineIdx)
338+
val lineNbr = (lineNum + 1).toString
339+
val linePrefix = String.format(s"%${lineNumberWidth}s |", lineNbr)
340+
val lnum = hl(" " * math.max(0, offset - linePrefix.length - 1) + linePrefix)
341+
sb.append(lnum).append(lineContent.stripLineEnd).append(EOL)
342+
343+
// Find all positions that should show markers after this line
344+
// A position shows its marker after its start line
345+
val positionsOnLine = validPosAndMsg.filter(_.pos.startLine == lineNum)
346+
.sortBy(pm => (pm.pos.startColumn, !pm.isPrimary)) // Primary positions first if same column
347+
348+
for posAndMsg <- positionsOnLine do
349+
val marker = positionMarker(posAndMsg.pos)
350+
val err = errorMsg(posAndMsg.pos, posAndMsg.msg.message)
351+
sb.append(marker).append(EOL)
352+
sb.append(err).append(EOL)
353+
}
307354

355+
// Add explanation if needed
308356
if Diagnostic.shouldExplain(dia) then
309357
sb.append(EOL).append(newBox())
310358
sb.append(EOL).append(offsetBox).append(" Explanation (enabled by `-explain`)")
@@ -317,6 +365,81 @@ trait MessageRendering {
317365
sb.append(EOL).append(offsetBox).append(" longer explanation available when compiling with `-explain`")
318366

319367
sb.toString
368+
end messageAndPosMultiSpan
369+
370+
/** The whole message rendered from `dia.msg`.
371+
*
372+
* For a position in an inline expansion, choose `pos1`
373+
* which is the most specific position in the call written
374+
* by the user. For a diagnostic at EOF, where the last char
375+
* of source text is a newline, adjust the position to render
376+
* before the newline, at the end of the last line of text.
377+
*
378+
* The rendering begins with a label and position (`posString`).
379+
* Then `sourceLines` with embedded caret `positionMarker`
380+
* and rendered message.
381+
*
382+
* Then an `Inline stack trace` showing context for inlined code.
383+
* Inlined positions are taken which don't contain `pos1`.
384+
* (That should probably be positions not contained by outermost.)
385+
* Note that position equality includes `outer` position;
386+
* usually we intend to test `contains` or `coincidesWith`.
387+
*
388+
*/
389+
def messageAndPos(dia: Diagnostic)(using Context): String =
390+
if dia.getSubdiags.nonEmpty then messageAndPosMultiSpan(dia)
391+
else
392+
val msg = dia.msg
393+
val pos = dia.pos
394+
val pos1 = adjust(pos.nonInlined) // innermost pos contained by call.pos
395+
val outermost = pos.outermost // call.pos
396+
val inlineStack = pos.inlinePosStack.filterNot(outermost.contains(_))
397+
given Level = Level(dia.level)
398+
given Offset =
399+
val maxLineNumber =
400+
if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1
401+
else 0
402+
Offset(maxLineNumber.toString.length + 2)
403+
val sb = StringBuilder()
404+
val posString = posStr(pos1, msg, diagnosticLevel(dia))
405+
if posString.nonEmpty then sb.append(posString).append(EOL)
406+
if pos.exists && pos1.exists && pos1.source.file.exists then
407+
val (srcBefore, srcAfter, offset) = sourceLines(pos1)
408+
val marker = positionMarker(pos1)
409+
val err = errorMsg(pos1, msg.message)
410+
sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL))
411+
412+
if inlineStack.nonEmpty then
413+
sb.append(EOL).append(newBox())
414+
sb.append(EOL).append(offsetBox).append(i"Inline stack trace")
415+
for inlinedPos <- inlineStack do
416+
sb.append(EOL).append(newBox(soft = true))
417+
sb.append(EOL).append(offsetBox).append(i"This location contains code that was inlined from $pos")
418+
if inlinedPos.source.file.exists then
419+
val (srcBefore, srcAfter, _) = sourceLines(inlinedPos)
420+
val marker = positionMarker(inlinedPos)
421+
sb.append(EOL).append((srcBefore ::: marker :: srcAfter).mkString(EOL))
422+
sb.append(EOL).append(endBox)
423+
end if
424+
else sb.append(msg.message)
425+
426+
dia.getSubdiags.foreach(addSubdiagnostic(sb, _))
427+
428+
if dia.isVerbose then
429+
appendFilterHelp(dia, sb)
430+
431+
if Diagnostic.shouldExplain(dia) then
432+
sb.append(EOL).append(newBox())
433+
sb.append(EOL).append(offsetBox).append(" Explanation (enabled by `-explain`)")
434+
sb.append(EOL).append(newBox(soft = true))
435+
dia.msg.explanation.split(raw"\R").foreach: line =>
436+
sb.append(EOL).append(offsetBox).append(if line.isEmpty then "" else " ").append(line)
437+
sb.append(EOL).append(endBox)
438+
else if dia.msg.canExplain then
439+
sb.append(EOL).append(offsetBox)
440+
sb.append(EOL).append(offsetBox).append(" longer explanation available when compiling with `-explain`")
441+
442+
sb.toString
320443
end messageAndPos
321444

322445
private def addSubdiagnostic(sb: StringBuilder, subdiag: Subdiagnostic)(using Context, Level, Offset): Unit =

0 commit comments

Comments
 (0)