attempt to improve syntax highlighting UX?
This commit is contained in:
@@ -457,10 +457,9 @@ public static class RoslynAnalysis
|
|||||||
|
|
||||||
var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
|
var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
|
||||||
var root = await syntaxTree!.GetRootAsync(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));
|
|
||||||
|
|
||||||
|
var classifiedSpans = await ClassifierHelper.GetClassifiedSpansAsync(document, root.FullSpan, ClassificationOptions.Default, false, cancellationToken);
|
||||||
|
var result = classifiedSpans.Select(s => (syntaxTree.GetMappedLineSpan(s.TextSpan), s)).ToList();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Godot;
|
|||||||
using Godot.Collections;
|
using Godot.Collections;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.Classification;
|
using Microsoft.CodeAnalysis.Classification;
|
||||||
|
using SharpIDE.Godot.Features.CodeEditor;
|
||||||
using SharpIDE.RazorAccess;
|
using SharpIDE.RazorAccess;
|
||||||
|
|
||||||
namespace SharpIDE.Godot;
|
namespace SharpIDE.Godot;
|
||||||
@@ -30,6 +31,67 @@ public partial class CustomHighlighter : SyntaxHighlighter
|
|||||||
.ToList();
|
.ToList();
|
||||||
_classifiedSpansByLine = spansGroupedByFileSpan.ToDictionary(g => g.Key, g => g.ToImmutableArray());
|
_classifiedSpansByLine = spansGroupedByFileSpan.ToDictionary(g => g.Key, g => g.ToImmutableArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Indicates that lines were removed or added, and the overall result of that is that a line (wasLineNumber), is now (becameLineNumber)
|
||||||
|
// So if you added a line above line 10, then wasLineNumber=10, becameLineNumber=11
|
||||||
|
// If you removed a line above line 10, then wasLineNumber=10, becameLineNumber=9
|
||||||
|
//
|
||||||
|
// This is all a very dodgy workaround to move highlighting up and down, while we wait for the workspace to return us highlighting for the updated file
|
||||||
|
public void LinesChanged(long wasLineNumber, long becameLineNumber, SharpIdeCodeEdit.LineEditOrigin origin)
|
||||||
|
{
|
||||||
|
var difference = (int)(becameLineNumber - wasLineNumber);
|
||||||
|
if (difference is 0) return;
|
||||||
|
if (difference > 0)
|
||||||
|
{
|
||||||
|
LinesAdded(wasLineNumber, difference, origin);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LinesRemoved(wasLineNumber, -difference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LinesAdded(long fromLine, int difference, SharpIdeCodeEdit.LineEditOrigin origin)
|
||||||
|
{
|
||||||
|
var newRazorDict = new System.Collections.Generic.Dictionary<int, ImmutableArray<SharpIdeRazorClassifiedSpan>>();
|
||||||
|
|
||||||
|
foreach (var kvp in _razorClassifiedSpansByLine)
|
||||||
|
{
|
||||||
|
bool shouldShift =
|
||||||
|
kvp.Key > fromLine || // always shift lines after the insertion point
|
||||||
|
(origin == SharpIdeCodeEdit.LineEditOrigin.StartOfLine && kvp.Key == fromLine); // shift current line if origin is Start
|
||||||
|
|
||||||
|
int newKey = shouldShift ? kvp.Key + difference : kvp.Key;
|
||||||
|
newRazorDict[newKey] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_razorClassifiedSpansByLine = newRazorDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LinesRemoved(long fromLine, int numberOfLinesRemoved)
|
||||||
|
{
|
||||||
|
// everything from 'fromLine' onwards needs to be shifted up by numberOfLinesRemoved
|
||||||
|
var newRazorDict = new System.Collections.Generic.Dictionary<int, ImmutableArray<SharpIdeRazorClassifiedSpan>>();
|
||||||
|
|
||||||
|
foreach (var kvp in _razorClassifiedSpansByLine)
|
||||||
|
{
|
||||||
|
if (kvp.Key < fromLine)
|
||||||
|
{
|
||||||
|
newRazorDict[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
else if (kvp.Key == fromLine)
|
||||||
|
{
|
||||||
|
newRazorDict[kvp.Key - numberOfLinesRemoved] = kvp.Value;
|
||||||
|
}
|
||||||
|
else if (kvp.Key >= fromLine + numberOfLinesRemoved)
|
||||||
|
{
|
||||||
|
newRazorDict[kvp.Key - numberOfLinesRemoved] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_razorClassifiedSpansByLine = newRazorDict;
|
||||||
|
}
|
||||||
|
|
||||||
public override Dictionary _GetLineSyntaxHighlighting(int line)
|
public override Dictionary _GetLineSyntaxHighlighting(int line)
|
||||||
{
|
{
|
||||||
var highlights = (_classifiedSpansByLine, _razorClassifiedSpansByLine) switch
|
var highlights = (_classifiedSpansByLine, _razorClassifiedSpansByLine) switch
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using Timer = Godot.Timer;
|
|||||||
|
|
||||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||||
|
|
||||||
|
#pragma warning disable VSTHRD101
|
||||||
public partial class SharpIdeCodeEdit : CodeEdit
|
public partial class SharpIdeCodeEdit : CodeEdit
|
||||||
{
|
{
|
||||||
[Signal]
|
[Signal]
|
||||||
@@ -48,6 +49,41 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
|||||||
SymbolHovered += OnSymbolHovered;
|
SymbolHovered += OnSymbolHovered;
|
||||||
SymbolValidate += OnSymbolValidate;
|
SymbolValidate += OnSymbolValidate;
|
||||||
SymbolLookup += OnSymbolLookup;
|
SymbolLookup += OnSymbolLookup;
|
||||||
|
LinesEditedFrom += OnLinesEditedFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LineEditOrigin
|
||||||
|
{
|
||||||
|
StartOfLine,
|
||||||
|
EndOfLine,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
// Line removed - fromLine 55, toLine 54
|
||||||
|
// Line added - fromLine 54, toLine 55
|
||||||
|
// Multi cursor gets a single line event for each
|
||||||
|
// problem is 10 to 11 gets returned for 'Enter' at the start of line 10, as well as 'Enter' at the end of line 10
|
||||||
|
// This means that the line that moves down needs to be based on whether the new line was from the start or end of the line
|
||||||
|
private void OnLinesEditedFrom(long fromLine, long toLine)
|
||||||
|
{
|
||||||
|
if (fromLine == toLine) return;
|
||||||
|
var fromLineText = GetLine((int)fromLine);
|
||||||
|
var caretPosition = this.GetCaretPosition();
|
||||||
|
var textFrom0ToCaret = fromLineText[..caretPosition.col];
|
||||||
|
var caretPositionEnum = LineEditOrigin.Unknown;
|
||||||
|
if (string.IsNullOrWhiteSpace(textFrom0ToCaret))
|
||||||
|
{
|
||||||
|
caretPositionEnum = LineEditOrigin.StartOfLine;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var textfromCaretToEnd = fromLineText[caretPosition.col..];
|
||||||
|
if (string.IsNullOrWhiteSpace(textfromCaretToEnd))
|
||||||
|
{
|
||||||
|
caretPositionEnum = LineEditOrigin.EndOfLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//GD.Print($"Lines edited from {fromLine} to {toLine}, origin: {caretPositionEnum}, current caret position: {caretPosition}");
|
||||||
|
_syntaxHighlighter.LinesChanged(fromLine, toLine, caretPositionEnum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
@@ -208,19 +244,31 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
|||||||
// GD.Print($"Selection changed to line {_currentLine}, start {_selectionStartCol}, end {_selectionEndCol}");
|
// GD.Print($"Selection changed to line {_currentLine}, start {_selectionStartCol}, end {_selectionEndCol}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ideally this method completes in < 35ms, to ~ handle 28 char/s spam typing
|
private CancellationTokenSource _textChangedCts = new();
|
||||||
private async void OnTextChanged()
|
private void OnTextChanged()
|
||||||
{
|
{
|
||||||
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}");
|
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}");
|
||||||
// Note that we are currently on the UI thread, so be very conscious of what we do here
|
_ = Task.GodotRun(async () =>
|
||||||
// If we update the documents syntax highlighting here, we won't get any flashes of incorrect highlighting, most noticeable when inserting new lines
|
{
|
||||||
_currentFile.IsDirty.Value = true;
|
_currentFile.IsDirty.Value = true;
|
||||||
await Singletons.FileManager.UpdateFileTextInMemory(_currentFile, Text);
|
await Singletons.FileManager.UpdateFileTextInMemory(_currentFile, Text);
|
||||||
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
await _textChangedCts.CancelAsync(); // Currently the below methods throw, TODO Fix with suppress throwing, and handle
|
||||||
var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
|
_textChangedCts.Dispose();
|
||||||
SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting);
|
_textChangedCts = new CancellationTokenSource();
|
||||||
__?.Dispose();
|
_ = Task.GodotRun(async () =>
|
||||||
_ = Task.GodotRun(async () => SetDiagnosticsModel(await RoslynAnalysis.GetDocumentDiagnostics(_currentFile)));
|
{
|
||||||
|
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile, _textChangedCts.Token);
|
||||||
|
var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile, _textChangedCts.Token);
|
||||||
|
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting);
|
||||||
|
await this.InvokeAsync(async () => SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting));
|
||||||
|
__?.Dispose();
|
||||||
|
});
|
||||||
|
_ = Task.GodotRun(async () =>
|
||||||
|
{
|
||||||
|
var documentDiagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile, _textChangedCts.Token);
|
||||||
|
await this.InvokeAsync(() => SetDiagnosticsModel(documentDiagnostics));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is now significantly slower, invoke -> text updated in editor
|
// TODO: This is now significantly slower, invoke -> text updated in editor
|
||||||
@@ -296,7 +344,8 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
|||||||
});
|
});
|
||||||
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, setTextTask); // Text must be set before setting syntax highlighting
|
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, setTextTask); // Text must be set before setting syntax highlighting
|
||||||
await this.InvokeAsync(async () => SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting));
|
await this.InvokeAsync(async () => SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting));
|
||||||
SetDiagnosticsModel(await diagnostics);
|
await diagnostics;
|
||||||
|
await this.InvokeAsync(async () => SetDiagnosticsModel(await diagnostics));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnFileChangedExternallyFromDisk()
|
private async Task OnFileChangedExternallyFromDisk()
|
||||||
@@ -400,17 +449,19 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
|||||||
SetLineBackgroundColor(line, lineColour);
|
SetLineBackgroundColor(line, lineColour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RequiresGodotUiThread]
|
||||||
private void SetDiagnosticsModel(ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> diagnostics)
|
private void SetDiagnosticsModel(ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> diagnostics)
|
||||||
{
|
{
|
||||||
_diagnostics = diagnostics;
|
_diagnostics = diagnostics;
|
||||||
Callable.From(QueueRedraw).CallDeferred();
|
QueueRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RequiresGodotUiThread]
|
||||||
private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans, IEnumerable<SharpIdeRazorClassifiedSpan> razorClassifiedSpans)
|
private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans, IEnumerable<SharpIdeRazorClassifiedSpan> razorClassifiedSpans)
|
||||||
{
|
{
|
||||||
_syntaxHighlighter.SetHighlightingData(classifiedSpans, razorClassifiedSpans);
|
_syntaxHighlighter.SetHighlightingData(classifiedSpans, razorClassifiedSpans);
|
||||||
//_syntaxHighlighter.ClearHighlightingCache();
|
//_syntaxHighlighter.ClearHighlightingCache();
|
||||||
_syntaxHighlighter.UpdateCache();
|
_syntaxHighlighter.UpdateCache(); // I don't think this does anything, it will call _UpdateCache which we have not implemented
|
||||||
SyntaxHighlighter = null;
|
SyntaxHighlighter = null;
|
||||||
SyntaxHighlighter = _syntaxHighlighter; // Reassign to trigger redraw
|
SyntaxHighlighter = _syntaxHighlighter; // Reassign to trigger redraw
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ public static class ControlExtensions
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Has no functionality, just used as a reminder to indicate that a method must be called on the Godot UI thread.
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
|
public class RequiresGodotUiThreadAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public static class NodeExtensions
|
public static class NodeExtensions
|
||||||
{
|
{
|
||||||
extension(TreeItem treeItem)
|
extension(TreeItem treeItem)
|
||||||
|
|||||||
Reference in New Issue
Block a user