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 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Godot;
|
||||
using Godot.Collections;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Classification;
|
||||
using SharpIDE.Godot.Features.CodeEditor;
|
||||
using SharpIDE.RazorAccess;
|
||||
|
||||
namespace SharpIDE.Godot;
|
||||
@@ -30,6 +31,67 @@ public partial class CustomHighlighter : SyntaxHighlighter
|
||||
.ToList();
|
||||
_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)
|
||||
{
|
||||
var highlights = (_classifiedSpansByLine, _razorClassifiedSpansByLine) switch
|
||||
|
||||
@@ -15,6 +15,7 @@ using Timer = Godot.Timer;
|
||||
|
||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||
|
||||
#pragma warning disable VSTHRD101
|
||||
public partial class SharpIdeCodeEdit : CodeEdit
|
||||
{
|
||||
[Signal]
|
||||
@@ -48,6 +49,41 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
SymbolHovered += OnSymbolHovered;
|
||||
SymbolValidate += OnSymbolValidate;
|
||||
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()
|
||||
@@ -208,19 +244,31 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
// 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 async void OnTextChanged()
|
||||
private CancellationTokenSource _textChangedCts = new();
|
||||
private void 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
|
||||
// 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;
|
||||
await Singletons.FileManager.UpdateFileTextInMemory(_currentFile, Text);
|
||||
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
||||
var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
|
||||
SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting);
|
||||
__?.Dispose();
|
||||
_ = Task.GodotRun(async () => SetDiagnosticsModel(await RoslynAnalysis.GetDocumentDiagnostics(_currentFile)));
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
_currentFile.IsDirty.Value = true;
|
||||
await Singletons.FileManager.UpdateFileTextInMemory(_currentFile, Text);
|
||||
await _textChangedCts.CancelAsync(); // Currently the below methods throw, TODO Fix with suppress throwing, and handle
|
||||
_textChangedCts.Dispose();
|
||||
_textChangedCts = new CancellationTokenSource();
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
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
|
||||
@@ -296,7 +344,8 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
});
|
||||
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, setTextTask); // Text must be set before setting syntax highlighting
|
||||
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()
|
||||
@@ -400,17 +449,19 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
SetLineBackgroundColor(line, lineColour);
|
||||
}
|
||||
|
||||
[RequiresGodotUiThread]
|
||||
private void SetDiagnosticsModel(ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> diagnostics)
|
||||
{
|
||||
_diagnostics = diagnostics;
|
||||
Callable.From(QueueRedraw).CallDeferred();
|
||||
QueueRedraw();
|
||||
}
|
||||
|
||||
[RequiresGodotUiThread]
|
||||
private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans, IEnumerable<SharpIdeRazorClassifiedSpan> razorClassifiedSpans)
|
||||
{
|
||||
_syntaxHighlighter.SetHighlightingData(classifiedSpans, razorClassifiedSpans);
|
||||
//_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 = _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
|
||||
{
|
||||
extension(TreeItem treeItem)
|
||||
|
||||
Reference in New Issue
Block a user