From a66537a9629b095bbef5d60d6e638a5db6057131 Mon Sep 17 00:00:00 2001 From: Etienne BAUDOUX Date: Mon, 19 Dec 2022 15:44:03 -0800 Subject: [PATCH] Added a simple form with Monaco Editor. No way to communicate with the editor yet. --- .gitignore | 8 + init.ps1 | 8 + init.sh | 10 + src/DevToys.sln | 19 ++ src/Directory.Packages.props | 9 +- .../CodeEditor/CodeEditor.Wasm.ts | 113 +++++++++++ .../CodeEditor/CodeEditor.Windows.html | 93 +++++++++ .../CodeEditor/CodeEditor.cs | 46 +++++ .../CodeEditor/CodeEditorPresenter.Wasm.cs | 80 ++++++++ .../CodeEditor/CodeEditorPresenter.Windows.cs | 63 ++++++ .../CodeEditor/ICodeEditorPresenter.cs | 32 +++ .../DevToys.MonacoEditor.csproj | 81 ++++++++ .../DevToys.MonacoEditor/Themes/Generic.xaml | 28 +++ .../Monaco.Helpers.DebugLogger.ts | 5 + .../Monaco.Helpers.KeyboardListener.ts | 5 + .../otherScriptsToBeOrganized.ts | 134 +++++++++++++ .../registerCodeLensProvider.ts | 25 +++ .../ts-helpermethods/registerColorProvider.ts | 21 ++ .../registerCompletionItemProvider.ts | 23 +++ .../Monaco.Helpers.ParentAccessor.ts | 15 ++ .../Monaco.Helpers.ThemeAccessor.ts | 6 + .../asyncCallbackHelpers.ts | 130 ++++++++++++ .../ts-helpermethods-Wasm/tsconfig.json | 17 ++ .../Monaco.Helpers.ParentAccessor.ts | 14 ++ .../asyncCallbackHelpers.ts | 14 ++ .../ts-helpermethods/tsconfig.json | 7 + .../ts-helpermethods/updateSelectedContent.ts | 48 +++++ src/app/dev/DevToys.UI/App.xaml.cs | 43 ++-- .../Core/Threading/TaskExtension.cs | 54 +++++ .../DevToys.UI/Core/Threading/ThreadHelper.cs | 188 ++++++++++++++++++ src/app/dev/DevToys.UI/DevToys.UI.projitems | 8 +- src/app/dev/DevToys.UI/DevToys.UI.shproj | 26 ++- src/app/dev/DevToys.UI/Views/MainPage.xaml | 22 +- .../DevToys.Wasm/DevToys.Wasm.csproj | 19 +- .../DevToys.Windows/DevToys.Windows.csproj | 6 +- src/app/dev/shared/SharedAssemblyInfo.cs | 4 +- tools/Restore-MonacoEditor.ps1 | 50 +++++ tools/Restore-MonacoEditor.sh | 46 +++++ 38 files changed, 1466 insertions(+), 54 deletions(-) create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj create mode 100644 src/app/dev/DevToys.MonacoEditor/Themes/Generic.xaml create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.DebugLogger.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.KeyboardListener.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ParentAccessor.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/asyncCallbackHelpers.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/tsconfig.json create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/Monaco.Helpers.ParentAccessor.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/asyncCallbackHelpers.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts create mode 100644 src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs create mode 100644 src/app/dev/DevToys.UI/Core/Threading/ThreadHelper.cs create mode 100644 tools/Restore-MonacoEditor.ps1 create mode 100644 tools/Restore-MonacoEditor.sh diff --git a/.gitignore b/.gitignore index c4128ad12d..c0ee4e1d37 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,14 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# TypeScript ignores +*.js +*.js.map +package-lock.json + +# Monaco Dependency (Download from https://microsoft.github.io/monaco-editor/) +src/app/dev/DevToys.MonacoEditor/monaco-editor + # Nuke Build .nuke/temp/ diff --git a/init.ps1 b/init.ps1 index e45c77a065..c828873147 100644 --- a/init.ps1 +++ b/init.ps1 @@ -16,5 +16,13 @@ Get-ChildItem $PSScriptRoot\src\ -rec |? { $_.FullName.EndsWith('.sln') } |% { Write-Host "Restoring packages for $($SolutionPath)..." ExecSafe { & $env:DOTNET_EXE restore -v:quiet $SolutionPath } } + +Write-Host "Done." +Write-Output "---------------------------------------" + +# Restore Monaco Editor +Write-Host "Restoring Monaco Editor" +ExecSafe { & "$PSScriptRoot\tools\Restore-MonacoEditor.ps1" -RootFolder $PSScriptRoot } + Write-Host "Done." Write-Output "---------------------------------------" \ No newline at end of file diff --git a/init.sh b/init.sh index cb43813272..822ab6e4ff 100644 --- a/init.sh +++ b/init.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +bash --version 2>&1 | head -n 1 + set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) @@ -14,5 +16,13 @@ do echo "Restoring packages for $SOLUTION_FILE..." "$DOTNET_EXE" restore -v:quiet $SOLUTION_FILE done + +echo "Done." +echo "---------------------------------------" + +# Restore Monaco Editor +echo "Restoring Monaco Editor" +. "./tools/Restore-MonacoEditor.sh" $SCRIPT_DIR + echo "Done." echo "---------------------------------------" diff --git a/src/DevToys.sln b/src/DevToys.sln index c26303e76c..376dc5da8a 100644 --- a/src/DevToys.sln +++ b/src/DevToys.sln @@ -64,6 +64,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevToys.CLI", "app\dev\plat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevToys.UnitTests", "app\tests\DevToys.UnitTests\DevToys.UnitTests.csproj", "{FBD1A1FE-C8F7-4A1F-8B89-C81D3780DADC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevToys.MonacoEditor", "app\dev\DevToys.MonacoEditor\DevToys.MonacoEditor.csproj", "{E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,22 @@ Global {FBD1A1FE-C8F7-4A1F-8B89-C81D3780DADC}.Release|x64.Build.0 = Release|Any CPU {FBD1A1FE-C8F7-4A1F-8B89-C81D3780DADC}.Release|x86.ActiveCfg = Release|Any CPU {FBD1A1FE-C8F7-4A1F-8B89-C81D3780DADC}.Release|x86.Build.0 = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|arm64.ActiveCfg = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|arm64.Build.0 = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|x64.Build.0 = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Debug|x86.Build.0 = Debug|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|Any CPU.Build.0 = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|arm64.ActiveCfg = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|arm64.Build.0 = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|x64.ActiveCfg = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|x64.Build.0 = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|x86.ActiveCfg = Release|Any CPU + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -219,6 +237,7 @@ Global {6279C845-92F8-4333-AB99-3D213163593C} = {B20E4027-0777-4A75-A848-609594E9A6B0} {A1CB6EEE-3CD2-416C-85E0-2C612FE2DF49} = {3D2285D5-0E44-4B1A-9B74-2BBE3108D5B7} {FBD1A1FE-C8F7-4A1F-8B89-C81D3780DADC} = {4C40BF47-315A-4B60-9ED6-8D65B5E79C2C} + {E4354FEC-B0BB-49AC-A9D2-ABBE7F74DCC5} = {B20E4027-0777-4A75-A848-609594E9A6B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C2C953F5-F97F-4198-B15C-4947A886050F} diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6dfdd620bb..29f564b10f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -2,15 +2,18 @@ 8.0.0 6.0.0 - 4.6.19 + 4.7.0-dev.666 8.0.0-dev.65 + 10.0.22000.28 + + @@ -18,17 +21,21 @@ + + + + diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts new file mode 100644 index 0000000000..ee2e25be6c --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts @@ -0,0 +1,113 @@ +(function() +{ + //Debug.log("Create dynamic style element"); + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.id='dynamic'; + head.appendChild(style); + + //Debug.log("Starting Monaco Load"); + + var editor; + var model; + var contexts = {}; + window.contexts = {}; + var decorations = []; + window.decorations = []; + var modifingSelection = false; // Supress updates to selection when making edits. + window.modifyingSelection = false; + + require.config({ paths: { 'vs': '{UNO_BOOTSTRAP_WEBAPP_BASE_PATH}{UNO_BOOTSTRAP_APP_BASE}/devtoys.monacoeditor/monaco-editor/min/vs' } }); + require( + ['vs/editor/editor.main'], + function () + { + //Debug.log("Grabbing Monaco Options"); + + //var opt = {}; + //try + //{ + // opt = getOptions(); + //} + //catch(err) + //{ + // //Debug.log("Unable to read options - " + err); + //} + + //Debug.log("Getting Parent Text value"); + //opt["value"] = getParentValue("Text"); + + //Debug.log("Getting Host container"); + //Debug.log("Creating Editor"); + const editor = monaco.editor.create(element, null /* opt */); + window.editor = editor; + + //Debug.log("Getting Editor model"); + //model = editor.getModel(); + //window.model = model; + + //// Listen for Content Changes + ////Debug.log("Listening for changes in the editor model - " + (!model)); + //model.onDidChangeContent( + // (event) => + // { + // //Parent.setValue("Text", stringifyForMarshalling(model.getValue())); + // //console.log("buffers: " + JSON.stringify(model._buffer._pieceTree._buffers)); + // //console.log("commandMgr: " + JSON.stringify(model._commandManager)); + // //console.log("viewState:" + JSON.stringify(editor.saveViewState())); + // }); + + //// Listen for Selection Changes + ////Debug.log("Listening for changes in the editor selection"); + //editor.onDidChangeCursorSelection( + // (event) => + // { + // if (!modifingSelection) + // { + // console.log(event.source); + // //Parent.setValue("SelectedText", stringifyForMarshalling(model.getValueInRange(event.selection))); + // // Parent.setValueWithType("SelectedRange", stringifyForMarshalling(JSON.stringify(event.selection)), "Selection"); + // } + // }); + + //// Set theme + ////Debug.log("Getting parent theme value"); + //let theme = getParentJsonValue("RequestedTheme"); + //theme + // = { + // "0": "Default", + // "1": "Light", + // "2": "Dark" + // } + // [theme]; + + ////Debug.log("Current theme value - " + theme); + //if (theme == "Default") + //{ + // Debug.log("Loading default theme"); + // theme = getThemeCurrentThemeName(); + //} + + ////Debug.log("Changing theme"); + //changeTheme(theme, getThemeIsHighContrast()); + + // Update Monaco Size when we receive a window resize event + //Debug.log("Listen for resize events on the window and resize the editor"); + window.addEventListener( + "resize", + () => + { + editor.layout(); + }); + + // Disable WebView Scrollbar so Monaco Scrollbar can do heavy lifting + document.body.style.overflow = 'hidden'; + + // Callback to Parent that we're loaded + //Debug.log("Loaded Monaco"); + //Parent.callAction("Loaded"); + + //Debug.log("Ending Monaco Load"); + } + ); +})(); \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html new file mode 100644 index 0000000000..5033c2f0c3 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html @@ -0,0 +1,93 @@ + + + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs new file mode 100644 index 0000000000..90ebb8a5c7 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs @@ -0,0 +1,46 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace DevToys.MonacoEditor; + +/// +/// .Net wrapper for the Monaco CodeEditor +/// https://microsoft.github.io/monaco-editor/ +/// +[TemplatePart(Name = "View", Type = typeof(ICodeEditorPresenter))] +public sealed partial class CodeEditor : Control +{ + private ICodeEditorPresenter _view; + + public CodeEditor() + { + DefaultStyleKey = typeof(CodeEditor); + } + + protected override void OnApplyTemplate() + { + if (_view != null) + { + //_view.NavigationStarting -= WebView_NavigationStarting; + //_view.DOMContentLoaded -= WebView_DOMContentLoaded; + //_view.NavigationCompleted -= WebView_NavigationCompleted; + //_view.NewWindowRequested -= WebView_NewWindowRequested; + //Debug.WriteLine("Setting initialized - false"); + //_initialized = false; + } + + _view = (ICodeEditorPresenter)GetTemplateChild("View"); + + if (_view != null) + { + //_view.NavigationStarting += WebView_NavigationStarting; + //_view.DOMContentLoaded += WebView_DOMContentLoaded; + //_view.NavigationCompleted += WebView_NavigationCompleted; + //_view.NewWindowRequested += WebView_NewWindowRequested; + + _view.LaunchAsync(); + + base.OnApplyTemplate(); + } + } +} diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs new file mode 100644 index 0000000000..594e42726f --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs @@ -0,0 +1,80 @@ +#if __WASM__ + +using System.Reflection; +using DevToys.ComponentModel.Threading; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Web.WebView2.Core; +using Uno.Foundation.Interop; +using Uno.UI.Runtime.WebAssembly; +using Windows.Foundation; +using Windows.Storage; + +namespace DevToys.MonacoEditor; + +/// +/// Provides a
object in the app's web page that will show the Monaco Editor. +///
+[HtmlElement("div")] +public sealed partial class CodeEditorPresenter : Control, ICodeEditorPresenter, IJSObject +{ + private static readonly string UNO_BOOTSTRAP_APP_BASE = Environment.GetEnvironmentVariable(nameof(UNO_BOOTSTRAP_APP_BASE)); + private static readonly string UNO_BOOTSTRAP_WEBAPP_BASE_PATH = Environment.GetEnvironmentVariable(nameof(UNO_BOOTSTRAP_WEBAPP_BASE_PATH)) ?? ""; + + private readonly JSObjectHandle _handle; + + public CodeEditorPresenter() + { + _handle = JSObjectHandle.Create(this); + } + + /// + JSObjectHandle IJSObject.Handle => _handle; + + /// + public event TypedEventHandler? NewWindowRequested; + + /// + public event TypedEventHandler? NavigationStarting; + + /// + public event TypedEventHandler? DOMContentLoaded; + + /// + public event TypedEventHandler? NavigationCompleted; + + public async Task LaunchAsync() + { + Task domContentLoadedEventTask + = DispatcherQueue.RunOnUIThreadAsync( + DispatcherQueuePriority.Low, + () => DOMContentLoaded?.Invoke(this, new CoreWebView2DOMContentLoadedEventArgs())); + + Assembly assembly = GetType().GetTypeInfo().Assembly; + using Stream stream = assembly.GetManifestResourceStream("DevToys.MonacoEditor.CodeEditor.Wasm.ts")!; + using var streamReader = new StreamReader(stream); + + string monacoEditorJavaScriptEntryPoint = await streamReader.ReadToEndAsync(); + Guard.IsNotNullOrWhiteSpace(monacoEditorJavaScriptEntryPoint); + + monacoEditorJavaScriptEntryPoint + = monacoEditorJavaScriptEntryPoint + .Replace($"{{{nameof(UNO_BOOTSTRAP_WEBAPP_BASE_PATH)}}}", UNO_BOOTSTRAP_WEBAPP_BASE_PATH) + .Replace($"{{{nameof(UNO_BOOTSTRAP_APP_BASE)}}}", UNO_BOOTSTRAP_APP_BASE); + + await domContentLoadedEventTask; + + await DispatcherQueue.RunOnUIThreadAsync( + DispatcherQueuePriority.Low, + () => NavigationStarting?.Invoke(this, new CoreWebView2NavigationStartingEventArgs())); + + await this.ExecuteJavascriptAsync(monacoEditorJavaScriptEntryPoint); + + await DispatcherQueue.RunOnUIThreadAsync( + DispatcherQueuePriority.Low, + () => NavigationCompleted?.Invoke(this, new CoreWebView2NavigationCompletedEventArgs())); + } +} + +#endif diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs new file mode 100644 index 0000000000..b848f438c9 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs @@ -0,0 +1,63 @@ +#if __WINDOWS__ + +using Microsoft.UI.Xaml.Controls; +using Microsoft.Web.WebView2.Core; +using Windows.Foundation; +using Windows.Storage; + +namespace DevToys.MonacoEditor; + +/// +/// Provides a WebView that displays the Monaco Editor. +/// +public sealed partial class CodeEditorPresenter : UserControl, ICodeEditorPresenter +{ + private readonly WebView2 _webView = new(); + + public CodeEditorPresenter() + { + Content = _webView; + + _webView.CoreWebView2Initialized += WebView_CoreWebView2Initialized; + } + + /// + public event TypedEventHandler? NewWindowRequested; + + /// + public event TypedEventHandler? NavigationStarting; + + /// + public event TypedEventHandler? DOMContentLoaded; + + /// + public event TypedEventHandler? NavigationCompleted; + + public async Task LaunchAsync() + { + await _webView.EnsureCoreWebView2Async(); + + StorageFile storageFile + = await StorageFile.GetFileFromApplicationUriAsync( + new Uri("ms-appx:///DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html")); + + string rootDirectory = Directory.GetParent(storageFile.Path)!.Parent!.FullName; + + _webView.CoreWebView2.SetVirtualHostNameToFolderMapping( + hostName: "devtoys.local", + folderPath: rootDirectory, + CoreWebView2HostResourceAccessKind.Allow); + + _webView.Source = new Uri("https://devtoys.local/CodeEditor/CodeEditor.Windows.html"); + } + + private void WebView_CoreWebView2Initialized(WebView2 sender, CoreWebView2InitializedEventArgs args) + { + _webView.CoreWebView2.NewWindowRequested += (wv, args) => NewWindowRequested?.Invoke(this, args); + _webView.CoreWebView2.NavigationStarting += (wv, args) => NavigationStarting?.Invoke(this, args); + _webView.CoreWebView2.DOMContentLoaded += (wv, args) => DOMContentLoaded?.Invoke(this, args); + _webView.CoreWebView2.NavigationCompleted += (wv, args) => NavigationCompleted?.Invoke(this, args); + } +} + +#endif diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs new file mode 100644 index 0000000000..31e6add81c --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs @@ -0,0 +1,32 @@ +using Microsoft.Web.WebView2.Core; +using Windows.Foundation; + +namespace DevToys.MonacoEditor; + +public interface ICodeEditorPresenter +{ + /// + /// Occurs when a user performs an action in a WebView that causes content to be opened in a new window. + /// + event TypedEventHandler? NewWindowRequested; + + /// + /// Occurs before the WebView navigates to new content. + /// + event TypedEventHandler? NavigationStarting; + + /// + /// Occurs when the WebView has finished parsing the current HTML content. + /// + event TypedEventHandler? DOMContentLoaded; + + /// + /// Occurs when the WebView has finished loading the current content or if navigation has failed. + /// + event TypedEventHandler? NavigationCompleted; + + /// + /// Launch the web element of Monaco editor. + /// + Task LaunchAsync(); +} diff --git a/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj b/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj new file mode 100644 index 0000000000..78bdcaf0dd --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj @@ -0,0 +1,81 @@ + + + $(NetCore);$(NetCoreWindows) + + true + + false + false + true + + + + true + $(DefineConstants);__WINDOWS__ + + + + true + $(DefineConstants);__WASM__ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %(Filename).ts + + + + + + + + + + + DevToys.MonacoEditor.CodeEditor.Wasm.ts + + + + + %(Filename) + + + + + + + + + \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/Themes/Generic.xaml b/src/app/dev/DevToys.MonacoEditor/Themes/Generic.xaml new file mode 100644 index 0000000000..b7028c633c --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/Themes/Generic.xaml @@ -0,0 +1,28 @@ + + + + + diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.DebugLogger.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.DebugLogger.ts new file mode 100644 index 0000000000..5ce135fa09 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.DebugLogger.ts @@ -0,0 +1,5 @@ +//namespace Monaco.Helpers { + interface DebugLogger { + log(message: string); + } +//} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.KeyboardListener.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.KeyboardListener.ts new file mode 100644 index 0000000000..2ec5e0341e --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.KeyboardListener.ts @@ -0,0 +1,5 @@ +//namespace Monaco.Helpers { +interface KeyboardListener { + keyDown(keycode: number, ctrl: boolean, shift: boolean, alt: boolean, meta: boolean): boolean; +} +//} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts new file mode 100644 index 0000000000..9a52eef179 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts @@ -0,0 +1,134 @@ +/// +declare var Parent: ParentAccessor; +declare var Keyboard: KeyboardListener; + +declare var editor: monaco.editor.IStandaloneCodeEditor; +declare var model: monaco.editor.ITextModel; +declare var contexts: { [index: string]: monaco.editor.IContextKey };//{}; +declare var decorations: string[]; +declare var modifingSelection:boolean; // Supress updates to selection when making edits. + +var registerHoverProvider = function (languageId: string) { + return monaco.languages.registerHoverProvider(languageId, { + provideHover: function (model, position) { + return callParentEventAsync("HoverProvider" + languageId, [JSON.stringify(position)]).then(result => { + if (result) { + return JSON.parse(result); + } + }); + } + }); +}; + +var addAction = function (action: monaco.editor.IActionDescriptor) { + action.run = function (ed) { + Parent.callAction("Action" + action.id) + }; + + editor.addAction(action); +}; + +var addCommand = function (keybindingStr, handlerName, context) { + return editor.addCommand(parseInt(keybindingStr), + function() { + let objs = []; + if (arguments + ) { // Use arguments as Monaco will pass each as it's own parameter, so we don't know how many that may be. + for (let i = 1; i < arguments.length; i++) { // Skip first one as that's the sender? + objs.push(arguments[i]); + } + } + + return callParentActionWithParameters(handlerName, objs); + }, + context + ); +}; + +var createContext = function (context) { + if (context) { + contexts[context.key] = editor.createContextKey(context.key, context.defaultValue); + } +}; + +var updateContext = function (key, value) { + contexts[key].set(value); +} + +var updateContent = function (content) { + // Need to ignore updates from us notifying of a change + if (content != model.getValue()) { + model.setValue(content); + } +}; + + + + + + + +var updateDecorations = function (newHighlights) { + if (newHighlights) { + decorations = editor.deltaDecorations(decorations, newHighlights); + } else { + decorations = editor.deltaDecorations(decorations, []); + } +}; + +var updateStyle = function (innerStyle) { + var style = document.getElementById("dynamic"); + style.innerHTML = innerStyle; +}; + +var getOptions = function (): monaco.editor.IEditorOptions { + let opt = null; + try { + var jopt = getParentJsonValue("Options"); + // console.log('Options: ' + jopt); + opt = JSON.parse(jopt); + } finally { + + } + + if (opt != null && typeof opt === "object") { + return opt; + } + + return {}; +}; + +var updateOptions = function (opt: monaco.editor.IEditorOptions) { + if (opt != null && typeof opt === "object") { + editor.updateOptions(opt); + } +}; + +var updateLanguage = function (language) { + monaco.editor.setModelLanguage(model, language); +}; + +var changeTheme = function (theme: string, highcontrast) { + var newTheme = 'vs'; + if (highcontrast == "True" || highcontrast == "true") { + newTheme = 'hc-black'; + } else if (theme == "Dark") { + newTheme = 'vs-dark'; + } + + monaco.editor.setTheme(newTheme); +}; + + + +var keyDown = function (event) { + //Debug.log("Key Down:" + event.keyCode + " " + event.ctrlKey); + var result = Keyboard.keyDown(event.keyCode, event.ctrlKey, event.shiftKey, event.altKey, event.metaKey); + if (result) { + event.cancelBubble = true; + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + return false; + } +}; diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts new file mode 100644 index 0000000000..2c0fc6ae42 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts @@ -0,0 +1,25 @@ +/// +declare var Parent: ParentAccessor; + +var registerCodeLensProvider = function (languageId) { + return monaco.languages.registerCodeLensProvider(languageId, { + provideCodeLenses: function (model, token) { + return callParentEventAsync("ProvideCodeLenses" + languageId, null).then(result => { + if (result) { + return JSON.parse(result); + } + return null; + + }); + }, + resolveCodeLens: function (model, codeLens, token) { + return callParentEventAsync("ResolveCodeLens" + languageId, [JSON.stringify(codeLens)]).then(result => { + if (result) { + return JSON.parse(result); + } + return null; + }); + } + // TODO: onDidChange, don't know what this does. + }); +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts new file mode 100644 index 0000000000..a85a7648f0 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts @@ -0,0 +1,21 @@ +/// +declare var Parent: ParentAccessor; + +var registerColorProvider = function (languageId) { + return monaco.languages.registerColorProvider(languageId, { + provideColorPresentations: function (model, colorInfo, token) { + return callParentEventAsync("ProvideColorPresentations" + languageId, [JSON.stringify(colorInfo)]).then(result => { + if (result) { + return JSON.parse(result); + } + }); + }, + provideDocumentColors: function (model, token) { + return callParentEventAsync("ProvideDocumentColors" + languageId, []).then(result => { + if (result) { + return JSON.parse(result); + } + }); + } + }); +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts new file mode 100644 index 0000000000..d10aa849a4 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts @@ -0,0 +1,23 @@ +/// +declare var Parent: ParentAccessor; + +var registerCompletionItemProvider = (languageId, characters) => + monaco.languages.registerCompletionItemProvider(languageId, + { + triggerCharacters: characters, + provideCompletionItems: (model, position, context, token) => callParentEventAsync( + "CompletionItemProvider" + languageId, + [JSON.stringify(position), JSON.stringify(context)]).then(result => { + if (result) { + return JSON.parse(result); + } + return null; + }), + resolveCompletionItem: (item, token) => callParentEventAsync("CompletionItemRequested" + languageId, + [JSON.stringify(item)]).then(result => { + if (result) { + return JSON.parse(result); + } + return null; + }) + }); diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ParentAccessor.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ParentAccessor.ts new file mode 100644 index 0000000000..4e672df567 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ParentAccessor.ts @@ -0,0 +1,15 @@ +//namespace Monaco.Helpers { + interface ParentAccessor { + callAction(name: string): boolean; + callActionWithParameters(name: string, parameter1: string, parameter2: string): boolean; + callEvent(name: string, callbackMethod: string, parameter1: string, parameter2: string); + close(); + getChildValue(name: string, child: string): any; + getJsonValue(name: string, returnId: string); + //getValue(name: string, returnId: string); + //setValue(name: string, value: any); + //setValue(name: string, value: string, type: string); + setValue(name: string, value: string); + setValueWithType(name: string, value: string, type: string); + } +//} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts new file mode 100644 index 0000000000..a827cf0817 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts @@ -0,0 +1,6 @@ +//namespace Monaco.Helpers { +interface ThemeAccessor { + getCurrentThemeName(returnId: string); + getIsHighContrast(returnId: string); + } +//} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/asyncCallbackHelpers.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/asyncCallbackHelpers.ts new file mode 100644 index 0000000000..6f05e51791 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/asyncCallbackHelpers.ts @@ -0,0 +1,130 @@ +declare var Parent: ParentAccessor; +declare var Theme: ThemeAccessor; + +type MethodWithReturnId = (parameter: string) => void; +type NumberCallback = (parameter: any) => void; +declare var asyncCallbackMap: { [promiseId: string]: NumberCallback }; +declare var nextAsync: number; + +nextAsync = 1; +asyncCallbackMap = {}; + +declare var returnValueCallbackMap: { [returnId: string]: string }; +declare var nextReturn: number; + +nextReturn = 1; +returnValueCallbackMap = {}; + +const asyncCallback = (promiseId: string, parameter: string) => { + const promise = asyncCallbackMap[promiseId]; + if (promise) { + //console.log('Async response: ' + parameter); + promise(parameter); + } +} + +const returnValueCallback = (returnId: string, returnValue: string) => { + //console.log('Return value for id ' + returnId + ' is ' + returnValue); + returnValueCallbackMap[returnId] = returnValue; +} + +const invokeAsyncMethod = (syncMethod: NumberCallback): Promise => { + if (nextAsync==null) { + nextAsync = 0; + } + if (asyncCallbackMap==null) { + asyncCallbackMap = {}; + } + const promise = new Promise((resolve, reject) => { + var nextId = nextAsync++; + asyncCallbackMap[nextId] = resolve; + syncMethod(`${nextId}`); + }); + return promise; +} + +const replaceAll = (str: string, find: string, rep: string): string => { + if (find == "\\") + { + find = "\\\\"; + } + return (`${str}`).replace(new RegExp(find, "g"), rep); +} + +const sanitize = (jsonString: string): string => { + if (jsonString == null) { + //console.log('Sanitized is null'); + return null; + } + + const replacements = "%&\\\"'{}:,"; + for (let i = 0; i < replacements.length; i++) { + jsonString = replaceAll(jsonString, replacements.charAt(i), `%${replacements.charCodeAt(i)}`); + } + //console.log('Sanitized: ' + jsonString); + return jsonString; +} + +const desantize = (parameter: string): string => { + //System.Diagnostics.Debug.WriteLine($"Encoded String: {parameter}"); + if (parameter == null) return parameter; + const replacements = "&\\\"'{}:,%"; + //System.Diagnostics.Debug.WriteLine($"Replacements: >{replacements}<"); + for (let i = 0; i < replacements.length; i++) + { + //console.log("Replacing: >%" + replacements.charCodeAt(i) + "< with >" + replacements.charAt(i) + "< "); + parameter = replaceAll(parameter, "%" + replacements.charCodeAt(i), replacements.charAt(i)); + } + + //console.log("Decoded String: " + parameter ); + return parameter; +} + +const stringifyForMarshalling = (value: any): string => sanitize(value) + +const invokeWithReturnValue = (methodToInvoke: MethodWithReturnId): string => { + const nextId = nextReturn++; + methodToInvoke(nextId + ''); + var json = returnValueCallbackMap[nextId]; + //console.log('Return json ' + json); + json = desantize(json); + return json; +} + +const getParentValue = (name: string): any => { + const jsonString = invokeWithReturnValue((returnId) => Parent.getJsonValue(name, returnId)); + const obj = JSON.parse(jsonString); + return obj; +} + +const getParentJsonValue = (name: string): string => + invokeWithReturnValue((returnId) => Parent.getJsonValue(name, returnId)) + +const getThemeIsHighContrast = (): boolean => + invokeWithReturnValue((returnId) => Theme.getIsHighContrast(returnId)) == "true"; + +const getThemeCurrentThemeName = (): string => + invokeWithReturnValue((returnId) => Theme.getCurrentThemeName(returnId)); + + +const callParentEventAsync = (name: string, parameters: string[]): Promise => + invokeAsyncMethod(async (promiseId) => { + let result = await Parent.callEvent(name, + promiseId, + parameters != null && parameters.length > 0 ? stringifyForMarshalling(parameters[0]) : null, + parameters != null && parameters.length > 1 ? stringifyForMarshalling(parameters[1]) : null); + if (result) { + //console.log('Parent event result: ' + name + ' - ' + result); + result = desantize(result); + //console.log('Desanitized: ' + name + ' - ' + result); + } else { + //console.log('No Parent event result for ' + name); + } + + return result; + }); + +const callParentActionWithParameters = (name: string, parameters: string[]): boolean => + Parent.callActionWithParameters(name, + parameters != null && parameters.length > 0 ? stringifyForMarshalling(parameters[0]) : null, + parameters != null && parameters.length > 1 ? stringifyForMarshalling(parameters[1]) : null); diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/tsconfig.json b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/tsconfig.json new file mode 100644 index 0000000000..d7cbf4076e --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "emitBOM": false, + "target": "ES2015", + "lib": [ "ES2015", "DOM" ], + "sourceMap": false, + "outFile": "..\\WasmScripts\\uno-monaco-helpers.js" + }, + "include": [ + ".\\", + "..\\ts-helpermethods" + ], + "exclude": [ + "..\\ts-helpermethods\\ts-helpermethods-uwp" + ] +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/Monaco.Helpers.ParentAccessor.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/Monaco.Helpers.ParentAccessor.ts new file mode 100644 index 0000000000..3082610cb4 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/Monaco.Helpers.ParentAccessor.ts @@ -0,0 +1,14 @@ +//namespace Monaco.Helpers { + interface ParentAccessor { + callAction(name: string): boolean; + callActionWithParameters(name: string, parameters: string[]): boolean; + callEvent(name: string, parameters: string[]): Promise + close(); + getChildValue(name: string, child: string): any; + getJsonValue(name: string): string; + getValue(name: string): any; + setValue(name: string, value: any); + setValue(name: string, value: string, type: string); + } + +//} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/asyncCallbackHelpers.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/asyncCallbackHelpers.ts new file mode 100644 index 0000000000..32bddd1344 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/asyncCallbackHelpers.ts @@ -0,0 +1,14 @@ +var callParentEventAsync = function (name: string, parameters: string[]): Promise { + return Parent.callEvent(name, parameters); +} + +var callParentActionWithParameters = function (name: string, parameters: string[]): boolean { + return Parent.callActionWithParameters(name, parameters); +} + +var getParentJsonValue = function (name: string): string { + return Parent.getJsonValue(name); +} + + + diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json new file mode 100644 index 0000000000..6c7547f82c --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "ES2015", + "sourceMap": false + } +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts new file mode 100644 index 0000000000..e742a81aa4 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts @@ -0,0 +1,48 @@ +/// +declare var Parent: ParentAccessor; +declare var Keyboard: KeyboardListener; + +declare var editor: monaco.editor.IStandaloneCodeEditor; +declare var model: monaco.editor.ITextModel; +declare var contexts: { [index: string]: monaco.editor.IContextKey };//{}; +declare var decorations: string[]; +declare var modifingSelection: boolean; // Supress updates to selection when making edits. + +var updateSelectedContent = function (content) { + let selection = editor.getSelection(); + + // Need to ignore updates from us notifying of a change + if (content != model.getValueInRange(selection)) { + modifingSelection = true; + let range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn); + let op = { identifier: { major: 1, minor: 1 }, range, text: content, forceMoveMarkers: true }; + + // Make change to selection + //TODO how to properly fix this code? + //model.pushEditOperations([], [op]); + model.pushEditOperations([], [op], null); + + // Update selection to new text. + var newEndLineNumber = selection.startLineNumber + content.split('\r').length - 1; // TODO: Not sure if line end is situational/platform specific... investigate more. + var newEndColumn = (selection.startLineNumber === selection.endLineNumber) + ? selection.startColumn + content.length + : content.length - content.lastIndexOf('\r'); + + selection = selection.setEndPosition(newEndLineNumber, newEndColumn); + + // Update other selection bound for direction. + + //TODO how to properly fix this code? + selection = selection.setEndPosition(selection.endLineNumber, selection.endColumn); + //if (selection.getDirection() == monaco.SelectionDirection.LTR) { + // selection.positionColumn = selection.endColumn; + // selection.positionLineNumber = selection.endLineNumber; + //} else { + // selection.selectionStartColumn = selection.endColumn; + // selection.selectionStartLineNumber = selection.endLineNumber; + //} + + modifingSelection = false; + editor.setSelection(selection); + } +}; \ No newline at end of file diff --git a/src/app/dev/DevToys.UI/App.xaml.cs b/src/app/dev/DevToys.UI/App.xaml.cs index 282bab59e9..6b19c40dbd 100644 --- a/src/app/dev/DevToys.UI/App.xaml.cs +++ b/src/app/dev/DevToys.UI/App.xaml.cs @@ -145,32 +145,35 @@ private static void InitializeLogging() builder.AddFilter("Windows", LogLevel.Warning); builder.AddFilter("Microsoft", LogLevel.Warning); - // Generic Xaml events - // builder.AddFilter("Windows.UI.Xaml", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.VisualStateGroup", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.StateTriggerBase", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.UIElement", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.FrameworkElement", LogLevel.Trace ); + if (Debugger.IsAttached) + { + // Generic Xaml events + builder.AddFilter("Windows.UI.Xaml", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.VisualStateGroup", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.StateTriggerBase", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.UIElement", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.FrameworkElement", LogLevel.Trace); - // Layouter specific messages - // builder.AddFilter("Windows.UI.Xaml.Controls", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.Controls.Layouter", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.Controls.Panel", LogLevel.Debug ); + // Layouter specific messages + builder.AddFilter("Windows.UI.Xaml.Controls", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.Controls.Layouter", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.Controls.Panel", LogLevel.Debug); - // builder.AddFilter("Windows.Storage", LogLevel.Debug ); + builder.AddFilter("Windows.Storage", LogLevel.Debug); - // Binding related messages - // builder.AddFilter("Windows.UI.Xaml.Data", LogLevel.Debug ); - // builder.AddFilter("Windows.UI.Xaml.Data", LogLevel.Debug ); + // Binding related messages + builder.AddFilter("Windows.UI.Xaml.Data", LogLevel.Debug); + builder.AddFilter("Windows.UI.Xaml.Data", LogLevel.Debug); - // Binder memory references tracking - // builder.AddFilter("Uno.UI.DataBinding.BinderReferenceHolder", LogLevel.Debug ); + // Binder memory references tracking + builder.AddFilter("Uno.UI.DataBinding.BinderReferenceHolder", LogLevel.Debug); - // RemoteControl and HotReload related - // builder.AddFilter("Uno.UI.RemoteControl", LogLevel.Information); + // RemoteControl and HotReload related + builder.AddFilter("Uno.UI.RemoteControl", LogLevel.Information); - // Debug JS interop - // builder.AddFilter("Uno.Foundation.WebAssemblyRuntime", LogLevel.Debug ); + // Debug JS interop + builder.AddFilter("Uno.Foundation.WebAssemblyRuntime", LogLevel.Debug); + } }); global::Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory = factory; diff --git a/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs b/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs new file mode 100644 index 0000000000..395972bdda --- /dev/null +++ b/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs @@ -0,0 +1,54 @@ +using Windows.Foundation; +using System.Threading.Tasks; + +namespace DevToys.ComponentModel.Threading; + +/// +/// Provides a set of helper method to play around with threads. +/// +internal static class TaskExtension +{ + /// + /// Runs a task without waiting for its result. + /// + internal static void Forget(this Task _) + { + } + /// + /// Runs a task without waiting for its result. + /// + internal static void Forget(this IAsyncAction _) + { + } + + /// + /// Runs a task without waiting for its result. + /// + internal static void Forget(this Task _) + { + } + + /// + /// Runs a task without waiting for its result. Swallows or handle any exception caused by the task. + /// + /// The action to run when an exception is caught. + internal static async void ForgetSafely(this Task task, Action? errorHandler = null) + { + try + { + await task.ConfigureAwait(true); + } + catch (Exception ex) + { + errorHandler?.Invoke(ex); + } + } + + /// + /// Gets the result of the task synchronously, on the current thread. + /// + internal static T CompleteOnCurrentThread(this Task task) + { + return task.GetAwaiter().GetResult(); + } +} diff --git a/src/app/dev/DevToys.UI/Core/Threading/ThreadHelper.cs b/src/app/dev/DevToys.UI/Core/Threading/ThreadHelper.cs new file mode 100644 index 0000000000..31306e164d --- /dev/null +++ b/src/app/dev/DevToys.UI/Core/Threading/ThreadHelper.cs @@ -0,0 +1,188 @@ +using Microsoft.UI.Dispatching; +using System.Threading.Tasks; + +namespace DevToys.ComponentModel.Threading; + +internal static class ThreadHelper +{ + internal static void ThrowIfNotOnUIThread(this DispatcherQueue dispatcherQueue) + { + if (!dispatcherQueue.HasThreadAccess) + { + throw new Exception("The UI thread is expected, but the current call stack is running on another thread."); + } + } + + internal static void ThrowIfOnUIThread(this DispatcherQueue dispatcherQueue) + { + if (dispatcherQueue.HasThreadAccess) + { + throw new Exception("The UI thread is not expected, but the current call stack is running on UI thread."); + } + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, Action action) + { + return RunOnUIThreadAsync(dispatcherQueue, DispatcherQueuePriority.Normal, action); + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, Action action) + { + if (action is null) + { + return Task.CompletedTask; + } + + if (dispatcherQueue.HasThreadAccess && priority == DispatcherQueuePriority.Normal) + { + action(); + return Task.CompletedTask; + } + else + { + var tcs = new TaskCompletionSource(0); + dispatcherQueue.TryEnqueue( + priority, + () => + { + try + { + action(); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + finally + { + tcs.TrySetResult(0); + } + }); + return tcs.Task; + } + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, Func func) + { + return RunOnUIThreadAsync(dispatcherQueue, DispatcherQueuePriority.Normal, func); + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, Func func) + { + if (func is null) + { + return Task.FromResult(default!); + } + + if (dispatcherQueue.HasThreadAccess && priority == DispatcherQueuePriority.Normal) + { + return Task.FromResult(func()); + } + else + { + var tcs = new TaskCompletionSource(); + dispatcherQueue.TryEnqueue( + priority, + () => + { + T result = default!; + try + { + result = func(); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + finally + { + tcs.TrySetResult(result); + } + }); + return tcs.Task; + } + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, Func action) + { + return RunOnUIThreadAsync(dispatcherQueue, DispatcherQueuePriority.Normal, action); + } + + internal static async Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, Func action) + { + if (action is null) + { + return; + } + + if (dispatcherQueue.HasThreadAccess && priority == DispatcherQueuePriority.Normal) + { + await action().ConfigureAwait(true); + } + else + { + var tcs = new TaskCompletionSource(0); + dispatcherQueue.TryEnqueue( + priority, + async () => + { + try + { + ThrowIfNotOnUIThread(dispatcherQueue); + await action().ConfigureAwait(true); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + finally + { + tcs.TrySetResult(0); + } + }); + + await tcs.Task.ConfigureAwait(false); + } + } + + internal static Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, Func> action) + { + return RunOnUIThreadAsync(dispatcherQueue, DispatcherQueuePriority.Normal, action); + } + + internal static async Task RunOnUIThreadAsync(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, Func> action) + { + Guard.IsNotNull(action); + + if (dispatcherQueue.HasThreadAccess && priority == DispatcherQueuePriority.Normal) + { + return await action().ConfigureAwait(true); + } + else + { + T result = default!; + var tcs = new TaskCompletionSource(0); + dispatcherQueue.TryEnqueue( + priority, + async () => + { + try + { + ThrowIfNotOnUIThread(dispatcherQueue); + result = await action().ConfigureAwait(true); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + finally + { + tcs.TrySetResult(0); + } + }); + + await tcs.Task.ConfigureAwait(false); + return result!; + } + } +} diff --git a/src/app/dev/DevToys.UI/DevToys.UI.projitems b/src/app/dev/DevToys.UI/DevToys.UI.projitems index 366ddd6758..db4430a31b 100644 --- a/src/app/dev/DevToys.UI/DevToys.UI.projitems +++ b/src/app/dev/DevToys.UI/DevToys.UI.projitems @@ -18,6 +18,8 @@ App.xaml + +
@@ -33,26 +35,20 @@ MSBuild:Compile - <_Globbed_Compile Include="$(MSBuildThisFileDirectory)**/*.xaml.cs" Exclude="@(Compile)"> %(Filename) <_Globbed_Compile Include="$(MSBuildThisFileDirectory)**/*.cs" Exclude="@(Compile);@(_Globbed_Compile)" /> - <_Globbed_PRIResource Include="$(MSBuildThisFileDirectory)**/*.resw" Exclude="@(PRIResource)" /> - <_Globbed_Content Include="$(MSBuildThisFileDirectory)Assets/**/*.*" Exclude="@(Content)" /> - - - <_Globbed_Embedded_Resource Include="$(MSBuildThisFileDirectory)*.json" Exclude="@(EmbeddedResource)" /> diff --git a/src/app/dev/DevToys.UI/DevToys.UI.shproj b/src/app/dev/DevToys.UI/DevToys.UI.shproj index 48de812ae6..c80c807277 100644 --- a/src/app/dev/DevToys.UI/DevToys.UI.shproj +++ b/src/app/dev/DevToys.UI/DevToys.UI.shproj @@ -1,13 +1,17 @@  - - 6279c845-92f8-4333-ab99-3d213163593c - 14.0 - - - - - - - - + + 6279c845-92f8-4333-ab99-3d213163593c + 14.0 + + + + + + + + + <_Globbed_Compile Remove="Core\Threading\TaskExtension.cs" /> + <_Globbed_Compile Remove="Core\Threading\ThreadHelper.cs" /> + + \ No newline at end of file diff --git a/src/app/dev/DevToys.UI/Views/MainPage.xaml b/src/app/dev/DevToys.UI/Views/MainPage.xaml index 050d6386cc..db013445d6 100644 --- a/src/app/dev/DevToys.UI/Views/MainPage.xaml +++ b/src/app/dev/DevToys.UI/Views/MainPage.xaml @@ -5,10 +5,28 @@ xmlns:local="using:DevToys.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:monaco="using:DevToys.MonacoEditor" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - - + + + + + + + + + + + + + + + diff --git a/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj b/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj index 8fa8c6d991..940da7c32a 100644 --- a/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj +++ b/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj @@ -24,16 +24,6 @@ - - - - - - - - - - @@ -52,6 +42,14 @@ + + + + @@ -66,6 +64,7 @@ + \ No newline at end of file diff --git a/src/app/dev/platforms/DevToys.Windows/DevToys.Windows.csproj b/src/app/dev/platforms/DevToys.Windows/DevToys.Windows.csproj index 7784dcb2fd..7e09d572a3 100644 --- a/src/app/dev/platforms/DevToys.Windows/DevToys.Windows.csproj +++ b/src/app/dev/platforms/DevToys.Windows/DevToys.Windows.csproj @@ -10,6 +10,7 @@ win10-$(Platform).pubxml true true + true @@ -40,6 +41,7 @@ + @@ -52,8 +54,8 @@ the "Microsoft.Windows.SDK.BuildTools" package above, and the "revision" version number must be the highest found in https://www.nuget.org/packages/Microsoft.Windows.SDK.NET.Ref. --> - - + + diff --git a/src/app/dev/shared/SharedAssemblyInfo.cs b/src/app/dev/shared/SharedAssemblyInfo.cs index 2486d5d821..c0a5180dac 100644 --- a/src/app/dev/shared/SharedAssemblyInfo.cs +++ b/src/app/dev/shared/SharedAssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Reflection; using System.Runtime.CompilerServices; -#if NET6_0_OR_GREATER && WINDOWS && !HAS_UNO +#if (NET6_0_OR_GREATER && WINDOWS && !HAS_UNO) || (__WINDOWS__) using System.Runtime.Versioning; #endif @@ -25,7 +25,7 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -#if NET6_0_OR_GREATER && WINDOWS && !HAS_UNO +#if (NET6_0_OR_GREATER && WINDOWS && !HAS_UNO) || (__WINDOWS__) [assembly: SupportedOSPlatform("windows10.0.17763.0")] #endif diff --git a/tools/Restore-MonacoEditor.ps1 b/tools/Restore-MonacoEditor.ps1 new file mode 100644 index 0000000000..e002e3aa39 --- /dev/null +++ b/tools/Restore-MonacoEditor.ps1 @@ -0,0 +1,50 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string]$RootFolder +) + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } + +if (-not (Test-Path -Path "$RootFolder\\.gitignore")) +{ + Write-Error "Please run this script from the repository's root folder" + return +} + +# Reference to Monaco Version to Use in the Package +$monaco_version = "0.34.1" + +# ------------------------ +$monaco_tgz_url = "https://registry.npmjs.org/monaco-editor/-/monaco-editor-$monaco_version.tgz" +$temp_dir_name = ".temp" +$script_dir = "$RootFolder\tools" + +Push-Location $script_dir + +# Remove Old Dependency +Remove-Item "..\src\app\dev\DevToys.MonacoEditor\monaco-editor" -Force -Recurse -ErrorAction SilentlyContinue + +# Clean-up Temp Dir, if already exist +Remove-Item $temp_dir_name -Force -Recurse -ErrorAction SilentlyContinue + +# Create Temp Directory and Output +New-Item -Name $temp_dir_name -ItemType Directory | Out-Null +New-Item -Name "..\src\app\dev\DevToys.MonacoEditor\monaco-editor" -ItemType Directory | Out-Null + +Write-Host "Downloading Monaco" + +[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" +Invoke-WebRequest -Uri $monaco_tgz_url -OutFile ".\$temp_dir_name\monaco.tgz" + +Write-Host "Extracting..." + +mkdir "$script_dir\$temp_dir_name\monaco" +tar -zxf "$script_dir\$temp_dir_name\monaco.tgz" -C "$script_dir\$temp_dir_name\monaco" + +Copy-Item -Path ".\$temp_dir_name\monaco\package\*" -Destination "..\src\app\dev\DevToys.MonacoEditor\monaco-editor" -Recurse + +# Clean-up Temp Dir +Remove-Item $temp_dir_name -Force -Recurse -ErrorAction SilentlyContinue + +Pop-Location \ No newline at end of file diff --git a/tools/Restore-MonacoEditor.sh b/tools/Restore-MonacoEditor.sh new file mode 100644 index 0000000000..d34fbc2fb5 --- /dev/null +++ b/tools/Restore-MonacoEditor.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail + +if ! [[ -f "$SCRIPT_DIR//.gitignore" ]]; then + echo "Please run this script from the repository's root folder" + exit -1 +fi + +# Reference to Monaco Version to Use in the Package +MONACO_VERSION="0.34.1" + +# ------------------------ +MONACO_TGZ_URL="https://registry.npmjs.org/monaco-editor/-/monaco-editor-$MONACO_VERSION.tgz" +TEMP_DIR_NAME=".temp" +SCRIPT_DIR="$1/tools" + +cd $SCRIPT_DIR + +# Remove Old Dependency +rm -rf "../src/app/dev/DevToys.MonacoEditor/monaco-editor" + +# Clean-up Temp Dir, if already exist +rm -rf "$SCRIPT_DIR/$TEMP_DIR_NAME" + +# Create Temp Directory and Output +mkdir "$SCRIPT_DIR/$TEMP_DIR_NAME" +mkdir "../src/app/dev/DevToys.MonacoEditor/monaco-editor" + +echo "Downloading Monaco" + +curl -Lsfo "$SCRIPT_DIR/$TEMP_DIR_NAME/monaco.tgz" $MONACO_TGZ_URL + +echo "Extracting..." + +mkdir "$SCRIPT_DIR/$TEMP_DIR_NAME/monaco" +tar -zxf "$SCRIPT_DIR/$TEMP_DIR_NAME/monaco.tgz" -C "$SCRIPT_DIR/$TEMP_DIR_NAME/monaco" + +cp -r "$SCRIPT_DIR/$TEMP_DIR_NAME/monaco/package/" "../src/app/dev/DevToys.MonacoEditor/monaco-editor" + +# Clean-up Temp Dir +rm -rf "$SCRIPT_DIR/$TEMP_DIR_NAME" + +cd $1 \ No newline at end of file