diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fb8d1517ee..29b7671d19 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -8,14 +8,10 @@ 7.1.2 7.0.0 2.0.1 - 6.2.14 - 10.0.22621.755 4.7.37 7.1.100 8.0.0-dev.65 - 1.0.1462.37 4.9.4 - 1.2.0 @@ -34,14 +30,15 @@ + - + - - + + diff --git a/src/app/dev/DevToys.Api/Core/IFontProvider.cs b/src/app/dev/DevToys.Api/Core/IFontProvider.cs new file mode 100644 index 0000000000..6ba923d9ca --- /dev/null +++ b/src/app/dev/DevToys.Api/Core/IFontProvider.cs @@ -0,0 +1,12 @@ +namespace DevToys.Api.Core; + +/// +/// Provides a platform agnostic way to retrieve information about fonts installed on the operating system. +/// +public interface IFontProvider +{ + /// + /// Retrieves the list of font families available on the operating system. + /// + string[] GetFontFamilies(); +} diff --git a/src/app/dev/DevToys.Api/Settings/PredefinedSettings.cs b/src/app/dev/DevToys.Api/Settings/PredefinedSettings.cs index 9807653b05..b791b64c4d 100644 --- a/src/app/dev/DevToys.Api/Settings/PredefinedSettings.cs +++ b/src/app/dev/DevToys.Api/Settings/PredefinedSettings.cs @@ -10,6 +10,76 @@ public static readonly SettingDefinition CompactMode name: nameof(CompactMode), defaultValue: false); + /// + /// Preferred default fonts. The app will the first font in this list that is available on the system. + /// + public static readonly string[] DefaultFonts + = new[] + { + // Popular fonts developers install. If it's on the system, users likely want to use it. + "Fira Code", + "Source Code Pro", + "DejaVu Sans Mono", + "Hack", + + // Default Visual Studio fonts. + "Cascadia Mono", + "Cascadia Code", + + // Fonts included on MacOS. + "Menlo", + "Monaco", + "SF Mono", + "SF Pro", + + // Fonts included on Windows and MacOS. + "Consolas", + "Courier New", + + // Fonts included on Windows. + "Segoe UI", + }; + + /// + /// The font family name to use in the text editor. + /// + public static readonly SettingDefinition TextEditorFont + = new( + name: nameof(TextEditorFont), + defaultValue: string.Empty); // Default value will be defined by the app depending on the operating system. + + /// + /// Whether the text in the text editor should wrap. + /// + public static readonly SettingDefinition TextEditorTextWrapping + = new( + name: nameof(TextEditorTextWrapping), + defaultValue: false); + + /// + /// Whether the line numbers should be displayed in the text editor. + /// + public static readonly SettingDefinition TextEditorLineNumbers + = new( + name: nameof(TextEditorLineNumbers), + defaultValue: true); + + /// + /// Whether the line where the caret is should be highlighted in the text editor. + /// + public static readonly SettingDefinition TextEditorHighlightCurrentLine + = new( + name: nameof(TextEditorHighlightCurrentLine), + defaultValue: true); + + /// + /// Whether white spaces should be rendered in the text editor. + /// + public static readonly SettingDefinition TextEditorRenderWhitespace + = new( + name: nameof(TextEditorRenderWhitespace), + defaultValue: false); + /// /// Whether when using the Paste command, the text in the editor should be replaced or appended. /// diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIButton.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIButton.cs index a7aa8f6dcb..42c3f4fe44 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIButton.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIButton.cs @@ -49,7 +49,7 @@ internal set public static partial class GUI { /// - /// Create component that represents a button, which reacts when clicking on it. + /// Create a component that represents a button, which reacts when clicking on it. /// public static IUIButton Button() { @@ -57,7 +57,7 @@ public static IUIButton Button() } /// - /// Create component that represents a button, which reacts when clicking on it. + /// Create a component that represents a button, which reacts when clicking on it. /// /// An optional unique identifier for this UI element. public static IUIButton Button(string? id) @@ -66,7 +66,7 @@ public static IUIButton Button(string? id) } /// - /// Create component that represents a button, which reacts when clicking on it. + /// Create a component that represents a button, which reacts when clicking on it. /// /// An optional unique identifier for this UI element. /// The text to display in the button. diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIMultilineLineTextInput.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIMultilineLineTextInput.cs index abfd3e7db5..6f22a198ba 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIMultilineLineTextInput.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUIMultilineLineTextInput.cs @@ -108,7 +108,7 @@ public static IUIMultilineLineTextInput MultilineTextInput(string? id, string pr /// /// Sets the list of spans to highlight in the text document. /// - public static IUIMultilineLineTextInput Hihglight(this IUIMultilineLineTextInput element, params TextSpan[] spans) + public static IUIMultilineLineTextInput Highlight(this IUIMultilineLineTextInput element, params TextSpan[] spans) { ((UIMultilineTextInput)element).HighlightedSpans = spans; return element; diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISetting.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISetting.cs index 231d1f59fc..12524b1235 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISetting.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISetting.cs @@ -1,6 +1,4 @@ -using System; - -namespace DevToys.Api; +namespace DevToys.Api; /// /// A component that represents a setting, with a title, description, icon and for the option value. diff --git a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISinglelineTextInput.cs b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISinglelineTextInput.cs index cf3bf6efdd..8fc2e44696 100644 --- a/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISinglelineTextInput.cs +++ b/src/app/dev/DevToys.Api/Tool/GUI/Components/IUISinglelineTextInput.cs @@ -142,7 +142,7 @@ public static IUISinglelineTextInput SinglelineTextInput(string? id) /// /// Sets the text input control as read-only. /// - public static T ReadOnly(this T element) where T : IUITitledElement + public static T ReadOnly(this T element) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -154,7 +154,7 @@ public static T ReadOnly(this T element) where T : IUITitledElement /// /// Sets the text input control as editable. /// - public static T Editable(this T element) where T : IUITitledElement + public static T Editable(this T element) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -166,7 +166,7 @@ public static T Editable(this T element) where T : IUITitledElement /// /// Shows the "copy" button when the editor is editable. /// - public static T CanCopyWhenEditable(this T element) where T : IUITitledElement + public static T CanCopyWhenEditable(this T element) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -178,7 +178,7 @@ public static T CanCopyWhenEditable(this T element) where T : IUITitledElemen /// /// Hides the "copy" button when the editor is editable. /// - public static T CannotCopyWhenEditable(this T element) where T : IUITitledElement + public static T CannotCopyWhenEditable(this T element) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -190,7 +190,7 @@ public static T CannotCopyWhenEditable(this T element) where T : IUITitledEle /// /// Sets the unformatted text of the control. /// - public static T Text(this T element, string text) where T : IUITitledElement + public static T Text(this T element, string text) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -202,7 +202,7 @@ public static T Text(this T element, string text) where T : IUITitledElement /// /// Selects the given span in the text document. /// - public static T Select(this T element, TextSpan span) where T : IUITitledElement + public static T Select(this T element, TextSpan span) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { @@ -214,7 +214,7 @@ public static T Select(this T element, TextSpan span) where T : IUITitledElem /// /// Selects the given span in the text document. /// - public static T Select(this T element, int start, int length) where T : IUITitledElement + public static T Select(this T element, int start, int length) where T : IUISinglelineTextInput { if (element is UISinglelineTextInput strongElement) { diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs index 040693757d..178f398f25 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs @@ -4,14 +4,19 @@ using DevToys.MonacoEditor.Extensions; using DevToys.MonacoEditor.Monaco; using DevToys.MonacoEditor.Monaco.Editor; +using DevToys.MonacoEditor.Monaco.Helpers; using DevToys.MonacoEditor.WebInterop; -using DevToys.UI.Framework.Threading; +using DevToys.UI.Framework.Controls; +using DevToys.UI.Framework.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; +using Uno.Extensions; using Windows.Foundation; +using Windows.UI; +using static System.Net.Mime.MediaTypeNames; using Range = DevToys.MonacoEditor.Monaco.Range; -using Microsoft.UI.Dispatching; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; namespace DevToys.MonacoEditor; @@ -24,7 +29,7 @@ namespace DevToys.MonacoEditor; [TemplateVisualState(Name = PointerOverState, GroupName = CommonStates)] [TemplateVisualState(Name = FocusedState, GroupName = CommonStates)] [TemplateVisualState(Name = DisabledState, GroupName = CommonStates)] -public sealed partial class CodeEditor : Control, IParentAccessorAcceptor, IDisposable +public sealed partial class CodeEditor : Control, IParentAccessorAcceptor, IDisposable, IMonacoEditor { internal const string CommonStates = "CommonStates"; internal const string NormalState = "Normal"; @@ -32,12 +37,22 @@ public sealed partial class CodeEditor : Control, IParentAccessorAcceptor, IDisp internal const string FocusedState = "Focused"; internal const string DisabledState = "Disabled"; - private DispatcherQueue UIAccess => this.DispatcherQueue; + private static readonly IModelDecorationOptions HighlightedSpanStyle + = new IModelDecorationOptions() + { + ClassName = new CssLineStyle() + { + BackgroundColor = Color.FromArgb(85, 234, 92, 0) // #55EA5C00 + } + }; + private readonly ILogger _logger; + private readonly ISettingsProvider _settingsProvider = Parts.SettingsProvider; private readonly DebugLogger _debugLogger = new(); - private readonly ThemeListener _themeListener; - private readonly long _themeToken; + private readonly ThemeListener _themeListener = new(); + private readonly CssStyleBroker _cssBroker; + private IReadOnlyList? _spansToHighlight; private ICodeEditorPresenter? _view; private int _focusCount; private bool _initialized; @@ -46,17 +61,19 @@ public sealed partial class CodeEditor : Control, IParentAccessorAcceptor, IDisp public CodeEditor() { + _logger = this.Log(); DefaultStyleKey = typeof(CodeEditor); ParentAccessor = new ParentAccessor(this); ParentAccessor.AddAssemblyForTypeLookup(typeof(Range).GetTypeInfo().Assembly); + ParentAccessor.AddAssemblyForTypeLookup(typeof(TextSpan).GetTypeInfo().Assembly); ParentAccessor.RegisterAction("Loaded", OnMonacoEditorLoaded); ParentAccessor.RegisterAction("GotFocus", OnMonacoEditorGotFocus); ParentAccessor.RegisterAction("LostFocus", OnMonacoEditorLostFocus); - _themeListener = new ThemeListener(UIAccess); _themeListener.ThemeChanged += ThemeListener_ThemeChanged; - _themeToken = RegisterPropertyChangedCallback(ActualThemeProperty, ActualTheme_PropertyChanged); + + _cssBroker = new CssStyleBroker(this); Options = new StandaloneEditorConstructionOptions(); DiffOptions = new DiffEditorConstructionOptions(); @@ -66,8 +83,7 @@ public CodeEditor() Options.PropertyChanged += Options_PropertyChanged; DiffOptions.PropertyChanged += DiffOptions_PropertyChanged; - - Unloaded += CodeEditor_Unloaded; + _settingsProvider.SettingChanged += SettingsProvider_SettingChanged; } public static DependencyProperty IsEditorLoadedProperty { get; } @@ -111,10 +127,17 @@ public bool IsDiffViewMode string.Empty, async (d, e) => { - if (d is CodeEditor { IsSettingValue: false } codeEditor && codeEditor._initialized) + if (d is CodeEditor codeEditor && codeEditor._initialized) { - // link:otherScriptsToBeOrganized.ts:updateContent - await codeEditor.InvokeScriptAsync("updateContent", e.NewValue.ToString() ?? string.Empty); + if (!codeEditor.IsSettingValue) + { + // link:otherScriptsToBeOrganized.ts:updateContent + await codeEditor.InvokeScriptAsync("updateContent", e.NewValue.ToString() ?? string.Empty); + } + else + { + codeEditor.TextChanged?.Invoke(d, EventArgs.Empty); + } } })); @@ -127,47 +150,6 @@ public string Text set => SetValue(TextProperty, value); } - public static DependencyProperty SelectedTextProperty { get; } - = DependencyProperty.Register( - nameof(SelectedText), - typeof(string), - typeof(CodeEditor), - new PropertyMetadata( - string.Empty, - async (d, e) => - { - if (d is CodeEditor { IsSettingValue: false } codeEditor && codeEditor._initialized) - { - // link:updateSelectedContent.ts:updateSelectedContent - await codeEditor.InvokeScriptAsync("updateSelectedContent", e.NewValue.ToString() ?? string.Empty); - } - })); - - /// - /// Gets the current Primary Selected CodeEditor Text, or replace the current selection by the given text. - /// - public string SelectedText - { - get => (string)GetValue(SelectedTextProperty); - set => SetValue(SelectedTextProperty, value); - } - - public static DependencyProperty SelectedRangeProperty { get; } - = DependencyProperty.Register( - nameof(SelectedRange), - typeof(Selection), - typeof(CodeEditor), - new PropertyMetadata(null)); - - /// - /// Gets or sets the span to select in the editor - /// - public Selection SelectedRange - { - get => (Selection)GetValue(SelectedRangeProperty); - internal set => SetValue(SelectedRangeProperty, value); - } - public static DependencyProperty ReadOnlyProperty { get; } = DependencyProperty.Register( nameof(ReadOnly), @@ -194,6 +176,38 @@ public Selection SelectedRange } })); + public static DependencyProperty SelectedSpanProperty { get; } + = DependencyProperty.Register( + nameof(SelectedSpan), + typeof(TextSpan), + typeof(CodeEditor), + new PropertyMetadata( + new TextSpan(0, 0), + async (d, e) => + { + if (d is CodeEditor codeEditor && codeEditor._initialized) + { + if (!codeEditor.IsSettingValue) + { + // link:updateSelection.ts:updateSelectedSpan + await codeEditor.InvokeScriptAsync("updateSelectedSpan", e.NewValue ?? new TextSpan(0, 0)); + } + else + { + codeEditor.SelectedSpanChanged?.Invoke(d, EventArgs.Empty); + } + } + })); + + /// + /// Gets the current selection in the editor, or replace the current selection by the given span. + /// + public TextSpan SelectedSpan + { + get => (TextSpan)GetValue(SelectedSpanProperty); + set => SetValue(SelectedSpanProperty, value); + } + /// /// Gets or sets whether the editor is read-only. /// @@ -220,6 +234,7 @@ public bool ReadOnly if (editor.Options != null && editor._initialized) { editor.Options.Language = e.NewValue.ToString(); + editor.Options.Folding = !string.IsNullOrWhiteSpace(editor.Options.Language) && !string.Equals("text", editor.Options.Language, StringComparison.OrdinalIgnoreCase); } })); @@ -348,6 +363,8 @@ public DiffEditorConstructionOptions DiffOptions public bool IsSettingValue { get; set; } + public Control UIHost => this; + internal ParentAccessor ParentAccessor { get; } /// @@ -365,6 +382,10 @@ public DiffEditorConstructionOptions DiffOptions /// public event TypedEventHandler? OpenLinkRequested; + public event EventHandler? TextChanged; + + public event EventHandler? SelectedSpanChanged; + #if HAS_UNO public new void Dispose() #else @@ -372,6 +393,52 @@ public void Dispose() #endif { ParentAccessor.Dispose(); + _cssBroker.Dispose(); + } + + public async Task HighlightSpansAsync(IReadOnlyList? spans) + { + if (!_initialized) + { + _spansToHighlight = spans; + return; + } + + var newDecorationsAdjust = new List(); + + if (spans is not null) + { + for (int i = 0; i < spans.Count; i++) + { + TextSpan span = spans[i]; + if (span.StartPosition + span.Length < Text.Length) + { + Position? startPosition = await GetModel().GetPositionAtAsync((uint)span.StartPosition); + Position? endPosition = await GetModel().GetPositionAtAsync((uint)(span.StartPosition + span.Length)); + if (startPosition is not null && endPosition is not null) + { + newDecorationsAdjust.Add( + new IModelDeltaDecoration( + new Range( + startPosition.LineNumber, + startPosition.Column, + endPosition.LineNumber, + endPosition.Column), + HighlightedSpanStyle)); + } + } + } + } + + if (_cssBroker.AssociateStyles(newDecorationsAdjust)) + { + // Update Styles First + await InvokeScriptAsync("updateStyle", _cssBroker.GetStyles()); + } + + // Send Command to Modify Decorations + // IMPORTANT: Need to cast to object here as we want this to be a single array object passed as a parameter, not a list of parameters to expand. + await InvokeScriptAsync("updateDecorations", (object)newDecorationsAdjust); } internal IModel GetModel() @@ -399,18 +466,20 @@ internal async Task SendScriptAsync( Guard.IsNotNull(_view); try { - return await _view.RunScriptAsync(script, member, file, line); + return await _view.RunScriptAsync(script, serializeResult: false, member, file, line); } catch (Exception e) { + LogInternalError(e); InternalException?.Invoke(this, e); } } else { -#if DEBUG - Debug.WriteLine("WARNING: Tried to call '" + script + "' before initialized."); -#endif + if (Debugger.IsAttached) + { + Debug.WriteLine("WARNING: Tried to call '" + script + "' before initialized."); + } } return default; @@ -466,14 +535,16 @@ internal async Task InvokeScriptAsync( } catch (Exception e) { + LogInternalError(e); InternalException?.Invoke(this, e); } } else { -#if DEBUG - Debug.WriteLine("WARNING: Tried to call '" + method + "' before initialized."); -#endif + if (Debugger.IsAttached) + { + Debug.WriteLine("WARNING: Tried to call '" + method + "' before initialized."); + } } return default; @@ -516,26 +587,6 @@ protected override void OnGotFocus(RoutedEventArgs e) GiveFocusToInnerEditor(); } - private void CodeEditor_Unloaded(object sender, RoutedEventArgs e) - { - if (_view != null) - { - _view.NavigationStarting -= WebView_NavigationStarting; - _view.DOMContentLoaded -= WebView_DOMContentLoaded; - _view.NavigationCompleted -= WebView_NavigationCompleted; - _view.NewWindowRequested -= WebView_NewWindowRequested; - _view.DotNetObjectInjectionRequested -= WebView_DotNetObjectInjectionRequested; - } - - Options.PropertyChanged -= Options_PropertyChanged; - DiffOptions.PropertyChanged -= DiffOptions_PropertyChanged; - _themeListener.ThemeChanged -= ThemeListener_ThemeChanged; - - UnregisterPropertyChangedCallback(ActualThemeProperty, _themeToken); - - _initialized = false; - } - private void Options_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (sender is not StandaloneEditorConstructionOptions options || IsDiffViewMode || !_initialized) @@ -587,29 +638,23 @@ private void DiffOptions_PropertyChanged(object? sender, PropertyChangedEventArg } } - private void ActualTheme_PropertyChanged(DependencyObject obj, DependencyProperty property) + private void ThemeListener_ThemeChanged(ThemeListener sender) { - ElementTheme theme = ActualTheme; - string themeName; - if (theme == ElementTheme.Default) - { - themeName = _themeListener.CurrentThemeName; - } - else - { - themeName = theme.ToString(); - } - - InvokeScriptAsync("setTheme", args: new string[] { _themeListener!.AccentColorHtmlHex }).Forget(); - InvokeScriptAsync("changeTheme", new string[] { themeName, _themeListener.IsHighContrast.ToString() }).Forget(); + InvokeScriptAsync( + "setTheme", + args: new string[] { sender.AccentColorHtmlHex }) + .Forget(); + InvokeScriptAsync( + "changeTheme", + args: new object[] { sender.CurrentTheme.ToString(), sender.IsHighContrast, IsFocusEngaged }) + .Forget(); } - private void ThemeListener_ThemeChanged(ThemeListener sender) + private void SettingsProvider_SettingChanged(object? sender, SettingChangedEventArgs e) { - if (RequestedTheme == ElementTheme.Default) + if (e.SettingName.Contains("TextEditor")) { - InvokeScriptAsync("setTheme", args: new string[] { sender.AccentColorHtmlHex }).Forget(); - InvokeScriptAsync("changeTheme", args: new string[] { sender.CurrentTheme.ToString(), sender.IsHighContrast.ToString() }).Forget(); + ApplySettings(); } } @@ -651,6 +696,10 @@ private void OnMonacoEditorGotFocus() _focusCount++; if (_focusCount > 0) { + InvokeScriptAsync( + "changeTheme", + args: new object[] { _themeListener.CurrentTheme.ToString(), _themeListener.IsHighContrast, true }) + .Forget(); VisualStateManager.GoToState(this, FocusedState, false); } } @@ -660,6 +709,10 @@ private void OnMonacoEditorLostFocus() _focusCount = Math.Max(0, _focusCount - 1); if (_focusCount == 0) { + InvokeScriptAsync( + "changeTheme", + args: new object[] { _themeListener.CurrentTheme.ToString(), _themeListener.IsHighContrast, false }) + .Forget(); VisualStateManager.GoToState(this, NormalState, false); } } @@ -677,19 +730,42 @@ private void OnMonacoEditorLoaded() .Forget(); InvokeScriptAsync( "changeTheme", - new string[] { _themeListener.CurrentTheme.ToString(), _themeListener.IsHighContrast.ToString() }) + new object[] { _themeListener.CurrentTheme.ToString(), _themeListener.IsHighContrast, IsFocusEngaged }) .Forget(); // Update options. _refrainFromUpdatingOptionsInternally = true; Options.SmoothScrolling = true; + Options.GlyphMargin = false; + Options.MouseWheelZoom = false; + Options.OverviewRulerBorder = false; + Options.ScrollBeyondLastLine = false; + Options.FontLigatures = false; + Options.SnippetSuggestions = SnippetSuggestions.None; + Options.CodeLens = true; + Options.QuickSuggestions = false; + Options.WordBasedSuggestions = false; Options.Minimap = new EditorMinimapOptions() { Enabled = false }; + Options.ShowFoldingControls = Show.Always; Options.ReadOnly = ReadOnly; Options.Language = CodeLanguage; + Options.Folding = !string.IsNullOrWhiteSpace(CodeLanguage) && !string.Equals("text", CodeLanguage, StringComparison.OrdinalIgnoreCase); + DiffOptions.SmoothScrolling = true; + DiffOptions.GlyphMargin = false; + DiffOptions.MouseWheelZoom = false; + DiffOptions.OverviewRulerBorder = false; + DiffOptions.ScrollBeyondLastLine = false; + DiffOptions.FontLigatures = false; + DiffOptions.SnippetSuggestions = SnippetSuggestions.None; + DiffOptions.CodeLens = true; + DiffOptions.QuickSuggestions = false; + DiffOptions.ShowFoldingControls = Show.Always; DiffOptions.OriginalEditable = !ReadOnly; DiffOptions.ReadOnly = ReadOnly; + ApplySettings(); + _refrainFromUpdatingOptionsInternally = false; if (IsDiffViewMode) { @@ -700,11 +776,32 @@ private void OnMonacoEditorLoaded() InvokeScriptAsync("updateOptions", Options).Forget(); } + // Set text, selection, highlighted spans that may have been set but not sent to the editor yet. + InvokeScriptAsync("updateContent", Text ?? string.Empty).Forget(); + InvokeScriptAsync("updateSelectedSpan", SelectedSpan).Forget(); + HighlightSpansAsync(_spansToHighlight).Forget(); + // We're done loading Monaco Editor. IsEditorLoaded = true; EditorLoaded?.Invoke(this, new RoutedEventArgs()); } + private void ApplySettings() + { + Options.WordWrapMinified = _settingsProvider.GetSetting(PredefinedSettings.TextEditorTextWrapping); + Options.WordWrap = _settingsProvider.GetSetting(PredefinedSettings.TextEditorTextWrapping) ? WordWrap.On : WordWrap.Off; + Options.LineNumbers = _settingsProvider.GetSetting(PredefinedSettings.TextEditorLineNumbers) ? LineNumbersType.On : LineNumbersType.Off; + Options.RenderLineHighlight = _settingsProvider.GetSetting(PredefinedSettings.TextEditorHighlightCurrentLine) ? RenderLineHighlight.All : RenderLineHighlight.None; + Options.RenderWhitespace = _settingsProvider.GetSetting(PredefinedSettings.TextEditorRenderWhitespace) ? RenderWhitespace.All : RenderWhitespace.None; + Options.FontFamily = _settingsProvider.GetSetting(PredefinedSettings.TextEditorFont); + DiffOptions.WordWrapMinified = _settingsProvider.GetSetting(PredefinedSettings.TextEditorTextWrapping); + DiffOptions.WordWrap = _settingsProvider.GetSetting(PredefinedSettings.TextEditorTextWrapping) ? WordWrap.On : WordWrap.Off; + DiffOptions.LineNumbers = _settingsProvider.GetSetting(PredefinedSettings.TextEditorLineNumbers) ? LineNumbersType.On : LineNumbersType.Off; + DiffOptions.RenderLineHighlight = _settingsProvider.GetSetting(PredefinedSettings.TextEditorHighlightCurrentLine) ? RenderLineHighlight.All : RenderLineHighlight.None; + DiffOptions.RenderWhitespace = _settingsProvider.GetSetting(PredefinedSettings.TextEditorRenderWhitespace) ? RenderWhitespace.All : RenderWhitespace.None; + DiffOptions.FontFamily = _settingsProvider.GetSetting(PredefinedSettings.TextEditorFont); + } + private void GiveFocusToInnerEditor() { if (_initialized) @@ -715,4 +812,7 @@ private void GiveFocusToInnerEditor() SendScriptAsync("editorContext.editor.focus();").Forget(); } } + + [LoggerMessage(2, LogLevel.Error, "An error occured related to the Monaco Editor.")] + partial void LogInternalError(Exception ex); } diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs index 96c5d51f66..7e8390d232 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs @@ -168,7 +168,7 @@ public async Task InvokeScriptAsync(string script) }} }})()"; - _debugLogger?.Debug($"Invoke Script: {script}"); + _debugLogger?.Debug($"Invoking script"); try { diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs index 4bce657f36..bad366f38b 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs @@ -13,6 +13,7 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using System.Runtime.CompilerServices; +using Microsoft.UI.Xaml.Media; namespace DevToys.MonacoEditor; @@ -57,8 +58,9 @@ public CodeEditorPresenter() { _logger = this.Log(); - // make the WebView2 transparent. - Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00FFFFFF"); + // Fill the WebView2 with ControlFillColorInputActive. + var controlFillColorInputActive = (Windows.UI.Color)Application.Current.Resources["ControlFillColorInputActive"]; + Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", $"{controlFillColorInputActive.R:X2}{controlFillColorInputActive.G:X2}{controlFillColorInputActive.B:X2}"); Content = _webView; @@ -92,6 +94,18 @@ public async Task LaunchAsync() { await _webView.EnsureCoreWebView2Async(); + _webView.CoreWebView2.Settings.IsZoomControlEnabled = false; + _webView.CoreWebView2.Settings.IsPinchZoomEnabled = false; + _webView.CoreWebView2.Settings.IsSwipeNavigationEnabled = false; + _webView.CoreWebView2.Settings.IsStatusBarEnabled = false; + _webView.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false; + _webView.CoreWebView2.Settings.IsGeneralAutofillEnabled = false; + _webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; +#if !DEBUG + _webView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false; + _webView.CoreWebView2.Settings.AreDevToolsEnabled = false; +#endif + string path = Path.Combine(AppContext.BaseDirectory, "DevToys.MonacoEditor", "CodeEditor", "CodeEditor.Windows.html"); if (!File.Exists(path)) { @@ -359,7 +373,7 @@ public async Task InvokeScriptAsync(string script) }} }})();"; - LogInvokingJavaScript(script); + LogInvokingJavaScript(); try { @@ -415,8 +429,8 @@ private void WebView_CoreWebView2Initialized(WebView2 sender, CoreWebView2Initia [LoggerMessage(7, LogLevel.Error, "{caller}-CALLBACK: Exception in {name}.{propertyName}.")] partial void LogDotNetObjectInjectionCallbackPropertyFailed(string name, string propertyName, Exception exception, [CallerMemberName] string? caller = null); - [LoggerMessage(8, LogLevel.Debug, "{caller}: Invoking JavaScript: {script}")] - partial void LogInvokingJavaScript(string script, [CallerMemberName] string? caller = null); + [LoggerMessage(8, LogLevel.Debug, "{caller}: Invoking JavaScript...")] + partial void LogInvokingJavaScript([CallerMemberName] string? caller = null); [LoggerMessage(9, LogLevel.Debug, "{caller}: JavaScript invoked successfully: {result}")] partial void LogInvokedJavaScriptSuccessfully(string result, [CallerMemberName] string? caller = null); diff --git a/src/app/dev/DevToys.MonacoEditor/Extensions/ICodeEditorPresenterExtensions.cs b/src/app/dev/DevToys.MonacoEditor/Extensions/ICodeEditorPresenterExtensions.cs index de7e5da043..1617efd93e 100644 --- a/src/app/dev/DevToys.MonacoEditor/Extensions/ICodeEditorPresenterExtensions.cs +++ b/src/app/dev/DevToys.MonacoEditor/Extensions/ICodeEditorPresenterExtensions.cs @@ -14,18 +14,19 @@ public static async Task RunScriptAsync( [CallerFilePath] string? file = null, [CallerLineNumber] int line = 0) { - await view.RunScriptAsync(script, member, file, line); + await view.RunScriptAsync(script, serializeResult: false, member, file, line); } public static async Task RunScriptAsync( this ICodeEditorPresenter view, string script, + bool serializeResult = true, [CallerMemberName] string? member = null, [CallerFilePath] string? file = null, [CallerLineNumber] int line = 0) { string start = "try {\n"; - if (typeof(T) != typeof(object)) + if (typeof(T) != typeof(object) && serializeResult) { script = script.Trim(';'); start += "return JSON.stringify(" + script + ");"; @@ -51,14 +52,6 @@ public static async Task RunScriptAsync( { string returnstring = await view.InvokeScriptAsync(script); - //if (JsonObject.TryParse(returnstring, out JsonObject result)) - //{ - // if (result.ContainsKey("wv_internal_error") && result["wv_internal_error"].ValueType == JsonValueType.Boolean && result["wv_internal_error"].GetBoolean()) - // { - // throw new JavaScriptInnerException(result["message"].GetString(), result["stack"].GetString()); - // } - //} - // TODO: Need to decode the error correctly if (!string.IsNullOrEmpty(returnstring)) { @@ -131,7 +124,11 @@ public static async Task InvokeScriptAsync( try { - Debug.WriteLine($"Begin invoke script (serialize - {serialize})"); + if (Debugger.IsAttached) + { + Debug.WriteLine($"Begin invoke script (serialize - {serialize})"); + } + if (serialize) { sanitizedargs @@ -161,13 +158,14 @@ public static async Task InvokeScriptAsync( string script = method + "(editorContext, " + string.Join(", ", sanitizedargs) + ");"; - Debug.WriteLine($"Script {script})"); - - return await RunScriptAsync(view, script, member, file, line); + return await RunScriptAsync(view, script, serialize, member, file, line); } catch (Exception ex) { - Debug.WriteLine($"Error {ex.Message} {ex.StackTrace} {ex.InnerException?.Message})"); + if (Debugger.IsAttached) + { + Debug.WriteLine($"Error {ex.Message} {ex.StackTrace} {ex.InnerException?.Message})"); + } return default; } } diff --git a/src/app/dev/DevToys.MonacoEditor/Monaco/Helpers/CssStyleBroker.cs b/src/app/dev/DevToys.MonacoEditor/Monaco/Helpers/CssStyleBroker.cs index ffb6cb2837..a7c98c6200 100644 --- a/src/app/dev/DevToys.MonacoEditor/Monaco/Helpers/CssStyleBroker.cs +++ b/src/app/dev/DevToys.MonacoEditor/Monaco/Helpers/CssStyleBroker.cs @@ -44,15 +44,17 @@ public static uint Register(ICssStyle style) return id; } - public bool AssociateStyles(IModelDeltaDecoration[] decorations) + public bool AssociateStyles(IReadOnlyList decorations) { /// By construction we assume that decorations will not be null from the call in bool newStyle = isDirty[_parent]; /// Can be set in . isDirty[_parent] = false; // Reset - foreach (IModelDeltaDecoration decoration in decorations) + for (int i = 0; i < decorations.Count; i++) { + IModelDeltaDecoration decoration = decorations[i]; + // Add (or ignore) elements to the collection. // If any Adds are new, we flag our boolean to return if (decoration.Options.ClassName != null) diff --git a/src/app/dev/DevToys.MonacoEditor/MonacoEditorFactory.cs b/src/app/dev/DevToys.MonacoEditor/MonacoEditorFactory.cs new file mode 100644 index 0000000000..47c3bb132b --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/MonacoEditorFactory.cs @@ -0,0 +1,12 @@ +using DevToys.UI.Framework.Controls; + +namespace DevToys.MonacoEditor; + +[Export(typeof(IMonacoEditorFactory))] +internal sealed class MonacoEditorFactory : IMonacoEditorFactory +{ + public IMonacoEditor CreateMonacoEditorInstance() + { + return new CodeEditor(); + } +} diff --git a/src/app/dev/DevToys.MonacoEditor/Themes/generic.xaml b/src/app/dev/DevToys.MonacoEditor/Themes/generic.xaml index 476b7ac955..d5f73a99bf 100644 --- a/src/app/dev/DevToys.MonacoEditor/Themes/generic.xaml +++ b/src/app/dev/DevToys.MonacoEditor/Themes/generic.xaml @@ -5,6 +5,26 @@ xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:converters="using:DevToys.UI.Framework.Converters"> + + + + + + + + + + + + + + + + + + + +