@@ -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