Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -0,0 +1,438 @@
@implements IDisposable
@using System.Timers
@using BlazorMonaco
@using BlazorMonaco.Editor
@using BlazorMonaco.Languages
@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.JSInterop
@using RobotNet10.Components.Clients
@using RobotNet10.ScriptEditor.Clients
@using RobotNet10.ScriptEditor.Helpers.Monaco.Languages
@using RobotNet10.ScriptEditor.Models
@using RobotNet10.ScriptEditor.Services
@using RobotNet10.ScriptEngine.Shared
@inject IJSRuntime jsRuntime
@inject ScriptWorkspace Workspace
@inject FileManagerHubClient FileManagerClient
@inject ScriptManagerHubClient ScriptManagerClient
@inject ISnackbar Snackbar
<div class="editor-container">
<div class="editor-header">
<div class="editor-header-left">
<span>@(Workspace.CurrentFile?.Name ?? "No file selected")</span>
</div>
<div class="editor-actions">
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
title="Reset Engine"
OnClick="HandleReset"
Disabled="@IsResetDisabled"
Size="@Size.Small"
Variant="@Variant.Text" />
<MudIconButton Icon="@Icons.Material.Filled.Build"
title="Build Scripts"
OnClick="HandleBuild"
Disabled="@IsBuildDisabled"
Size="@Size.Small"
Variant="@Variant.Text"
Color="@Color.Primary"/>
<MudIconButton Icon="@Icons.Material.Filled.PlayArrow"
title="Start Engine"
OnClick="HandleStart"
Disabled="@IsStartDisabled"
Size="@Size.Small"
Variant="@Variant.Text"
Color="@Color.Success" />
<MudIconButton Icon="@Icons.Material.Filled.Stop"
title="Stop Engine"
OnClick="HandleStop"
Disabled="@IsStopDisabled"
Size="@Size.Small"
Variant="@Variant.Text"
Color="@Color.Secondary" />
</div>
</div>
<div class="editor-content">
<StandaloneCodeEditor @ref="_editor" Id="script-code-editor"
ConstructionOptions="EditorConstructionOptions"
OnDidInit="EditorOnDidInit"
OnDidChangeModelContent="DidChangeModelContent" />
</div>
</div>
@code {
private object dotNetHelper = default!;
private IJSInProcessObjectReference? disposableSignatureHelpProvider;
private StandaloneCodeEditor? _editor = null;
private TextModel? _editorTextModel = null;
private readonly SemaphoreSlim _syncLock = new(1, 1);
private string _lastSyncedCode = "";
private System.Threading.Timer? _debounceTimer;
private readonly object _debounceLock = new();
protected override void OnInitialized()
{
// Subscribe to events early to catch state changes
Workspace.CurrentFileChanged += OnCurrentFileChanged;
Workspace.DiagnoticChanged += OnDiagnoticChanged;
ScriptManagerClient.StateChanged += OnScriptManagerStateChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
// Trigger state change handler to update UI with current state
// This ensures UI is updated even if state was loaded before subscription
OnScriptManagerStateChanged(ScriptManagerClient.State);
}
private void OnScriptManagerStateChanged(ScriptEngineState state)
{
if (ScriptManagerClient.State == ScriptEngineState.Idle && Workspace.CurrentFile is not null)
{
_editor?.UpdateOptions(new EditorUpdateOptions { ReadOnly = false }).ConfigureAwait(false);
}
else
{
_editor?.UpdateOptions(new EditorUpdateOptions { ReadOnly = true }).ConfigureAwait(false);
}
StateHasChanged();
}
private bool IsResetDisabled => ScriptManagerClient.State == ScriptEngineState.Initializing ||
ScriptManagerClient.State == ScriptEngineState.Resetting ||
ScriptManagerClient.State == ScriptEngineState.Building ||
ScriptManagerClient.State == ScriptEngineState.Starting ||
ScriptManagerClient.State == ScriptEngineState.Stopping;
private bool IsBuildDisabled => ScriptManagerClient.State != ScriptEngineState.Idle &&
ScriptManagerClient.State != ScriptEngineState.BuildError;
private bool IsStartDisabled => ScriptManagerClient.State != ScriptEngineState.Ready;
private bool IsStopDisabled => ScriptManagerClient.State != ScriptEngineState.Running;
private async Task HandleReset()
{
try
{
var result = await ScriptManagerClient.ResetAsync();
if (!result.IsSuccess)
{
Snackbar.Add($"Failed to reset engine: {result.Message}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error resetting engine: {ex.Message}", Severity.Error);
}
}
private async Task HandleBuild()
{
try
{
var result = await ScriptManagerClient.BuildAsync();
if (!result.IsSuccess)
{
Snackbar.Add($"Failed to build: {result.Message}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error building scripts: {ex.Message}", Severity.Error);
}
}
private async Task HandleStart()
{
try
{
var result = await ScriptManagerClient.StartEingineAsync();
if (!result.IsSuccess)
{
Snackbar.Add($"Failed to start engine: {result.Message}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error starting engine: {ex.Message}", Severity.Error);
}
}
private async Task HandleStop()
{
try
{
var result = await ScriptManagerClient.StopEingineAsync();
if (!result.IsSuccess)
{
Snackbar.Add($"Failed to stop engine: {result.Message}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error stopping engine: {ex.Message}", Severity.Error);
}
}
private static StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "csharp",
Theme = "vs-dark",
GlyphMargin = true,
AutomaticLayout = true,
ReadOnly = true,
Value = "",
};
}
private void OnCurrentFileChanged(ScriptFile? file)
{
_ = InvokeAsync(() => OnCurrentFileChangedAsync(file));
}
private async Task OnCurrentFileChangedAsync(ScriptFile? file)
{
if (_editor == null) return;
if (file is null)
{
await _editor.SetValue("");
await _editor.UpdateOptions(new EditorUpdateOptions { ReadOnly = true });
}
else
{
await _editor.SetValue(file.Code);
if (ScriptManagerClient.State == ScriptEngineState.Idle)
{
await _editor.UpdateOptions(new EditorUpdateOptions { ReadOnly = false });
}
await OnDiagnoticChangedAsync(file.Diagnostics);
}
// Update header to show current file name
StateHasChanged();
}
private async Task EditorOnDidInit()
{
if (_editor == null) return;
dotNetHelper = DotNetObjectReference.Create(this);
_editorTextModel = await _editor.GetModel();
await _editor.AddCommand((int)KeyMod.CtrlCmd | (int)KeyCode.KeyS, args =>
{
InvokeAsync(SaveCurrentFile).ConfigureAwait(false);
});
var triggerCharacters = new List<string>() { "." };
await BlazorMonaco.Languages.Global.RegisterDocumentFormattingEditProvider(jsRuntime, "csharp", OnFormatDocumentAsync);
await BlazorMonaco.Languages.Global.RegisterHoverProviderAsync(jsRuntime, "csharp", OnHoverAsync);
await BlazorMonaco.Languages.Global.RegisterCompletionItemProvider(jsRuntime, "csharp", new CompletionItemProvider(triggerCharacters, CompleteItemAsync, ResolveCompletionItemAsync));
disposableSignatureHelpProvider = await jsRuntime.InvokeAsync<IJSInProcessObjectReference>("robotnet.monaco.CSharpLanguageRegisterSignatureHelpProvider", dotNetHelper, nameof(GetSignatureHelp));
}
private async Task<TextEdit[]> OnFormatDocumentAsync(string modelUri, FormattingOptions options)
{
if (_editor is null || _editorTextModel is null || _editorTextModel.Uri != modelUri) return [];
var lines = await _editorTextModel.GetLineCount();
var columns = await _editorTextModel.GetLineMaxColumn(lines);
var value = await _editor.GetValue();
var result = Workspace.FormatCode(value);
return [
new TextEdit {
Range = new BlazorMonaco.Range(1, 1, lines, columns),
Text = result
}
];
}
private async Task<Hover> OnHoverAsync(string modelUri, BlazorMonaco.Position position, HoverContext context)
{
if (_editor is null || _editorTextModel is null || _editorTextModel.Uri != modelUri) return new();
var word = await _editorTextModel.GetWordAtPosition(position);
if (word is null) return new();
var info = await Workspace.GetQuickInfoCurrentFile(position.LineNumber - 1, position.Column - 1);
var contents = new List<MarkdownString>();
if (!string.IsNullOrWhiteSpace(info))
{
contents.Add(new MarkdownString { Value = info, SupportThemeIcons = false });
}
contents.Add(new MarkdownString { Value = word.Word, SupportThemeIcons = false });
return new Hover
{
Contents = [..contents],
Range = new BlazorMonaco.Range
{
StartLineNumber = position.LineNumber,
EndLineNumber = position.LineNumber,
StartColumn = word.StartColumn,
EndColumn = word.EndColumn
}
};
}
public async Task<CompletionList> CompleteItemAsync(string modelUri, BlazorMonaco.Position position, CompletionContext context)
{
var completions = new CompletionList() { Suggestions = [] };
if (_editor is null || _editorTextModel is null || _editorTextModel.Uri != modelUri)
return completions;
await _syncLock.WaitAsync();
try
{
var currentCode = await _editor.GetValue();
if (currentCode != _lastSyncedCode)
{
Workspace.WriteDocument(currentCode);
_lastSyncedCode = currentCode;
}
}
finally
{
_syncLock.Release();
}
var word = await _editorTextModel.GetWordAtPosition(position);
if (context.TriggerKind == CompletionTriggerKind.Invoke
&& string.IsNullOrEmpty(context.TriggerCharacter)
&& string.IsNullOrEmpty(word?.Word))
{
return completions;
}
char? triggerCharacter = null;
if (context.TriggerCharacter is not null && context.TriggerCharacter.Length > 0)
{
triggerCharacter = context.TriggerCharacter[0];
}
var completionItems = await Workspace.GetCompletionsCurrentFile(
position.LineNumber - 1,
position.Column - 1,
(int)(context.TriggerKind ?? 0),
triggerCharacter);
completions.Suggestions.AddRange(completionItems);
return completions;
}
private Task<CompletionItem> ResolveCompletionItemAsync(CompletionItem item)
{
return Task.FromResult(item);
}
[JSInvokable]
public async Task<SignatureHelpResult?> GetSignatureHelp(string modelUri, int line, int column)
{
if (_editor is null || _editorTextModel is null || _editorTextModel.Uri != modelUri) return null;
return await Workspace.GetSignatureHelpCurrentFile(line - 1, column - 1);
}
private async Task SaveCurrentFile()
{
if (Workspace.CurrentFile is null || !Workspace.CurrentFile.IsModified) return;
try
{
await FileManagerClient.SaveFileAsync(Workspace.CurrentFile.Path, Workspace.CurrentFile.Code);
// Update IsModified status after successful save
Workspace.CurrentFile.Saved();
}
catch (Exception)
{
// Error handling - could show snackbar notification here if needed
// For now, just let the exception propagate
throw;
}
}
private async Task DidChangeModelContent(ModelContentChangedEvent e)
{
if (_editor == null) return;
if (e.IsFlush || e.Changes.Count == 0) return;
await _syncLock.WaitAsync();
try
{
var newCode = await _editor.GetValue();
if (newCode != _lastSyncedCode)
{
// Run WriteDocument on background thread to avoid blocking UI
Workspace.WriteDocument(newCode);
_lastSyncedCode = newCode;
}
}
finally
{
_syncLock.Release();
}
}
private void OnDiagnoticChanged(IEnumerable<Microsoft.CodeAnalysis.Diagnostic> diagnostics)
{
Task.Run(() => OnDiagnoticChangedAsync(diagnostics)).GetAwaiter().GetResult();
}
private async Task OnDiagnoticChangedAsync(IEnumerable<Microsoft.CodeAnalysis.Diagnostic> diagnostics)
{
if (_editor == null) return;
var model = await _editor.GetModel();
await BlazorMonaco.Editor.Global.SetModelMarkers(jsRuntime, model, "default", diagnostics.Select(ToMonacoDiagnostic).ToList());
}
private static MarkerData ToMonacoDiagnostic(Microsoft.CodeAnalysis.Diagnostic diagnostic)
{
var lineSpan = diagnostic.Location.GetLineSpan();
return new()
{
StartLineNumber = lineSpan.StartLinePosition.Line + 1,
StartColumn = lineSpan.StartLinePosition.Character + 1,
EndLineNumber = lineSpan.EndLinePosition.Line + 1,
EndColumn = lineSpan.EndLinePosition.Character + 1,
Message = diagnostic.GetMessage(),
Severity = diagnostic.Severity switch
{
Microsoft.CodeAnalysis.DiagnosticSeverity.Info => MarkerSeverity.Info,
Microsoft.CodeAnalysis.DiagnosticSeverity.Warning => MarkerSeverity.Warning,
Microsoft.CodeAnalysis.DiagnosticSeverity.Error => MarkerSeverity.Error,
_ => MarkerSeverity.Hint,
},
};
}
public void Dispose()
{
lock (_debounceLock)
{
_debounceTimer?.Dispose();
_debounceTimer = null;
}
_syncLock?.Dispose();
Workspace.CurrentFileChanged -= OnCurrentFileChanged;
Workspace.DiagnoticChanged -= OnDiagnoticChanged;
ScriptManagerClient.StateChanged -= OnScriptManagerStateChanged;
GC.SuppressFinalize(this);
}
}