From 78c170ee999fb237443e3d7869f0debc2f5f455c Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 5 Dec 2025 22:23:55 +0800 Subject: [PATCH 1/4] Feat Further optimize the rich style of diagnostic message --- src/Compiler/Driver/CompilerDiagnostics.fs | 82 +++++++++++++++++++--- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/src/Compiler/Driver/CompilerDiagnostics.fs b/src/Compiler/Driver/CompilerDiagnostics.fs index 71b0ba0eb2c..bbcf7e935b9 100644 --- a/src/Compiler/Driver/CompilerDiagnostics.fs +++ b/src/Compiler/Driver/CompilerDiagnostics.fs @@ -2251,6 +2251,76 @@ type PhasedDiagnostic with // 'true' for "canSuggestNames" is passed last here because we want to report suggestions in fsc.exe and fsi.exe, just not in regular IDE usage. let diagnostics = CollectFormattedDiagnostics(tcConfig, severity, diagnostic, true) + let renderRich (details: FormattedDiagnosticDetailedInfo) = + let severityText = + match severity with + | FSharpDiagnosticSeverity.Error -> "Error" + | FSharpDiagnosticSeverity.Warning -> "Warning" + | FSharpDiagnosticSeverity.Info + | FSharpDiagnosticSeverity.Hidden -> "Info" + + let codeText = sprintf "FS%04d" details.Canonical.ErrorNumber + let lines = ResizeArray() + + // For multi-line messages, the first line with number and severity, the rest are indented + let messageLines = details.Message.Split([| '\n' |], StringSplitOptions.None) + if messageLines.Length > 0 then + lines.Add(sprintf "[%s] %s: %s" codeText severityText messageLines[0]) + for i in 1 .. messageLines.Length - 1 do + lines.Add(sprintf "│ %s" messageLines[i]) + else + lines.Add(sprintf "[%s] %s" codeText details.Message) + + let addLocationAndSnippet (l: FormattedDiagnosticLocation) = + let range = l.Range + let fileDisplay = if String.IsNullOrWhiteSpace l.File then "unknown" else l.File + lines.Add(sprintf "└─ [%s:(%d,%d)]" fileDisplay range.StartLine range.StartColumn) + lines.Add("") + + let tryRenderSnippet () = + try + let fullPath = + l.File + |> FileSystem.GetFullFilePathInDirectoryShim tcConfig.implicitIncludeDir + + if FileSystem.FileExistsShim fullPath then + let content = File.ReadAllLines fullPath + + if content.Length > 0 then + let startLine = max 1 (range.StartLine - 1) + let endLine = min content.Length (range.StartLine) + let snippetLines = + [ for ln in startLine .. min content.Length (endLine + 1) do + yield ln, content[ln - 1] ] + + let lineNoWidth = + snippetLines + |> List.map fst + |> List.map string + |> List.map String.length + |> List.max + + let caretStart = max 0 (range.StartColumn - 1) + let caretWidth = max 1 (max 0 (range.EndColumn - range.StartColumn)) + let caretLine = String.make caretStart ' ' + String.make caretWidth '^' + + for (ln, text) in snippetLines do + lines.Add(sprintf " %*d | %s" lineNoWidth ln text) + + lines.Add(sprintf " %s | %s" (String.make lineNoWidth ' ') caretLine) + with _ -> + () + + tryRenderSnippet () + + match details.Location with + | Some l when not l.IsEmpty -> addLocationAndSnippet l + | _ -> () + + for i = 0 to lines.Count - 1 do + buf.AppendString lines[i] + if i < lines.Count - 1 then buf.AppendString "\n" + for e in diagnostics do Printf.bprintf buf "\n" @@ -2273,17 +2343,7 @@ type PhasedDiagnostic with buf.AppendString details.Canonical.TextRepresentation buf.AppendString details.Message - | DiagnosticStyle.Rich -> - buf.AppendString details.Canonical.TextRepresentation - buf.AppendString details.Message - - match details.Location with - | Some l when not l.IsEmpty -> - buf.AppendString l.TextRepresentation - - if details.Context.IsSome then - buf.AppendString details.Context.Value - | _ -> () + | DiagnosticStyle.Rich -> renderRich details member diagnostic.OutputContext(buf, prefix, fileLineFunction) = match diagnostic.Range with From 57c1bfda90ff354dbb9ba5ef6c0d8c21cb322488 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 6 Dec 2025 11:02:11 +0800 Subject: [PATCH 2/4] Use seq instead of resize array --- src/Compiler/Driver/CompilerDiagnostics.fs | 127 +++++++++++---------- 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/src/Compiler/Driver/CompilerDiagnostics.fs b/src/Compiler/Driver/CompilerDiagnostics.fs index bbcf7e935b9..6d2395ff89b 100644 --- a/src/Compiler/Driver/CompilerDiagnostics.fs +++ b/src/Compiler/Driver/CompilerDiagnostics.fs @@ -2246,7 +2246,7 @@ type PhasedDiagnostic with /// used by fsc.exe and fsi.exe, but not by VS /// prints error and related errors to the specified StringBuilder - member diagnostic.Output(buf, tcConfig: TcConfig, severity) = + member diagnostic.Output(buf: StringBuilder, tcConfig: TcConfig, severity: FSharpDiagnosticSeverity) = // 'true' for "canSuggestNames" is passed last here because we want to report suggestions in fsc.exe and fsi.exe, just not in regular IDE usage. let diagnostics = CollectFormattedDiagnostics(tcConfig, severity, diagnostic, true) @@ -2260,66 +2260,75 @@ type PhasedDiagnostic with | FSharpDiagnosticSeverity.Hidden -> "Info" let codeText = sprintf "FS%04d" details.Canonical.ErrorNumber - let lines = ResizeArray() - - // For multi-line messages, the first line with number and severity, the rest are indented - let messageLines = details.Message.Split([| '\n' |], StringSplitOptions.None) - if messageLines.Length > 0 then - lines.Add(sprintf "[%s] %s: %s" codeText severityText messageLines[0]) - for i in 1 .. messageLines.Length - 1 do - lines.Add(sprintf "│ %s" messageLines[i]) - else - lines.Add(sprintf "[%s] %s" codeText details.Message) - - let addLocationAndSnippet (l: FormattedDiagnosticLocation) = - let range = l.Range - let fileDisplay = if String.IsNullOrWhiteSpace l.File then "unknown" else l.File - lines.Add(sprintf "└─ [%s:(%d,%d)]" fileDisplay range.StartLine range.StartColumn) - lines.Add("") - - let tryRenderSnippet () = - try - let fullPath = - l.File - |> FileSystem.GetFullFilePathInDirectoryShim tcConfig.implicitIncludeDir - - if FileSystem.FileExistsShim fullPath then - let content = File.ReadAllLines fullPath - - if content.Length > 0 then - let startLine = max 1 (range.StartLine - 1) - let endLine = min content.Length (range.StartLine) - let snippetLines = - [ for ln in startLine .. min content.Length (endLine + 1) do - yield ln, content[ln - 1] ] - - let lineNoWidth = - snippetLines - |> List.map fst - |> List.map string - |> List.map String.length - |> List.max - - let caretStart = max 0 (range.StartColumn - 1) - let caretWidth = max 1 (max 0 (range.EndColumn - range.StartColumn)) - let caretLine = String.make caretStart ' ' + String.make caretWidth '^' + let messageSentences = + details.Message.Split([| '\n' |], StringSplitOptions.None) + |> Seq.collect (fun line -> + let parts = line.Split([| '.' |], StringSplitOptions.None) - for (ln, text) in snippetLines do - lines.Add(sprintf " %*d | %s" lineNoWidth ln text) + parts + |> Seq.mapi (fun idx part -> + let trimmed = part.Trim() - lines.Add(sprintf " %s | %s" (String.make lineNoWidth ' ') caretLine) - with _ -> - () - - tryRenderSnippet () - - match details.Location with - | Some l when not l.IsEmpty -> addLocationAndSnippet l - | _ -> () - - for i = 0 to lines.Count - 1 do - buf.AppendString lines[i] - if i < lines.Count - 1 then buf.AppendString "\n" + if trimmed = "" then + None + else + let hasDot = idx < parts.Length - 1 || line.EndsWith(".") + Some(if hasDot then trimmed + "." else trimmed))) + |> Seq.choose id + |> Seq.toList + + let messageLines = + match messageSentences with + | head :: tail -> + seq { + yield sprintf "[%s] %s: %s" codeText severityText head + yield! tail |> Seq.map (fun s -> sprintf "│ %s" s) + } + | [] -> seq { yield sprintf "[%s] %s" codeText details.Message } + + let locationAndSnippet = + match details.Location with + | Some l when not l.IsEmpty -> + seq { + let range = l.Range + let fileDisplay = if String.IsNullOrWhiteSpace l.File then "unknown" else l.File + yield sprintf "└─ [%s:(%d,%d)]" fileDisplay range.StartLine range.StartColumn + yield "" + + try + let fullPath = l.File |> FileSystem.GetFullFilePathInDirectoryShim tcConfig.implicitIncludeDir + + if FileSystem.FileExistsShim fullPath then + let content = File.ReadAllLines fullPath + + if content.Length > 0 then + let startLine = max 1 (range.StartLine - 1) + let endLine = min content.Length range.StartLine + let snippetLines = + [ for ln in startLine .. min content.Length (endLine + 1) -> ln, content[ln - 1] ] + + let lineNoWidth = + snippetLines + |> List.map fst + |> List.map string + |> List.map String.length + |> List.max + + let caretStart = max 0 (range.StartColumn - 1) + let caretWidth = max 1 (max 0 (range.EndColumn - range.StartColumn)) + let caretLine = String.make caretStart ' ' + String.make caretWidth '^' + + for (ln, text) in snippetLines do + yield sprintf " %*d | %s" lineNoWidth ln text + if ln = range.StartLine then + yield sprintf " %s | %s" (String.make lineNoWidth ' ') caretLine + with _ -> () + } + | _ -> Seq.empty + + Seq.append messageLines locationAndSnippet + |> String.concat "\n" + |> fun rendered -> buf.Append(rendered) |> ignore for e in diagnostics do Printf.bprintf buf "\n" From bd92cbf3c2a53c9c47c580bd352c010ab78a5f3a Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 6 Dec 2025 11:08:49 +0800 Subject: [PATCH 3/4] Add Release notes --- docs/release-notes/.FSharp.Compiler.Service/10.0.200.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/.FSharp.Compiler.Service/10.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/10.0.200.md index 03c30d64d6c..1c946cea600 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/10.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/10.0.200.md @@ -8,3 +8,7 @@ * `SynExprLetOrUseTrivia` is now `SynLetOrUseTrivia`. ([PR #19090](https://github.com/dotnet/fsharp/pull/19090)) * `SynMemberDefn.LetBindings` has trivia. ([PR #19090](https://github.com/dotnet/fsharp/pull/19090)) * `SynModuleDecl.Let` has trivia. ([PR #19090](https://github.com/dotnet/fsharp/pull/19090)) + +### Changed + +* Improve rich diagnostic formatting. ([PR #19141](https://github.com/dotnet/fsharp/pull/19141)) From c6fe52455306d178281a0ef244be86480f7e9ce3 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sat, 6 Dec 2025 11:15:44 +0800 Subject: [PATCH 4/4] reformat --- src/Compiler/Driver/CompilerDiagnostics.fs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Compiler/Driver/CompilerDiagnostics.fs b/src/Compiler/Driver/CompilerDiagnostics.fs index 6d2395ff89b..4c836241d1b 100644 --- a/src/Compiler/Driver/CompilerDiagnostics.fs +++ b/src/Compiler/Driver/CompilerDiagnostics.fs @@ -2260,6 +2260,7 @@ type PhasedDiagnostic with | FSharpDiagnosticSeverity.Hidden -> "Info" let codeText = sprintf "FS%04d" details.Canonical.ErrorNumber + let messageSentences = details.Message.Split([| '\n' |], StringSplitOptions.None) |> Seq.collect (fun line -> @@ -2291,12 +2292,19 @@ type PhasedDiagnostic with | Some l when not l.IsEmpty -> seq { let range = l.Range - let fileDisplay = if String.IsNullOrWhiteSpace l.File then "unknown" else l.File + + let fileDisplay = + if String.IsNullOrWhiteSpace l.File then + "unknown" + else + l.File + yield sprintf "└─ [%s:(%d,%d)]" fileDisplay range.StartLine range.StartColumn yield "" try - let fullPath = l.File |> FileSystem.GetFullFilePathInDirectoryShim tcConfig.implicitIncludeDir + let fullPath = + l.File |> FileSystem.GetFullFilePathInDirectoryShim tcConfig.implicitIncludeDir if FileSystem.FileExistsShim fullPath then let content = File.ReadAllLines fullPath @@ -2304,8 +2312,11 @@ type PhasedDiagnostic with if content.Length > 0 then let startLine = max 1 (range.StartLine - 1) let endLine = min content.Length range.StartLine + let snippetLines = - [ for ln in startLine .. min content.Length (endLine + 1) -> ln, content[ln - 1] ] + [ + for ln in startLine .. min content.Length (endLine + 1) -> ln, content[ln - 1] + ] let lineNoWidth = snippetLines @@ -2320,9 +2331,11 @@ type PhasedDiagnostic with for (ln, text) in snippetLines do yield sprintf " %*d | %s" lineNoWidth ln text + if ln = range.StartLine then yield sprintf " %s | %s" (String.make lineNoWidth ' ') caretLine - with _ -> () + with _ -> + () } | _ -> Seq.empty