@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
@(Workspace.CurrentFile?.Name ?? "No file selected")
@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() { "." }; 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("robotnet.monaco.CSharpLanguageRegisterSignatureHelpProvider", dotNetHelper, nameof(GetSignatureHelp)); } private async Task 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 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(); 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 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 ResolveCompletionItemAsync(CompletionItem item) { return Task.FromResult(item); } [JSInvokable] public async Task 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 diagnostics) { Task.Run(() => OnDiagnoticChangedAsync(diagnostics)).GetAwaiter().GetResult(); } private async Task OnDiagnoticChangedAsync(IEnumerable 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); } }