From a2dce7ff17601faf8d7e3b6c29a654a4c207bc0b Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:39:06 +1000 Subject: [PATCH] document syntax highlighting --- .../Features/Analysis/RoslynAnalysis.cs | 18 +++++ src/SharpIDE.Godot/CustomSyntaxHighlighter.cs | 79 ++++++++++++++++--- src/SharpIDE.Godot/IdeRoot.cs | 3 + src/SharpIDE.Godot/IdeRoot.tscn | 3 + src/SharpIDE.Godot/SharpIdeCodeEdit.cs | 12 ++- 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index f8c35e1..1c2dadf 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using Ardalis.GuardClauses; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; @@ -143,6 +144,23 @@ public static class RoslynAnalysis return diagnostics; } + public static async Task> GetDocumentSyntaxHighlighting(SharpIdeFile fileModel) + { + await _solutionLoadedTcs.Task; + var cancellationToken = CancellationToken.None; + var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath); + var document = project.Documents.Single(s => s.FilePath == fileModel.Path); + Guard.Against.Null(document, nameof(document)); + + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken); + var root = await syntaxTree!.GetRootAsync(cancellationToken); + var classifiedSpans = await Classifier.GetClassifiedSpansAsync(document, root.FullSpan, cancellationToken); + + var result = classifiedSpans.Select(s => (syntaxTree.GetMappedLineSpan(s.TextSpan), s)); + + return result; + } + public static async Task> GetCodeFixesAsync(Diagnostic diagnostic) { var cancellationToken = CancellationToken.None; diff --git a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs index 44dec52..7ae3485 100644 --- a/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs +++ b/src/SharpIDE.Godot/CustomSyntaxHighlighter.cs @@ -1,30 +1,87 @@ -using System.Linq; +using System.Collections.Generic; using Godot; using Godot.Collections; -using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Classification; namespace SharpIDE.Godot; public partial class CustomHighlighter : SyntaxHighlighter { + public IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> ClassifiedSpans = []; public override Dictionary _GetLineSyntaxHighlighting(int line) + { + var highlights = MapClassifiedSpansToHighlights(line); + + return highlights; + } + + private static readonly StringName ColorStringName = "color"; + private Dictionary MapClassifiedSpansToHighlights(int line) { var highlights = new Dictionary(); - var text = GetTextEdit().GetLine(line); - var regex = new Regex(@"\bTODO\b"); - var matches = regex.Matches(text); - - foreach (Match match in matches) + foreach (var (fileSpan, classifiedSpan) in ClassifiedSpans) { - highlights[match.Index] = new Dictionary + // Only take spans on the requested line + if (fileSpan.StartLinePosition.Line != line) + continue; + + if (classifiedSpan.TextSpan.Length == 0) + continue; // Skip empty spans + + // Column index of the first character in this span + int columnIndex = fileSpan.StartLinePosition.Character; + + // Build the highlight entry + var highlightInfo = new Dictionary { - { "color", new Color(1, 0, 0) }, // red - { "underline", true }, // not implemented - { "length", match.Length } // not implemented + { ColorStringName, GetColorForClassification(classifiedSpan.ClassificationType) } }; + + highlights[columnIndex] = highlightInfo; } return highlights; } + + private Color GetColorForClassification(string classificationType) + { + return classificationType switch + { + // Keywords + "keyword" => new Color("569cd6"), + "keyword - control" => new Color("569cd6"), + "preprocessor keyword" => new Color("569cd6"), + + // Literals & comments + "string" => new Color("d69d85"), + "comment" => new Color("57a64a"), + "number" => new Color("b5cea8"), + + // Types (User Types) + "class name" => new Color("4ec9b0"), + "struct name" => new Color("4ec9b0"), + "interface name" => new Color("b8d7a3"), + "namespace name" => new Color("dcdcdc"), + + // Identifiers & members + "identifier" => new Color("dcdcdc"), + "method name" => new Color("dcdcaa"), + "extension method name" => new Color("dcdcaa"), + "property name" => new Color("dcdcdc"), + "static symbol" => new Color("dcdcdc"), + "parameter name" => new Color("9cdcfe"), + "local name" => new Color("9cdcfe"), + + // Punctuation & operators + "operator" => new Color("dcdcdc"), + "punctuation" => new Color("dcdcdc"), + + // Misc + "excluded code" => new Color("a9a9a9"), + + _ => new Color("dcdcdc") + }; + } } diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index 60a310f..4709b01 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -37,8 +37,11 @@ public partial class IdeRoot : Control var diFile = infraProject.Files.Single(s => s.Name == "DependencyInjection.cs"); var fileContents = await File.ReadAllTextAsync(diFile.Path); _sharpIdeCodeEdit.SetText(fileContents); + var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(diFile); + _sharpIdeCodeEdit.ProvideSyntaxHighlighting(syntaxHighlighting); var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(diFile); _sharpIdeCodeEdit.ProvideDiagnostics(diagnostics); + } catch (Exception e) { diff --git a/src/SharpIDE.Godot/IdeRoot.tscn b/src/SharpIDE.Godot/IdeRoot.tscn index 5af8c9d..b729058 100644 --- a/src/SharpIDE.Godot/IdeRoot.tscn +++ b/src/SharpIDE.Godot/IdeRoot.tscn @@ -51,7 +51,10 @@ layout_mode = 2 [node name="SharpIdeCodeEdit" type="CodeEdit" parent="VBoxContainer/HBoxContainer/HSplitContainer"] unique_name_in_owner = true layout_mode = 2 +theme_override_colors/current_line_color = Color(0.0588235, 0.0588235, 0.0588235, 1) +theme_override_colors/background_color = Color(0.117647, 0.117647, 0.117647, 1) theme_override_fonts/font = ExtResource("2_rk34b") +theme_override_font_sizes/font_size = 18 highlight_current_line = true gutters_draw_line_numbers = true code_completion_enabled = true diff --git a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs index 7a1013d..c0384ba 100644 --- a/src/SharpIDE.Godot/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/SharpIdeCodeEdit.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Collections.Immutable; using Godot; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Classification; namespace SharpIDE.Godot; @@ -12,6 +14,8 @@ public partial class SharpIdeCodeEdit : CodeEdit private int _currentLine; private int _selectionStartCol; private int _selectionEndCol; + + private CustomHighlighter _syntaxHighlighter = new(); private ImmutableArray _diagnostics = []; @@ -27,7 +31,7 @@ public partial class SharpIdeCodeEdit : CodeEdit _currentLine = GetCaretLine(); GD.Print($"Selection changed to line {_currentLine}, start {_selectionStartCol}, end {_selectionEndCol}"); }; - this.SyntaxHighlighter = new CustomHighlighter(); + this.SyntaxHighlighter = _syntaxHighlighter; } public void UnderlineRange(int line, int caretStartCol, int caretEndCol, Color color, float thickness = 1.5f) @@ -85,6 +89,12 @@ public partial class SharpIdeCodeEdit : CodeEdit { _diagnostics = diagnostics; } + public void ProvideSyntaxHighlighting(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans) + { + _syntaxHighlighter.ClassifiedSpans = classifiedSpans; + _syntaxHighlighter.UpdateCache(); // not sure if correct + QueueRedraw(); // TODO: Not working + } private void OnCodeFixesRequested() {