refactor code editor to scene
This commit is contained in:
14
src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.tscn
Normal file
14
src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.tscn
Normal file
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c5dlwgcx3ubyp"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cinaqbdghcvoi" path="res://Features/CodeEditor/SharpIdeCodeEdit.tscn" id="1_y4okr"]
|
||||
|
||||
[node name="CodeEditorPanel" type="MarginContainer"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="SharpIdeCodeEdit" parent="." instance=ExtResource("1_y4okr")]
|
||||
layout_mode = 2
|
||||
delimiter_strings = Array[String](["\" \"", "' '"])
|
||||
360
src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs
Normal file
360
src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using System.Collections.Immutable;
|
||||
using Ardalis.GuardClauses;
|
||||
using Godot;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Classification;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.Debugging;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.RazorAccess;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace SharpIDE.Godot;
|
||||
|
||||
public partial class SharpIdeCodeEdit : CodeEdit
|
||||
{
|
||||
[Signal]
|
||||
public delegate void CodeFixesRequestedEventHandler();
|
||||
|
||||
private int _currentLine;
|
||||
private int _selectionStartCol;
|
||||
private int _selectionEndCol;
|
||||
|
||||
public SharpIdeSolutionModel? Solution { get; set; }
|
||||
private SharpIdeFile _currentFile = null!;
|
||||
|
||||
private CustomHighlighter _syntaxHighlighter = new();
|
||||
private PopupMenu _popupMenu = null!;
|
||||
|
||||
private ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> _diagnostics = [];
|
||||
private ImmutableArray<CodeAction> _currentCodeActionsInPopup = [];
|
||||
private ExecutionStopInfo? _executionStopInfo;
|
||||
private bool _fileChangingSuppressBreakpointToggleEvent;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
SyntaxHighlighter = _syntaxHighlighter;
|
||||
_popupMenu = GetNode<PopupMenu>("CodeFixesMenu");
|
||||
_popupMenu.IdPressed += OnCodeFixSelected;
|
||||
CodeCompletionRequested += OnCodeCompletionRequested;
|
||||
CodeFixesRequested += OnCodeFixesRequested;
|
||||
BreakpointToggled += OnBreakpointToggled;
|
||||
CaretChanged += OnCaretChanged;
|
||||
TextChanged += OnTextChanged;
|
||||
SymbolHovered += OnSymbolHovered;
|
||||
SymbolValidate += OnSymbolValidate;
|
||||
SymbolLookup += OnSymbolLookup;
|
||||
GlobalEvents.DebuggerExecutionStopped += OnDebuggerExecutionStopped;
|
||||
}
|
||||
|
||||
private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo)
|
||||
{
|
||||
Guard.Against.Null(Solution, nameof(Solution));
|
||||
if (executionStopInfo.FilePath != _currentFile.Path)
|
||||
{
|
||||
var file = Solution.AllFiles.Single(s => s.Path == executionStopInfo.FilePath);
|
||||
await GodotGlobalEvents.InvokeFileExternallySelectedAndWait(file);
|
||||
}
|
||||
var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed
|
||||
Guard.Against.Negative(lineInt, nameof(lineInt));
|
||||
_executionStopInfo = executionStopInfo;
|
||||
|
||||
await this.InvokeAsync(() =>
|
||||
{
|
||||
SetLineBackgroundColor(lineInt, new Color("665001"));
|
||||
SetLineAsExecuting(lineInt, true);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBreakpointToggled(long line)
|
||||
{
|
||||
if (_fileChangingSuppressBreakpointToggleEvent) return;
|
||||
var lineInt = (int)line;
|
||||
var breakpointAdded = IsLineBreakpointed(lineInt);
|
||||
var lineForDebugger = lineInt + 1; // Godot is 0-indexed, Debugging is 1-indexed
|
||||
var breakpoints = Singletons.RunService.Breakpoints.GetOrAdd(_currentFile, []);
|
||||
if (breakpointAdded)
|
||||
{
|
||||
breakpoints.Add(new Breakpoint { Line = lineForDebugger } );
|
||||
}
|
||||
else
|
||||
{
|
||||
var breakpoint = breakpoints.Single(b => b.Line == lineForDebugger);
|
||||
breakpoints.Remove(breakpoint);
|
||||
}
|
||||
SetLineColour(lineInt);
|
||||
GD.Print($"Breakpoint {(breakpointAdded ? "added" : "removed")} at line {lineForDebugger}");
|
||||
}
|
||||
|
||||
private void OnSymbolLookup(string symbol, long line, long column)
|
||||
{
|
||||
GD.Print($"Symbol lookup requested: {symbol} at line {line}, column {column}");
|
||||
}
|
||||
|
||||
private void OnSymbolValidate(string symbol)
|
||||
{
|
||||
GD.Print($"Symbol validating: {symbol}");
|
||||
SetSymbolLookupWordAsValid(true);
|
||||
}
|
||||
|
||||
private void OnSymbolHovered(string symbol, long line, long column)
|
||||
{
|
||||
GD.Print($"Symbol hovered: {symbol}");
|
||||
}
|
||||
|
||||
private void OnCaretChanged()
|
||||
{
|
||||
_selectionStartCol = GetSelectionFromColumn();
|
||||
_selectionEndCol = GetSelectionToColumn();
|
||||
_currentLine = GetCaretLine();
|
||||
GD.Print($"Selection changed to line {_currentLine}, start {_selectionStartCol}, end {_selectionEndCol}");
|
||||
}
|
||||
|
||||
private void OnTextChanged()
|
||||
{
|
||||
// update the MSBuildWorkspace
|
||||
RoslynAnalysis.UpdateDocument(_currentFile, Text);
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
||||
var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
|
||||
var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
||||
var slnDiagnostics = RoslynAnalysis.UpdateSolutionDiagnostics();
|
||||
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, diagnostics);
|
||||
Callable.From(() =>
|
||||
{
|
||||
SetSyntaxHighlightingModel(syntaxHighlighting.Result, razorSyntaxHighlighting.Result);
|
||||
SetDiagnosticsModel(diagnostics.Result);
|
||||
}).CallDeferred();
|
||||
await slnDiagnostics;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCodeFixSelected(long id)
|
||||
{
|
||||
GD.Print($"Code fix selected: {id}");
|
||||
var codeAction = _currentCodeActionsInPopup[(int)id];
|
||||
if (codeAction is null) return;
|
||||
var currentCaretPosition = GetCaretPosition();
|
||||
var vScroll = GetVScroll();
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
await RoslynAnalysis.ApplyCodeActionAsync(codeAction);
|
||||
var fileContents = await File.ReadAllTextAsync(_currentFile.Path);
|
||||
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
||||
var razorSyntaxHighlighting = await RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
|
||||
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
||||
Callable.From(() =>
|
||||
{
|
||||
BeginComplexOperation();
|
||||
SetText(fileContents);
|
||||
SetSyntaxHighlightingModel(syntaxHighlighting, razorSyntaxHighlighting);
|
||||
SetDiagnosticsModel(diagnostics);
|
||||
SetCaretLine(currentCaretPosition.line);
|
||||
SetCaretColumn(currentCaretPosition.col);
|
||||
SetVScroll(vScroll);
|
||||
EndComplexOperation();
|
||||
}).CallDeferred();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Ensure not running on UI thread
|
||||
public async Task SetSharpIdeFile(SharpIdeFile file)
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); // get off the UI thread
|
||||
_currentFile = file;
|
||||
var readFileTask = File.ReadAllTextAsync(_currentFile.Path);
|
||||
|
||||
var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
||||
var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile);
|
||||
var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
||||
var setTextTask = this.InvokeAsync(async () =>
|
||||
{
|
||||
_fileChangingSuppressBreakpointToggleEvent = true;
|
||||
SetText(await readFileTask);
|
||||
_fileChangingSuppressBreakpointToggleEvent = false;
|
||||
});
|
||||
await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, setTextTask); // Text must be set before setting syntax highlighting
|
||||
SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting);
|
||||
SetDiagnosticsModel(await diagnostics);
|
||||
}
|
||||
|
||||
public void UnderlineRange(int line, int caretStartCol, int caretEndCol, Color color, float thickness = 1.5f)
|
||||
{
|
||||
if (line < 0 || line >= GetLineCount())
|
||||
return;
|
||||
|
||||
if (caretStartCol > caretEndCol) // something went wrong
|
||||
return;
|
||||
|
||||
// Clamp columns to line length
|
||||
int lineLength = GetLine(line).Length;
|
||||
caretStartCol = Mathf.Clamp(caretStartCol, 0, lineLength);
|
||||
caretEndCol = Mathf.Clamp(caretEndCol, 0, lineLength);
|
||||
|
||||
// GetRectAtLineColumn returns the rectangle for the character before the column passed in, or the first character if the column is 0.
|
||||
var startRect = GetRectAtLineColumn(line, caretStartCol);
|
||||
var endRect = GetRectAtLineColumn(line, caretEndCol);
|
||||
//DrawLine(startRect.Position, startRect.End, color);
|
||||
//DrawLine(endRect.Position, endRect.End, color);
|
||||
|
||||
var startPos = startRect.End;
|
||||
if (caretStartCol is 0)
|
||||
{
|
||||
startPos.X -= startRect.Size.X;
|
||||
}
|
||||
var endPos = endRect.End;
|
||||
startPos.Y -= 3;
|
||||
endPos.Y -= 3;
|
||||
if (caretStartCol == caretEndCol)
|
||||
{
|
||||
endPos.X += 10;
|
||||
}
|
||||
DrawDashedLine(startPos, endPos, color, thickness);
|
||||
//DrawLine(startPos, endPos, color, thickness);
|
||||
}
|
||||
public override void _Draw()
|
||||
{
|
||||
//UnderlineRange(_currentLine, _selectionStartCol, _selectionEndCol, new Color(1, 0, 0));
|
||||
foreach (var (fileSpan, diagnostic) in _diagnostics)
|
||||
{
|
||||
if (diagnostic.Location.IsInSource)
|
||||
{
|
||||
var line = fileSpan.StartLinePosition.Line;
|
||||
var startCol = fileSpan.StartLinePosition.Character;
|
||||
var endCol = fileSpan.EndLinePosition.Character;
|
||||
var color = diagnostic.Severity switch
|
||||
{
|
||||
DiagnosticSeverity.Error => new Color(1, 0, 0),
|
||||
DiagnosticSeverity.Warning => new Color("ffb700"),
|
||||
_ => new Color(0, 1, 0) // Info or other
|
||||
};
|
||||
UnderlineRange(line, startCol, endCol, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void _UnhandledKeyInput(InputEvent @event)
|
||||
{
|
||||
if (@event.IsActionPressed(InputStringNames.CodeFixes))
|
||||
{
|
||||
EmitSignalCodeFixesRequested();
|
||||
}
|
||||
else if (@event.IsActionPressed(InputStringNames.StepOver))
|
||||
{
|
||||
SendDebuggerStepOver();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendDebuggerStepOver()
|
||||
{
|
||||
if (_executionStopInfo is null) return;
|
||||
var godotLine = _executionStopInfo.Line - 1;
|
||||
SetLineAsExecuting(godotLine, false);
|
||||
SetLineColour(godotLine);
|
||||
var threadId = _executionStopInfo.ThreadId;
|
||||
_executionStopInfo = null;
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
await Singletons.RunService.SendDebuggerStepOver(threadId);
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Color _breakpointLineColor = new Color("3a2323");
|
||||
private readonly Color _executingLineColor = new Color("665001");
|
||||
private void SetLineColour(int line)
|
||||
{
|
||||
var breakpointed = IsLineBreakpointed(line);
|
||||
var executing = IsLineExecuting(line);
|
||||
var lineColour = (breakpointed, executing) switch
|
||||
{
|
||||
(_, true) => _executingLineColor,
|
||||
(true, false) => _breakpointLineColor,
|
||||
(false, false) => Colors.Transparent
|
||||
};
|
||||
SetLineBackgroundColor(line, lineColour);
|
||||
}
|
||||
|
||||
private void SetDiagnosticsModel(ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> diagnostics)
|
||||
{
|
||||
_diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans, IEnumerable<SharpIdeRazorClassifiedSpan> razorClassifiedSpans)
|
||||
{
|
||||
_syntaxHighlighter.ClassifiedSpans = classifiedSpans.ToHashSet();
|
||||
_syntaxHighlighter.RazorClassifiedSpans = razorClassifiedSpans.ToHashSet();
|
||||
Callable.From(() =>
|
||||
{
|
||||
_syntaxHighlighter.ClearHighlightingCache();
|
||||
//_syntaxHighlighter.UpdateCache();
|
||||
SyntaxHighlighter = null;
|
||||
SyntaxHighlighter = _syntaxHighlighter; // Reassign to trigger redraw
|
||||
GD.Print("Provided syntax highlighting");
|
||||
}).CallDeferred();
|
||||
}
|
||||
|
||||
private void OnCodeFixesRequested()
|
||||
{
|
||||
var (caretLine, caretColumn) = GetCaretPosition();
|
||||
var popupMenuPosition = GetCaretDrawPos() with { X = 0 } + GetGlobalPosition();
|
||||
_popupMenu.Position = new Vector2I((int)popupMenuPosition.X, (int)popupMenuPosition.Y);
|
||||
_popupMenu.Clear();
|
||||
_popupMenu.AddItem("Getting Context Actions...", 0);
|
||||
_popupMenu.Popup();
|
||||
GD.Print($"Code fixes requested at line {caretLine}, column {caretColumn}");
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
var linePos = new LinePosition(caretLine, caretColumn);
|
||||
var codeActions = await RoslynAnalysis.GetCodeFixesForDocumentAtPosition(_currentFile, linePos);
|
||||
Callable.From(() =>
|
||||
{
|
||||
_popupMenu.Clear();
|
||||
foreach (var (index, codeAction) in codeActions.Index())
|
||||
{
|
||||
_currentCodeActionsInPopup = codeActions;
|
||||
_popupMenu.AddItem(codeAction.Title, index);
|
||||
//_popupMenu.SetItemMetadata(menuItem, codeAction);
|
||||
}
|
||||
|
||||
if (codeActions.Length is not 0) _popupMenu.SetFocusedItem(0);
|
||||
GD.Print($"Code fixes found: {codeActions.Length}, displaying menu");
|
||||
}).CallDeferred();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCodeCompletionRequested()
|
||||
{
|
||||
var (caretLine, caretColumn) = GetCaretPosition();
|
||||
|
||||
GD.Print($"Code completion requested at line {caretLine}, column {caretColumn}");
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
var linePos = new LinePosition(caretLine, caretColumn);
|
||||
|
||||
var completions = await RoslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos);
|
||||
Callable.From(() =>
|
||||
{
|
||||
foreach (var completionItem in completions.ItemsList)
|
||||
{
|
||||
AddCodeCompletionOption(CodeCompletionKind.Class, completionItem.DisplayText, completionItem.DisplayText);
|
||||
}
|
||||
// partially working - displays menu only when caret is what CodeEdit determines as valid
|
||||
UpdateCodeCompletionOptions(true);
|
||||
//RequestCodeCompletion(true);
|
||||
GD.Print($"Found {completions.ItemsList.Count} completions, displaying menu");
|
||||
}).CallDeferred();
|
||||
});
|
||||
}
|
||||
|
||||
private (int line, int col) GetCaretPosition()
|
||||
{
|
||||
var caretColumn = GetCaretColumn();
|
||||
var caretLine = GetCaretLine();
|
||||
return (caretLine, caretColumn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://du2lt7r1p1qfy
|
||||
48
src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn
Normal file
48
src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.tscn
Normal file
@@ -0,0 +1,48 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://cinaqbdghcvoi"]
|
||||
|
||||
[ext_resource type="FontFile" uid="uid://7jc0nj310cu6" path="res://CascadiaCode.ttf" id="1_s7ira"]
|
||||
[ext_resource type="Script" uid="uid://du2lt7r1p1qfy" path="res://Features/CodeEditor/SharpIdeCodeEdit.cs" id="2_kp2fd"]
|
||||
|
||||
[sub_resource type="FontVariation" id="FontVariation_y3aoi"]
|
||||
base_font = ExtResource("1_s7ira")
|
||||
spacing_top = 3
|
||||
spacing_bottom = 2
|
||||
baseline_offset = 0.05
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v06ln"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(0.117647, 0.117647, 0.117647, 1)
|
||||
border_color = Color(0, 0, 0, 0.6)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
corner_detail = 5
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7ptyn"]
|
||||
|
||||
[node name="SharpIdeCodeEdit" type="CodeEdit"]
|
||||
theme_override_colors/current_line_color = Color(0.0588235, 0.0588235, 0.0588235, 1)
|
||||
theme_override_fonts/font = SubResource("FontVariation_y3aoi")
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_styles/normal = SubResource("StyleBoxFlat_v06ln")
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_7ptyn")
|
||||
highlight_current_line = true
|
||||
symbol_lookup_on_click = true
|
||||
symbol_tooltip_on_hover = true
|
||||
gutters_draw_breakpoints_gutter = true
|
||||
gutters_draw_executing_lines = true
|
||||
gutters_draw_line_numbers = true
|
||||
code_completion_enabled = true
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
script = ExtResource("2_kp2fd")
|
||||
|
||||
[node name="CodeFixesMenu" type="PopupMenu" parent="."]
|
||||
size = Vector2i(217, 100)
|
||||
item_count = 1
|
||||
item_0/text = "Getting Context Actions..."
|
||||
item_0/id = 0
|
||||
Reference in New Issue
Block a user