295 lines
8.9 KiB
C#
295 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
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.SolutionDiscovery;
|
|
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;
|
|
|
|
private SharpIdeFile _currentFile = null!;
|
|
|
|
private CustomHighlighter _syntaxHighlighter = new();
|
|
private PopupMenu _popupMenu = null!;
|
|
|
|
private ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> _diagnostics = [];
|
|
private ImmutableArray<CodeAction> _currentCodeActionsInPopup = [];
|
|
|
|
public override void _Ready()
|
|
{
|
|
SyntaxHighlighter = _syntaxHighlighter;
|
|
_popupMenu = GetNode<PopupMenu>("CodeFixesMenu");
|
|
_popupMenu.IdPressed += OnCodeFixSelected;
|
|
CodeCompletionRequested += OnCodeCompletionRequested;
|
|
CodeFixesRequested += OnCodeFixesRequested;
|
|
CaretChanged += OnCaretChanged;
|
|
TextChanged += OnTextChanged;
|
|
SymbolHovered += OnSymbolHovered;
|
|
SymbolValidate += OnSymbolValidate;
|
|
SymbolLookup += OnSymbolLookup;
|
|
}
|
|
|
|
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.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
|
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
|
Callable.From(() =>
|
|
{
|
|
SetSyntaxHighlightingModel(syntaxHighlighting);
|
|
SetDiagnosticsModel(diagnostics);
|
|
}).CallDeferred();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"Error Calling OnTextChanged: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
|
|
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.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await RoslynAnalysis.ApplyCodeActionAsync(codeAction);
|
|
var fileContents = await File.ReadAllTextAsync(_currentFile.Path);
|
|
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
|
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
|
Callable.From(() =>
|
|
{
|
|
BeginComplexOperation();
|
|
SetText(fileContents);
|
|
SetSyntaxHighlightingModel(syntaxHighlighting);
|
|
SetDiagnosticsModel(diagnostics);
|
|
SetCaretLine(currentCaretPosition.line);
|
|
SetCaretColumn(currentCaretPosition.col);
|
|
SetVScroll(vScroll);
|
|
EndComplexOperation();
|
|
}).CallDeferred();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.PrintErr($"Error applying code fix: {ex.Message}");
|
|
}
|
|
});
|
|
}
|
|
|
|
public async Task SetSharpIdeFile(SharpIdeFile file)
|
|
{
|
|
_currentFile = file;
|
|
var fileContents = await File.ReadAllTextAsync(_currentFile.Path);
|
|
SetText(fileContents);
|
|
var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile);
|
|
SetSyntaxHighlightingModel(syntaxHighlighting);
|
|
var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile);
|
|
SetDiagnosticsModel(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();
|
|
}
|
|
}
|
|
|
|
private void SetDiagnosticsModel(ImmutableArray<(FileLinePositionSpan fileSpan, Diagnostic diagnostic)> diagnostics)
|
|
{
|
|
_diagnostics = diagnostics;
|
|
}
|
|
|
|
private void SetSyntaxHighlightingModel(IEnumerable<(FileLinePositionSpan fileSpan, ClassifiedSpan classifiedSpan)> classifiedSpans)
|
|
{
|
|
_syntaxHighlighter.ClassifiedSpans = classifiedSpans;
|
|
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.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
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();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.Print(ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnCodeCompletionRequested()
|
|
{
|
|
var (caretLine, caretColumn) = GetCaretPosition();
|
|
|
|
GD.Print($"Code completion requested at line {caretLine}, column {caretColumn}");
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
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();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GD.Print(ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
private (int line, int col) GetCaretPosition()
|
|
{
|
|
var caretColumn = GetCaretColumn();
|
|
var caretLine = GetCaretLine();
|
|
return (caretLine, caretColumn);
|
|
}
|
|
} |