diff --git a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs index c11e707..b38b730 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/CodeEditorPanel.cs @@ -15,202 +15,233 @@ namespace SharpIDE.Godot.Features.CodeEditor; public partial class CodeEditorPanel : MarginContainer { - [Export] - public Texture2D CsFileTexture { get; set; } = null!; - public SharpIdeSolutionModel Solution { get; set; } = null!; - private PackedScene _sharpIdeCodeEditScene = GD.Load("res://Features/CodeEditor/SharpIdeCodeEdit.tscn"); - private TabContainer _tabContainer = null!; - private ConcurrentDictionary _debuggerExecutionStopInfoByProject = []; - - [Inject] private readonly RunService _runService = null!; - public override void _Ready() - { - _tabContainer = GetNode("TabContainer"); - _tabContainer.RemoveChildAndQueueFree(_tabContainer.GetChild(0)); // Remove the default tab - _tabContainer.TabClicked += OnTabClicked; - var tabBar = _tabContainer.GetTabBar(); - tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; - tabBar.TabClosePressed += OnTabClosePressed; + [Export] + public Texture2D CsFileTexture { get; set; } = null!; + public SharpIdeSolutionModel Solution { get; set; } = null!; + private PackedScene _sharpIdeCodeEditScene = GD.Load("res://Features/CodeEditor/SharpIdeCodeEdit.tscn"); + private TabContainer _tabContainer = null!; + private ConcurrentDictionary _debuggerExecutionStopInfoByProject = []; + + [Inject] private readonly RunService _runService = null!; + public override void _Ready() + { + _tabContainer = GetNode("TabContainer"); + _tabContainer.RemoveChildAndQueueFree(_tabContainer.GetChild(0)); // Remove the default tab + _tabContainer.TabClicked += OnTabClicked; + var tabBar = _tabContainer.GetTabBar(); + tabBar.TabCloseDisplayPolicy = TabBar.CloseButtonDisplayPolicy.ShowAlways; + tabBar.TabClosePressed += OnTabClosePressed; GlobalEvents.Instance.DebuggerExecutionStopped.Subscribe(OnDebuggerExecutionStopped); GlobalEvents.Instance.ProjectStoppedDebugging.Subscribe(OnProjectStoppedDebugging); - } + } - public override void _ExitTree() - { - var selectedTabIndex = _tabContainer.CurrentTab; - var thisSolution = Singletons.AppState.RecentSlns.Single(s => s.FilePath == Solution.FilePath); - thisSolution.IdeSolutionState.OpenTabs = _tabContainer.GetChildren().OfType() - .Select((t, index) => new OpenTab - { - FilePath = t.SharpIdeFile.Path, - CaretLine = t.GetCaretLine(), - CaretColumn = t.GetCaretColumn(), - IsSelected = index == selectedTabIndex - }) - .ToList(); - } + public override void _GuiInput(InputEvent @event) + { + if (Input.IsActionPressed(InputStringNames.EditorFontSizeIncrease)) + { + AdjustCodeEditorUiScale(true); + } + else if (Input.IsActionPressed(InputStringNames.EditorFontSizeDecrease)) + { + AdjustCodeEditorUiScale(false); + } + } - public override void _UnhandledKeyInput(InputEvent @event) - { - if (@event.IsActionPressed(InputStringNames.StepOver)) - { - SendDebuggerStepCommand(DebuggerStepAction.StepOver); - } - else if (@event.IsActionPressed(InputStringNames.DebuggerStepOut)) - { - SendDebuggerStepCommand(DebuggerStepAction.StepOut); - } - else if (@event.IsActionPressed(InputStringNames.DebuggerStepIn)) - { - SendDebuggerStepCommand(DebuggerStepAction.StepIn); - } - else if (@event.IsActionPressed(InputStringNames.DebuggerContinue)) - { - SendDebuggerStepCommand(DebuggerStepAction.Continue); - } - } + private void AdjustCodeEditorUiScale(bool increase) + { + const int minFontSize = 8; + const int maxFontSize = 72; - private void OnTabClicked(long tab) - { - var sharpIdeCodeEdit = _tabContainer.GetChild((int)tab); - var sharpIdeFile = sharpIdeCodeEdit.SharpIdeFile; - var caretLinePosition = new SharpIdeFileLinePosition(sharpIdeCodeEdit.GetCaretLine(), sharpIdeCodeEdit.GetCaretColumn()); - GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(sharpIdeFile, caretLinePosition); - } + var editors = _tabContainer.GetChildren().OfType().ToList(); + if (editors.Count is 0) return; - private void OnTabClosePressed(long tabIndex) - { - var tab = _tabContainer.GetChild((int)tabIndex); - var previousSibling = _tabContainer.GetChildOrNull((int)tabIndex - 1); - if (previousSibling is not null) - { - var sharpIdeFile = previousSibling.SharpIdeFile; - var caretLinePosition = new SharpIdeFileLinePosition(previousSibling.GetCaretLine(), previousSibling.GetCaretColumn()); - // This isn't actually necessary - closing a tab automatically selects the previous tab, however we need to do it to select the file in sln explorer, record navigation event etc - GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(sharpIdeFile, caretLinePosition); - } - _tabContainer.RemoveChild(tab); - tab.QueueFree(); - } + var currentFontSize = editors.First().GetThemeFontSize(ThemeStringNames.FontSize); + var newFontSize = increase + ? Mathf.Clamp(currentFontSize + 2, minFontSize, maxFontSize) + : Mathf.Clamp(currentFontSize - 2, minFontSize, maxFontSize); - public async Task SetSharpIdeFile(SharpIdeFile file, SharpIdeFileLinePosition? fileLinePosition) - { + foreach (var editor in editors) + { + editor.AddThemeFontSizeOverride(ThemeStringNames.FontSize, newFontSize); + } + } + + public override void _ExitTree() + { + var selectedTabIndex = _tabContainer.CurrentTab; + var thisSolution = Singletons.AppState.RecentSlns.Single(s => s.FilePath == Solution.FilePath); + thisSolution.IdeSolutionState.OpenTabs = _tabContainer.GetChildren().OfType() + .Select((t, index) => new OpenTab + { + FilePath = t.SharpIdeFile.Path, + CaretLine = t.GetCaretLine(), + CaretColumn = t.GetCaretColumn(), + IsSelected = index == selectedTabIndex + }) + .ToList(); + } + + public override void _UnhandledKeyInput(InputEvent @event) + { + if (@event.IsActionPressed(InputStringNames.StepOver)) + { + SendDebuggerStepCommand(DebuggerStepAction.StepOver); + } + else if (@event.IsActionPressed(InputStringNames.DebuggerStepOut)) + { + SendDebuggerStepCommand(DebuggerStepAction.StepOut); + } + else if (@event.IsActionPressed(InputStringNames.DebuggerStepIn)) + { + SendDebuggerStepCommand(DebuggerStepAction.StepIn); + } + else if (@event.IsActionPressed(InputStringNames.DebuggerContinue)) + { + SendDebuggerStepCommand(DebuggerStepAction.Continue); + } + } + + private void OnTabClicked(long tab) + { + var sharpIdeCodeEdit = _tabContainer.GetChild((int)tab); + var sharpIdeFile = sharpIdeCodeEdit.SharpIdeFile; + var caretLinePosition = new SharpIdeFileLinePosition(sharpIdeCodeEdit.GetCaretLine(), sharpIdeCodeEdit.GetCaretColumn()); + GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(sharpIdeFile, caretLinePosition); + } + + private void OnTabClosePressed(long tabIndex) + { + var tab = _tabContainer.GetChild((int)tabIndex); + var previousSibling = _tabContainer.GetChildOrNull((int)tabIndex - 1); + if (previousSibling is not null) + { + var sharpIdeFile = previousSibling.SharpIdeFile; + var caretLinePosition = new SharpIdeFileLinePosition(previousSibling.GetCaretLine(), previousSibling.GetCaretColumn()); + // This isn't actually necessary - closing a tab automatically selects the previous tab, however we need to do it to select the file in sln explorer, record navigation event etc + GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(sharpIdeFile, caretLinePosition); + } + _tabContainer.RemoveChild(tab); + tab.QueueFree(); + } + + public async Task SetSharpIdeFile(SharpIdeFile file, SharpIdeFileLinePosition? fileLinePosition) + { await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); - var existingTab = await this.InvokeAsync(() => _tabContainer.GetChildren().OfType().FirstOrDefault(t => t.SharpIdeFile == file)); - if (existingTab is not null) - { - var existingTabIndex = existingTab.GetIndex(); - await this.InvokeAsync(() => - { - _tabContainer.CurrentTab = existingTabIndex; - if (fileLinePosition is not null) existingTab.SetFileLinePosition(fileLinePosition.Value); - }); - return; - } - var newTab = _sharpIdeCodeEditScene.Instantiate(); - newTab.Solution = Solution; - await this.InvokeAsync(() => - { - _tabContainer.AddChild(newTab); - var newTabIndex = _tabContainer.GetTabCount() - 1; - _tabContainer.SetIconsForFileExtension(file, newTabIndex); - _tabContainer.SetTabTitle(newTabIndex, file.Name); - _tabContainer.SetTabTooltip(newTabIndex, file.Path); - _tabContainer.CurrentTab = newTabIndex; - - file.IsDirty.Skip(1).SubscribeOnThreadPool().ObserveOnThreadPool().SubscribeAwait(async (isDirty, ct) => - { - //GD.Print($"File dirty state changed: {file.Path} is now {(isDirty ? "dirty" : "clean")}"); - await this.InvokeAsync(() => - { - var tabIndex = newTab.GetIndex(); - var title = file.Name + (isDirty ? " (*)" : ""); - _tabContainer.SetTabTitle(tabIndex, title); - }); - }).AddTo(newTab); // needs to be on ui thread - }); - - await newTab.SetSharpIdeFile(file, fileLinePosition); - } - - private static readonly Color ExecutingLineColor = new Color("665001"); - private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) - { - Guard.Against.Null(Solution, nameof(Solution)); - - var currentSharpIdeFile = await this.InvokeAsync(() => _tabContainer.GetChild(_tabContainer.CurrentTab).SharpIdeFile); - - if (executionStopInfo.FilePath != currentSharpIdeFile?.Path) - { - var file = Solution.AllFiles[executionStopInfo.FilePath]; - await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(file, null).ConfigureAwait(false); - } - var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed - Guard.Against.Negative(lineInt, nameof(lineInt)); - if (_debuggerExecutionStopInfoByProject.TryGetValue(executionStopInfo.Project, out _)) throw new InvalidOperationException("Debugger is already stopped for this project."); - _debuggerExecutionStopInfoByProject[executionStopInfo.Project] = executionStopInfo; - - await this.InvokeAsync(() => - { - var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); - tabForStopInfo.SetLineBackgroundColor(lineInt, ExecutingLineColor); - tabForStopInfo.SetLineAsExecuting(lineInt, true); - }); - } - - private enum DebuggerStepAction { StepOver, StepIn, StepOut, Continue } - [RequiresGodotUiThread] - private void SendDebuggerStepCommand(DebuggerStepAction debuggerStepAction) - { - // TODO: Debugging needs a rework - debugging commands should be scoped to a debug session, ie the debug panel sub-tabs - // For now, just use the first project that is currently stopped - var stoppedProjects = _debuggerExecutionStopInfoByProject.Keys.ToList(); - if (stoppedProjects.Count == 0) return; // ie not currently stopped anywhere - var project = stoppedProjects[0]; - if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; - var godotLine = executionStopInfo.Line - 1; - var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); - tabForStopInfo.SetLineAsExecuting(godotLine, false); - tabForStopInfo.SetLineColour(godotLine); - var threadId = executionStopInfo.ThreadId; - _ = Task.GodotRun(async () => - { - var task = debuggerStepAction switch - { - DebuggerStepAction.StepOver => _runService.SendDebuggerStepOver(threadId), - DebuggerStepAction.StepIn => _runService.SendDebuggerStepInto(threadId), - DebuggerStepAction.StepOut => _runService.SendDebuggerStepOut(threadId), - DebuggerStepAction.Continue => _runService.SendDebuggerContinue(threadId), - _ => throw new ArgumentOutOfRangeException(nameof(debuggerStepAction), debuggerStepAction, null) - }; - await task; - }); - } - - private async Task OnProjectStoppedDebugging(SharpIdeProjectModel project) - { - if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; - await this.InvokeAsync(() => - { - var godotLine = executionStopInfo.Line - 1; - var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); - tabForStopInfo.SetLineAsExecuting(godotLine, false); - tabForStopInfo.SetLineColour(godotLine); - }); - } + var existingTab = await this.InvokeAsync(() => _tabContainer.GetChildren().OfType().FirstOrDefault(t => t.SharpIdeFile == file)); + if (existingTab is not null) + { + var existingTabIndex = existingTab.GetIndex(); + await this.InvokeAsync(() => + { + _tabContainer.CurrentTab = existingTabIndex; + if (fileLinePosition is not null) existingTab.SetFileLinePosition(fileLinePosition.Value); + }); + return; + } + var newTab = _sharpIdeCodeEditScene.Instantiate(); + newTab.Solution = Solution; + await this.InvokeAsync(() => + { + _tabContainer.AddChild(newTab); + var newTabIndex = _tabContainer.GetTabCount() - 1; + _tabContainer.SetIconsForFileExtension(file, newTabIndex); + _tabContainer.SetTabTitle(newTabIndex, file.Name); + _tabContainer.SetTabTooltip(newTabIndex, file.Path); + _tabContainer.CurrentTab = newTabIndex; + + file.IsDirty.Skip(1).SubscribeOnThreadPool().ObserveOnThreadPool().SubscribeAwait(async (isDirty, ct) => + { + //GD.Print($"File dirty state changed: {file.Path} is now {(isDirty ? "dirty" : "clean")}"); + await this.InvokeAsync(() => + { + var tabIndex = newTab.GetIndex(); + var title = file.Name + (isDirty ? " (*)" : ""); + _tabContainer.SetTabTitle(tabIndex, title); + }); + }).AddTo(newTab); // needs to be on ui thread + }); + + await newTab.SetSharpIdeFile(file, fileLinePosition); + } + + private static readonly Color ExecutingLineColor = new Color("665001"); + private async Task OnDebuggerExecutionStopped(ExecutionStopInfo executionStopInfo) + { + Guard.Against.Null(Solution, nameof(Solution)); + + var currentSharpIdeFile = await this.InvokeAsync(() => _tabContainer.GetChild(_tabContainer.CurrentTab).SharpIdeFile); + + if (executionStopInfo.FilePath != currentSharpIdeFile?.Path) + { + var file = Solution.AllFiles[executionStopInfo.FilePath]; + await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(file, null).ConfigureAwait(false); + } + var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed + Guard.Against.Negative(lineInt, nameof(lineInt)); + if (_debuggerExecutionStopInfoByProject.TryGetValue(executionStopInfo.Project, out _)) throw new InvalidOperationException("Debugger is already stopped for this project."); + _debuggerExecutionStopInfoByProject[executionStopInfo.Project] = executionStopInfo; + + await this.InvokeAsync(() => + { + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineBackgroundColor(lineInt, ExecutingLineColor); + tabForStopInfo.SetLineAsExecuting(lineInt, true); + }); + } + + private enum DebuggerStepAction { StepOver, StepIn, StepOut, Continue } + [RequiresGodotUiThread] + private void SendDebuggerStepCommand(DebuggerStepAction debuggerStepAction) + { + // TODO: Debugging needs a rework - debugging commands should be scoped to a debug session, ie the debug panel sub-tabs + // For now, just use the first project that is currently stopped + var stoppedProjects = _debuggerExecutionStopInfoByProject.Keys.ToList(); + if (stoppedProjects.Count == 0) return; // ie not currently stopped anywhere + var project = stoppedProjects[0]; + if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; + var godotLine = executionStopInfo.Line - 1; + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineAsExecuting(godotLine, false); + tabForStopInfo.SetLineColour(godotLine); + var threadId = executionStopInfo.ThreadId; + _ = Task.GodotRun(async () => + { + var task = debuggerStepAction switch + { + DebuggerStepAction.StepOver => _runService.SendDebuggerStepOver(threadId), + DebuggerStepAction.StepIn => _runService.SendDebuggerStepInto(threadId), + DebuggerStepAction.StepOut => _runService.SendDebuggerStepOut(threadId), + DebuggerStepAction.Continue => _runService.SendDebuggerContinue(threadId), + _ => throw new ArgumentOutOfRangeException(nameof(debuggerStepAction), debuggerStepAction, null) + }; + await task; + }); + } + + private async Task OnProjectStoppedDebugging(SharpIdeProjectModel project) + { + if (!_debuggerExecutionStopInfoByProject.TryRemove(project, out var executionStopInfo)) return; + await this.InvokeAsync(() => + { + var godotLine = executionStopInfo.Line - 1; + var tabForStopInfo = _tabContainer.GetChildren().OfType().Single(t => t.SharpIdeFile.Path == executionStopInfo.FilePath); + tabForStopInfo.SetLineAsExecuting(godotLine, false); + tabForStopInfo.SetLineColour(godotLine); + }); + } } file static class TabContainerExtensions { - extension(TabContainer tabContainer) - { - public void SetIconsForFileExtension(SharpIdeFile file, int newTabIndex) - { - var (icon, overlayIcon) = FileIconHelper.GetIconForFileExtension(file.Extension); - tabContainer.SetTabIcon(newTabIndex, icon); - - // Unfortunately TabContainer doesn't have a SetTabIconOverlay method - //tabContainer.SetIconOverlay(0, overlayIcon); - } - } + extension(TabContainer tabContainer) + { + public void SetIconsForFileExtension(SharpIdeFile file, int newTabIndex) + { + var (icon, overlayIcon) = FileIconHelper.GetIconForFileExtension(file.Extension); + tabContainer.SetTabIcon(newTabIndex, icon); + + // Unfortunately TabContainer doesn't have a SetTabIconOverlay method + //tabContainer.SetIconOverlay(0, overlayIcon); + } + } } \ No newline at end of file diff --git a/src/SharpIDE.Godot/InputStringNames.cs b/src/SharpIDE.Godot/InputStringNames.cs index 1693453..b8674e6 100644 --- a/src/SharpIDE.Godot/InputStringNames.cs +++ b/src/SharpIDE.Godot/InputStringNames.cs @@ -14,6 +14,8 @@ public static class InputStringNames public static readonly StringName FindFiles = nameof(FindFiles); public static readonly StringName SaveFile = nameof(SaveFile); public static readonly StringName SaveAllFiles = nameof(SaveAllFiles); + public static readonly StringName EditorFontSizeIncrease = nameof(EditorFontSizeIncrease); + public static readonly StringName EditorFontSizeDecrease = nameof(EditorFontSizeDecrease); } public static class ThemeStringNames diff --git a/src/SharpIDE.Godot/project.godot b/src/SharpIDE.Godot/project.godot index 5379150..06c04bb 100644 --- a/src/SharpIDE.Godot/project.godot +++ b/src/SharpIDE.Godot/project.godot @@ -88,6 +88,16 @@ DebuggerStepOut={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194342,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +EditorFontSizeIncrease={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"button_mask":8,"position":Vector2(96, 15),"global_position":Vector2(105, 63),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null) +] +} +EditorFontSizeDecrease={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"button_mask":16,"position":Vector2(120, 12),"global_position":Vector2(129, 60),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null) +] +} [rendering]