@@ -12,7 +12,7 @@ import io.AbstractFile
1212import printing .Highlighting .{Blue , Red , Yellow }
1313import printing .SyntaxHighlighting
1414import Diagnostic .*
15- import util .{SourcePosition , NoSourcePosition }
15+ import util .{SourceFile , SourcePosition , NoSourcePosition }
1616import util .Chars .{ LF , CR , FF , SU }
1717import scala .annotation .switch
1818
@@ -253,47 +253,21 @@ trait MessageRendering {
253253 else
254254 pos
255255
256- /** Render a message using multi-span information from Message.parts. */
257- def messageAndPosFromParts (dia : Diagnostic )(using Context ): String =
258- val msg = dia.msg
259- val pos = dia.pos
260- val pos1 = adjust(pos.nonInlined)
261- val msgParts = msg.parts
262-
263- if msgParts.isEmpty then
264- return msg.leading.getOrElse(" " ) + (if msg.leading.isDefined then " \n " else " " ) + msg.message
265-
266- // Collect all positions from message parts
267- val validParts = msgParts.filter(_.srcPos.exists)
268-
269- if validParts.isEmpty then
270- return msg.leading.getOrElse(" " ) + (if msg.leading.isDefined then " \n " else " " ) + msg.message
256+ /** Render parts for a single source file.
257+ * @param parts the message parts to render (all must be from the same source)
258+ * @param sb the StringBuilder to append to
259+ */
260+ private def renderPartsForFile (
261+ parts : List [Message .MessagePart ],
262+ sb : StringBuilder
263+ )(using Context , Level , Offset ): Unit =
264+ if parts.isEmpty then return
271265
272- // Check all positions are in the same source file
273- val source = validParts.head.srcPos.source
274- if ! validParts.forall(_.srcPos.source == source) || ! source.file.exists then
275- // TODO: support rendering source positions across multiple files
276- return msg.leading.getOrElse(" " ) + (if msg.leading.isDefined then " \n " else " " ) + msg.message
266+ val source = parts.head.srcPos.source
277267
278268 // Find the line range covering all positions
279- val minLine = validParts.map(_.srcPos.startLine).min
280- val maxLine = validParts.map(_.srcPos.endLine).max
281- val maxLineNumber = maxLine + 1
282-
283- given Level = Level (dia.level)
284- given Offset = Offset (maxLineNumber.toString.length + 2 )
285-
286- val sb = StringBuilder ()
287-
288- // Title using the primary position
289- val posString = posStr(pos1, msg, diagnosticLevel(dia))
290- if posString.nonEmpty then sb.append(posString).append(EOL )
291-
292- // Display leading text if present
293- msg.leading.foreach { leadingText =>
294- sb.append(leadingText)
295- if ! leadingText.endsWith(EOL ) then sb.append(EOL )
296- }
269+ val minLine = parts.map(_.srcPos.startLine).min
270+ val maxLine = parts.map(_.srcPos.endLine).max
297271
298272 // Render the unified code snippet
299273 val startOffset = source.lineToOffset(minLine)
@@ -316,6 +290,7 @@ trait MessageRendering {
316290 }
317291
318292 val lines = linesFrom(syntax)
293+ val maxLineNumber = maxLine + 1
319294 val lineNumberWidth = maxLineNumber.toString.length
320295
321296 // Render each line with its markers and messages
@@ -329,7 +304,7 @@ trait MessageRendering {
329304 sb.append(lnum).append(lineContent.stripLineEnd).append(EOL )
330305
331306 // Find all positions that should show markers after this line
332- val partsOnLine = validParts .filter(_.srcPos.startLine == lineNum)
307+ val partsOnLine = parts .filter(_.srcPos.startLine == lineNum)
333308 .sortBy(p => (p.srcPos.startColumn, ! p.isPrimary))
334309
335310 if partsOnLine.size == 1 then
@@ -382,17 +357,76 @@ trait MessageRendering {
382357 connectorLine.append(msgPadding).append(msgText)
383358
384359 sb.append(connectorLine).append(EOL )
360+ end renderPartsForFile
361+
362+ /** Group consecutive parts by their source file. */
363+ private def groupPartsByFile (parts : List [Message .MessagePart ]): List [(SourceFile , List [Message .MessagePart ])] =
364+ if parts.isEmpty then Nil
365+ else
366+ val head = parts.head
367+ val source = head.srcPos.source
368+ val (sameSrc, rest) = parts.span(_.srcPos.source == source)
369+ (source, sameSrc) :: groupPartsByFile(rest)
370+
371+ /** Render a message using multi-span information from Message.parts. */
372+ def messageAndPosFromParts (dia : Diagnostic )(using Context ): String =
373+ val msg = dia.msg
374+ val pos = dia.pos
375+ val pos1 = adjust(pos.nonInlined)
376+ val msgParts = msg.parts
377+
378+ if msgParts.isEmpty then
379+ return msg.leading.getOrElse(" " ) + (if msg.leading.isDefined then " \n " else " " ) + msg.message
380+
381+ // Collect all positions from message parts
382+ val validParts = msgParts.filter(p => p.srcPos.exists && p.srcPos.source.file.exists)
383+
384+ if validParts.isEmpty then
385+ return msg.leading.map(_ + " \n " ).getOrElse(" " ) + msg.message
386+
387+ // Group parts by consecutive source files
388+ val groupedParts = groupPartsByFile(validParts)
389+
390+ // Calculate the maximum line number across all files for consistent offset
391+ val maxLineNumber = validParts.map(_.srcPos.endLine).max + 1
392+
393+ given Level = Level (dia.level)
394+ given Offset = Offset (maxLineNumber.toString.length + 2 )
395+
396+ val sb = StringBuilder ()
397+
398+ // Title using the primary position
399+ val posString = posStr(pos1, msg, diagnosticLevel(dia))
400+ if posString.nonEmpty then sb.append(posString).append(EOL )
401+
402+ // Display leading text if present
403+ msg.leading.foreach { leadingText =>
404+ sb.append(leadingText)
405+ if ! leadingText.endsWith(EOL ) then sb.append(EOL )
406+ }
407+
408+ // Track the current file
409+ // When starting, we set it to the file of the diagnostic
410+ var currentFile : SourceFile = pos1.source
411+
412+ // Render each group of parts
413+ for (source, parts) <- groupedParts do
414+ // Add a file indicator line when switching to a different file
415+ if source != currentFile then
416+ sb.append(" ... " ).append(renderPath(source.file)).append(EOL )
417+ currentFile = source
418+ renderPartsForFile(parts, sb)
385419
386420 // Add explanation if needed
387421 if Diagnostic .shouldExplain(dia) then
388- sb.append(EOL ).append( newBox())
422+ sb.append(newBox())
389423 sb.append(EOL ).append(offsetBox).append(" Explanation (enabled by `-explain`)" )
390424 sb.append(EOL ).append(newBox(soft = true ))
391425 dia.msg.explanation.split(raw " \R " ).foreach: line =>
392426 sb.append(EOL ).append(offsetBox).append(if line.isEmpty then " " else " " ).append(line)
393427 sb.append(EOL ).append(endBox)
394428 else if dia.msg.canExplain then
395- sb.append(EOL ).append( offsetBox)
429+ sb.append(offsetBox)
396430 sb.append(EOL ).append(offsetBox).append(" longer explanation available when compiling with `-explain`" )
397431
398432 sb.toString
0 commit comments