From 00c3507491923bc94ea52c3539ff2304277b36fc Mon Sep 17 00:00:00 2001 From: Etienne BAUDOUX Date: Tue, 20 Dec 2022 16:00:55 -0800 Subject: [PATCH] Refactoring by important some changes from @jeromelaban from https://github.com/unoplatform/uno.monaco-editor-uwp/pull/14 --- src/Directory.Packages.props | 3 +- .../CodeEditor/CodeEditor.Wasm.ts | 113 --------- .../CodeEditor/CodeEditor.Windows.html | 119 ++++----- .../CodeEditor/CodeEditor.cs | 46 +++- .../CodeEditor/CodeEditorPresenter.Wasm.cs | 118 +++++++-- .../CodeEditor/CodeEditorPresenter.Windows.cs | 226 ++++++++++++++++++ .../CodeEditor/ICodeEditorPresenter.cs | 16 +- .../DevToys.MonacoEditor.csproj | 54 +++-- .../Threading/AsyncTypedEventHandler.cs | 3 + .../WebInterop/DebugLogger.Wasm.cs | 31 +++ .../WebInterop/DebugLogger.cs | 12 + .../Monaco.Helpers.ThemeAccessor.ts | 2 +- .../ts-helpermethods/editorContext.ts | 39 +++ .../ts-helpermethods/monacoInitializer.ts | 106 ++++++++ .../otherScriptsToBeOrganized.ts | 113 ++++----- .../registerCodeActionProvider.ts | 40 ++++ .../registerCodeLensProvider.ts | 16 +- .../ts-helpermethods/registerColorProvider.ts | 9 +- .../registerCompletionItemProvider.ts | 37 +-- .../Monaco.Helpers.ParentAccessor.ts | 29 ++- .../asyncCallbackHelpers.ts | 36 ++- .../ts-helpermethods-Wasm/tsconfig.json | 6 +- .../Monaco.Helpers.ParentAccessor.ts | 22 +- .../asyncCallbackHelpers.ts | 13 +- .../ts-helpermethods-Windows/tsconfig.json | 14 ++ .../ts-helpermethods/tsconfig.json | 7 - .../ts-helpermethods/updateSelectedContent.ts | 29 +-- src/app/dev/DevToys.UI/App.xaml.cs | 2 + .../Core/Threading/TaskExtension.cs | 8 + .../DevToys.Wasm/DevToys.Wasm.csproj | 7 - 30 files changed, 887 insertions(+), 389 deletions(-) delete mode 100644 src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/Threading/AsyncTypedEventHandler.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.Wasm.cs create mode 100644 src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.cs rename src/app/dev/DevToys.MonacoEditor/ts-helpermethods/{ts-helpermethods-Wasm => }/Monaco.Helpers.ThemeAccessor.ts (96%) create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/editorContext.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/monacoInitializer.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeActionProvider.ts create mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/tsconfig.json delete mode 100644 src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 29f564b10f..fb37c721d6 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -2,7 +2,7 @@ 8.0.0 6.0.0 - 4.7.0-dev.666 + 4.6.19 8.0.0-dev.65 10.0.22000.28 @@ -16,6 +16,7 @@ + diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts deleted file mode 100644 index ee2e25be6c..0000000000 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Wasm.ts +++ /dev/null @@ -1,113 +0,0 @@ -(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 index 5033c2f0c3..1ffde37b03 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.Windows.html @@ -14,80 +14,83 @@ width: 100%; } - - - -
- - + - + + + + + +
+ + \ 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 index 90ebb8a5c7..498267a762 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditor.cs @@ -1,5 +1,7 @@ -using Microsoft.UI.Xaml; +using DevToys.MonacoEditor.WebInterop; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.Web.WebView2.Core; namespace DevToys.MonacoEditor; @@ -21,11 +23,11 @@ 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"); + _view.NavigationStarting -= WebView_NavigationStarting; + _view.DOMContentLoaded -= WebView_DOMContentLoaded; + _view.NavigationCompleted -= WebView_NavigationCompleted; + _view.NewWindowRequested -= WebView_NewWindowRequested; + Debug.WriteLine("Setting initialized - false"); //_initialized = false; } @@ -33,14 +35,38 @@ protected override void OnApplyTemplate() if (_view != null) { - //_view.NavigationStarting += WebView_NavigationStarting; - //_view.DOMContentLoaded += WebView_DOMContentLoaded; - //_view.NavigationCompleted += WebView_NavigationCompleted; - //_view.NewWindowRequested += WebView_NewWindowRequested; + _view.NavigationStarting += WebView_NavigationStarting; + _view.DOMContentLoaded += WebView_DOMContentLoaded; + _view.NavigationCompleted += WebView_NavigationCompleted; + _view.NewWindowRequested += WebView_NewWindowRequested; + _view.DotNetObjectInjectionRequested += WebView_DotNetObjectInjectionRequested; _view.LaunchAsync(); base.OnApplyTemplate(); } } + + private async Task WebView_DotNetObjectInjectionRequested(ICodeEditorPresenter sender, EventArgs args) + { + await _view.InjectDotNetObjectToWebPageAsync("Debug", new DebugLogger()); + } + + private void WebView_NavigationStarting(ICodeEditorPresenter sender, CoreWebView2NavigationStartingEventArgs args) + { + } + + private void WebView_NavigationCompleted(ICodeEditorPresenter sender, CoreWebView2NavigationCompletedEventArgs args) + { + + } + + private void WebView_DOMContentLoaded(ICodeEditorPresenter sender, CoreWebView2DOMContentLoadedEventArgs args) + { + } + + private void WebView_NewWindowRequested(ICodeEditorPresenter sender, CoreWebView2NewWindowRequestedEventArgs args) + { + + } } diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs index 594e42726f..1df6689094 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Wasm.cs @@ -1,15 +1,22 @@ #if __WASM__ using System.Reflection; +using System.Xml.Linq; using DevToys.ComponentModel.Threading; +using DevToys.MonacoEditor.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.UI; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using Microsoft.Web.WebView2.Core; +using Uno.Extensions; +using Uno.Foundation; using Uno.Foundation.Interop; +using Uno.Logging; using Uno.UI.Runtime.WebAssembly; using Windows.Foundation; -using Windows.Storage; namespace DevToys.MonacoEditor; @@ -19,14 +26,28 @@ namespace DevToys.MonacoEditor; [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 static readonly string UNO_BOOTSTRAP_APP_BASE = Environment.GetEnvironmentVariable(nameof(UNO_BOOTSTRAP_APP_BASE)) ?? string.Empty; + private static readonly string UNO_BOOTSTRAP_WEBAPP_BASE_PATH = Environment.GetEnvironmentVariable(nameof(UNO_BOOTSTRAP_WEBAPP_BASE_PATH)) ?? string.Empty; private readonly JSObjectHandle _handle; + private readonly ILogger? _debugLogger; + private readonly ILogger? _informationLogger; + private readonly ILogger? _errorLogger; + private readonly string _htmlId; public CodeEditorPresenter() { + Guard.IsNotNullOrEmpty(UNO_BOOTSTRAP_APP_BASE); + + Background = new SolidColorBrush(Colors.Transparent); _handle = JSObjectHandle.Create(this); + + _htmlId = this.GetHtmlId(); + + ILogger logger = this.Log(); + _debugLogger = logger.IsEnabled(LogLevel.Debug) ? logger : null; + _informationLogger = logger.IsEnabled(LogLevel.Information) ? logger : null; + _errorLogger = logger.IsEnabled(LogLevel.Error) ? logger : null; } /// @@ -44,36 +65,85 @@ public CodeEditorPresenter() /// public event TypedEventHandler? NavigationCompleted; + /// + public event AsyncTypedEventHandler? DotNetObjectInjectionRequested; + 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); + await DispatcherQueue.RunOnUIThreadAsync( + DispatcherQueuePriority.Low, + () => NavigationStarting?.Invoke(this, new CoreWebView2NavigationStartingEventArgs())); - string monacoEditorJavaScriptEntryPoint = await streamReader.ReadToEndAsync(); - Guard.IsNotNullOrWhiteSpace(monacoEditorJavaScriptEntryPoint); + await DispatcherQueue.RunOnUIThreadAsync( + DispatcherQueuePriority.Low, + () => DOMContentLoaded?.Invoke(this, new CoreWebView2DOMContentLoadedEventArgs())); - monacoEditorJavaScriptEntryPoint - = monacoEditorJavaScriptEntryPoint - .Replace($"{{{nameof(UNO_BOOTSTRAP_WEBAPP_BASE_PATH)}}}", UNO_BOOTSTRAP_WEBAPP_BASE_PATH) - .Replace($"{{{nameof(UNO_BOOTSTRAP_APP_BASE)}}}", UNO_BOOTSTRAP_APP_BASE); + // Request to inject .Net web object into the web page. + Guard.IsNotNull(DotNetObjectInjectionRequested); + await DotNetObjectInjectionRequested.Invoke(this, EventArgs.Empty); - await domContentLoadedEventTask; + try + { + _informationLogger?.Info($"{nameof(LaunchAsync)}: Creating Monaco Editor"); - await DispatcherQueue.RunOnUIThreadAsync( - DispatcherQueuePriority.Low, - () => NavigationStarting?.Invoke(this, new CoreWebView2NavigationStartingEventArgs())); + string monacoEditorJavaScriptEntryPoint = $@"createMonacoEditor('{UNO_BOOTSTRAP_WEBAPP_BASE_PATH}{UNO_BOOTSTRAP_APP_BASE}/devtoys.monacoeditor', element)"; + this.ExecuteJavascript(monacoEditorJavaScriptEntryPoint); - await this.ExecuteJavascriptAsync(monacoEditorJavaScriptEntryPoint); + _informationLogger?.Info($"{nameof(LaunchAsync)}: Monaco Editor created successfully"); + } + catch (Exception e) + { + _errorLogger?.Error($"{nameof(LaunchAsync)} failed", e); + } await DispatcherQueue.RunOnUIThreadAsync( - DispatcherQueuePriority.Low, - () => NavigationCompleted?.Invoke(this, new CoreWebView2NavigationCompletedEventArgs())); + DispatcherQueuePriority.Low, + () => NavigationCompleted?.Invoke(this, new CoreWebView2NavigationCompletedEventArgs())); + } + + public Task InjectDotNetObjectToWebPageAsync(string name, T pObject) + { + _debugLogger?.Debug($"{nameof(InjectDotNetObjectToWebPageAsync)}: Trying to inject .NET object in web page - {name}"); + if (pObject is IJSObject obj) + { + MethodInfo? method = obj.Handle.GetType().GetMethod("GetNativeInstance", BindingFlags.NonPublic | BindingFlags.Instance); + if (method is null) + { + _errorLogger?.Error($"{nameof(InjectDotNetObjectToWebPageAsync)}: GetNativeInstance method doesn't exist."); + return Task.CompletedTask; + } + + string? native = method.Invoke(obj.Handle, new object[] { }) as string; + _debugLogger?.Debug($"{nameof(InjectDotNetObjectToWebPageAsync)}: Native handle {native}"); + + string injectorScript + = @$" + var value = {native}; + var frame = Uno.UI.WindowManager.current.getView({_htmlId}); + var editorContext = EditorContext.getEditorForElement(frame); + editorContext.{name} = value + "; + + _debugLogger?.Debug($"{nameof(InjectDotNetObjectToWebPageAsync)}: {injectorScript}"); + + try + { + this.ExecuteJavascript(injectorScript); + } + catch (Exception e) + { + _errorLogger?.Error($"{nameof(InjectDotNetObjectToWebPageAsync)} failed", e); + } + + _debugLogger?.Debug($"{nameof(InjectDotNetObjectToWebPageAsync)}: '{name}' injected successfully."); + } + else + { + _errorLogger?.Error($"{nameof(InjectDotNetObjectToWebPageAsync)}: '{name}' is not a JSObject"); + throw new InvalidOperationException($"{nameof(InjectDotNetObjectToWebPageAsync)}: '{name}' is not a JSObject"); + } + + return Task.CompletedTask; } } diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs index b848f438c9..00486fddb8 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/CodeEditorPresenter.Windows.cs @@ -1,8 +1,19 @@ #if __WINDOWS__ +using System; +using System.Reflection; +using System.Reflection.Metadata; +using System.Text; +using System.Xml.Linq; +using CommunityToolkit.Common; +using DevToys.ComponentModel.Threading; +using DevToys.MonacoEditor.Threading; +using Microsoft.UI; using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; +using Newtonsoft.Json; using Windows.Foundation; +using Windows.Security.Cryptography.Core; using Windows.Storage; namespace DevToys.MonacoEditor; @@ -12,12 +23,41 @@ namespace DevToys.MonacoEditor; /// public sealed partial class CodeEditorPresenter : UserControl, ICodeEditorPresenter { + private static readonly List> Handlers = new(); + + private enum PropertyAction + { + Read = 0, + Write = 1, + } + + private struct WebMessage + { + public Guid Guid { get; set; } + } + + private struct MethodWebMessage + { + public string Id { get; set; } + public string Method { get; set; } + public string Args { get; set; } + } + + private struct PropertyWebMessage + { + public string Id { get; set; } + public string Property { get; set; } + public PropertyAction Action { get; set; } + public string Value { get; set; } + } + private readonly WebView2 _webView = new(); public CodeEditorPresenter() { Content = _webView; + _webView.DefaultBackgroundColor = Colors.Transparent; _webView.CoreWebView2Initialized += WebView_CoreWebView2Initialized; } @@ -33,6 +73,9 @@ public CodeEditorPresenter() /// public event TypedEventHandler? NavigationCompleted; + /// + public event AsyncTypedEventHandler? DotNetObjectInjectionRequested; + public async Task LaunchAsync() { await _webView.EnsureCoreWebView2Async(); @@ -48,7 +91,190 @@ StorageFile storageFile folderPath: rootDirectory, CoreWebView2HostResourceAccessKind.Allow); + var tcs = new TaskCompletionSource(); + _webView.CoreWebView2.DOMContentLoaded += OnDOMContentLoaded; _webView.Source = new Uri("https://devtoys.local/CodeEditor/CodeEditor.Windows.html"); + + await tcs.Task; + + Guard.IsNotNull(DotNetObjectInjectionRequested); + await DotNetObjectInjectionRequested.Invoke(this, EventArgs.Empty); + + await _webView.ExecuteScriptAsync("createMonacoEditor(\"https://devtoys.local/\", document.getElementById('container'));"); + + void OnDOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) + { + _webView.CoreWebView2.DOMContentLoaded -= OnDOMContentLoaded; + tcs.TrySetResult(); + } + } + + public async Task InjectDotNetObjectToWebPageAsync(string name, T pObject) + { + var sb = new StringBuilder(); + sb.AppendLine($"EditorContext.getEditorForElement(document.getElementById('container')).{name} = {{ "); + + var methodsGuid = Guid.NewGuid(); + MethodInfo[] methodInfo = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var methods = new Dictionary(methodInfo.Length); + foreach (MethodInfo method in methodInfo) + { + string functionName = $"{char.ToLower(method.Name[0])}{method.Name.Substring(1)}"; + sb.AppendLine($@"{functionName}: function() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{methodsGuid}"", id: this._callbackIndex++, method: ""{functionName}"", args: JSON.stringify([...arguments]) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); + methods.Add(functionName, method); + } + + var propertiesGuid = Guid.NewGuid(); + PropertyInfo[] propertyInfo = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var properties = new Dictionary(propertyInfo.Length); + foreach (PropertyInfo property in propertyInfo) + { + string propertyName = $"{char.ToLower(property.Name[0])}{property.Name.Substring(1)}"; + if (property.CanRead) + { + sb.AppendLine($@"get {propertyName}() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int)PropertyAction.Read}"" }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); + } + if (property.CanWrite) + { + sb.AppendLine($@"set {propertyName}(value) {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int)PropertyAction.Write}"", value: JSON.stringify(value) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); + + } + properties[propertyName] = property; + } + + // Add a map to the object used to resolve results + sb.AppendLine($@"_callbacks: new Map(),"); + // And a shared counter to index into that map + sb.Append($@"_callbackIndex: 0,"); + + sb.AppendLine("}"); + + // TODO: Logs + // Logger.Debug("Creating web object {WebObjectName}: {WebObjectScript}", name, sb.ToString()); + // Logger.Debug("Methods GUID: {MethodsGuid}", methodsGuid); + // Logger.Debug("Properties GUID: {PropertiesGuid}", propertiesGuid); + Debug.WriteLine($"Creating web object {name}: {sb}"); + Debug.WriteLine($"Methods GUID: {methodsGuid}"); + Debug.WriteLine($"Properties GUID: {propertiesGuid}"); + try + { + //await webview.ExecuteScriptAsync($"try {{ {sb} }} catch (ex) {{ console.error(ex); }}").AsTask(); + await _webView.ExecuteScriptAsync($"{sb}").AsTask(); + } + catch (Exception ex) + { + // So we can see it in the JS debugger + // TODO: Logs + // Logger.Error(ex, "Error installing web allowed object"); + Debug.WriteLine($"Error installing web allowed object: {ex.Message}"); + } + + var handler = (TypedEventHandler)(async (_, e) => + { + var webMessageAsString = e.TryGetWebMessageAsString(); + // TODO: Logs + // Logger.Information("Received web message {WebMessage}", e.WebMessageAsString); + Debug.WriteLine($"Received web message: {webMessageAsString}"); + + WebMessage message = JsonConvert.DeserializeObject(webMessageAsString); + if (message.Guid == methodsGuid) + { + MethodWebMessage methodMessage = JsonConvert.DeserializeObject(webMessageAsString); + MethodInfo method = methods[methodMessage.Method]; + try + { + object? result = method.Invoke(pObject, JsonConvert.DeserializeObject(methodMessage.Args)); + if (result is object) + { + Type resultType = result.GetType(); + dynamic? task = null; + if (resultType.Name.StartsWith("TaskToAsyncOperationAdapter") + || resultType.IsInstanceOfType(typeof(IAsyncInfo))) + { + // IAsyncOperation that needs to be converted to a task first + if (resultType.GenericTypeArguments.Length > 0) + { + MethodInfo asTask = typeof(WindowsRuntimeSystemExtensions) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(method => method.GetParameters().Length == 1 + && method.Name == "AsTask" + && method.ToString()!.Contains("Windows.Foundation.IAsyncOperation`1[TResult]")) + .First(); + + //var asTask = typeof(WindowsRuntimeSystemExtensions) + // .GetMethod(nameof(WindowsRuntimeSystemExtensions.AsTask), + // new[] { typeof(IAsyncOperation<>).MakeGenericType(resultType.GenericTypeArguments[0]) } + // ); + + asTask = asTask.MakeGenericMethod(resultType.GenericTypeArguments[0]); + task = (Task)asTask.Invoke(null, new[] { result })!; + } + else + { + task = WindowsRuntimeSystemExtensions.AsTask((dynamic)result); + } + } + else + { + MethodInfo? awaiter = resultType.GetMethod(nameof(Task.GetAwaiter)); + if (awaiter is object) + { + task = (Task)result; + } + } + if (task is object) + { + result = await task; + } + } + string json = JsonConvert.SerializeObject(result); + await _webView.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});"); + } + catch (Exception ex) + { + // TODO: Logs + // Logger.Error(ex, "Exception in {WebObjectName}.{WebObjectFunction}", name, method.Name); + Debug.WriteLine($"Exception in {name}.{method.Name}: {ex.Message}"); + string json = JsonConvert.SerializeObject(ex, new JsonSerializerSettings() { Error = (_, e) => e.ErrorContext.Handled = true }); + await _webView.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});"); + //throw; + } + } + else if (message.Guid == propertiesGuid) + { + PropertyWebMessage propertyMessage = JsonConvert.DeserializeObject(webMessageAsString); + PropertyInfo property = properties[propertyMessage.Property]; + try + { + object? result; + if (propertyMessage.Action == PropertyAction.Read) + { + result = property.GetValue(pObject); + } + else + { + object? value = JsonConvert.DeserializeObject(propertyMessage.Value, property.PropertyType); + property.SetValue(pObject, value); + result = new object(); + } + + string json = JsonConvert.SerializeObject(result); + await _webView.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});"); + } + catch (Exception ex) + { + // TODO: Logs + // Logger.Error(ex, "Exception in {WebObjectName}.{WebObjectProperty}", name, property.Name); + Debug.WriteLine($"Exception in {name}.{property.Name}: {ex.Message}"); + string json = JsonConvert.SerializeObject(ex, new JsonSerializerSettings() { Error = (_, e) => e.ErrorContext.Handled = true }); + //await webview.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});"); + //throw; + } + } + }); + + Handlers.Add(handler); + _webView.WebMessageReceived += handler; } private void WebView_CoreWebView2Initialized(WebView2 sender, CoreWebView2InitializedEventArgs args) diff --git a/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs b/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs index 31e6add81c..b18e1a5e0c 100644 --- a/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs +++ b/src/app/dev/DevToys.MonacoEditor/CodeEditor/ICodeEditorPresenter.cs @@ -1,4 +1,5 @@ -using Microsoft.Web.WebView2.Core; +using DevToys.MonacoEditor.Threading; +using Microsoft.Web.WebView2.Core; using Windows.Foundation; namespace DevToys.MonacoEditor; @@ -25,8 +26,21 @@ public interface ICodeEditorPresenter /// event TypedEventHandler? NavigationCompleted; + /// + /// Occurs when the presenter needs to get allowed-web object to be injected in the web page. + /// + event AsyncTypedEventHandler? DotNetObjectInjectionRequested; + /// /// Launch the web element of Monaco editor. /// Task LaunchAsync(); + + /// + /// Adds a native .NET object as a global parameter to the top level document inside of a WebView. + /// This way, the object named by the parameter can be used directly in JavaScript. + /// + /// The name of the object to expose to the document in the WebView. + /// The object to expose to the document in the WebView. + Task InjectDotNetObjectToWebPageAsync(string name, T pObject); } diff --git a/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj b/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj index 78bdcaf0dd..1882a93ec5 100644 --- a/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj +++ b/src/app/dev/DevToys.MonacoEditor/DevToys.MonacoEditor.csproj @@ -9,6 +9,16 @@ true + + false + true + + + + true + false + + true $(DefineConstants);__WINDOWS__ @@ -22,6 +32,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -44,38 +58,42 @@ + + + - - - + %(Filename).ts - - + + + + + + + + + + + + + - - - - DevToys.MonacoEditor.CodeEditor.Wasm.ts - - %(Filename) - - - - - + + + + + \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/Threading/AsyncTypedEventHandler.cs b/src/app/dev/DevToys.MonacoEditor/Threading/AsyncTypedEventHandler.cs new file mode 100644 index 0000000000..77df5c62e8 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/Threading/AsyncTypedEventHandler.cs @@ -0,0 +1,3 @@ +namespace DevToys.MonacoEditor.Threading; + +public delegate Task AsyncTypedEventHandler(TSender sender, TResult args); diff --git a/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.Wasm.cs b/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.Wasm.cs new file mode 100644 index 0000000000..24e6bdc44f --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.Wasm.cs @@ -0,0 +1,31 @@ +#if __WASM__ + +using Microsoft.Extensions.Logging; +using Uno.Extensions; +using Uno.Foundation.Interop; + +namespace DevToys.MonacoEditor.WebInterop; + +internal partial class DebugLogger : IJSObject +{ + private readonly ILogger? _debugLogger; + + internal DebugLogger() + { + Handle = JSObjectHandle.Create(this); + + ILogger logger = this.Log(); + _debugLogger = logger.IsEnabled(LogLevel.Debug) ? logger : null; + } + + /// + public JSObjectHandle Handle { get; } + + public void log(string message) + { + Log(message); + _debugLogger?.LogDebug(message); + } +} + +#endif diff --git a/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.cs b/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.cs new file mode 100644 index 0000000000..42971ce27b --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/WebInterop/DebugLogger.cs @@ -0,0 +1,12 @@ +using Windows.Foundation.Metadata; + +namespace DevToys.MonacoEditor.WebInterop; + +[AllowForWeb] +internal sealed partial class DebugLogger +{ + internal void Log(string message) + { + Debug.WriteLine(message); + } +} 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/Monaco.Helpers.ThemeAccessor.ts similarity index 96% rename from src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts rename to src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.ThemeAccessor.ts index a827cf0817..152f10a584 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Wasm/Monaco.Helpers.ThemeAccessor.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/Monaco.Helpers.ThemeAccessor.ts @@ -2,5 +2,5 @@ interface ThemeAccessor { getCurrentThemeName(returnId: string); getIsHighContrast(returnId: string); - } +} //} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/editorContext.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/editorContext.ts new file mode 100644 index 0000000000..5c24231c1d --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/editorContext.ts @@ -0,0 +1,39 @@ +/// + +class EditorContext { + static _editors: Map = new Map(); + + public static registerEditorForElement(element: any, editor: monaco.editor.IStandaloneCodeEditor): EditorContext { + var value = EditorContext.getEditorForElement(element); + value.editor = editor; + return value; + } + + public static getEditorForElement(element: any): EditorContext { + var context = EditorContext._editors.get(element); + + if (!context) { + context = new EditorContext(); + EditorContext._editors.set(element, context); + } + + return context; + } + + constructor() { + this.modifingSelection = false; + this.contexts = {}; + this.decorations = []; + } + + public Accessor: ParentAccessor; + public Keyboard: KeyboardListener; + public Theme: ThemeAccessor; + public Debug: DebugLogger; + + public editor: monaco.editor.IStandaloneCodeEditor; + public model: monaco.editor.ITextModel; + public contexts: { [index: string]: monaco.editor.IContextKey }; + public decorations: string[]; + public modifingSelection: boolean; // Supress updates to selection when making edits. +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/monacoInitializer.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/monacoInitializer.ts new file mode 100644 index 0000000000..05b828b681 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/monacoInitializer.ts @@ -0,0 +1,106 @@ +/// + +const createMonacoEditor = (basePath: string, element: any) => { + let debug = EditorContext.getEditorForElement(element).Debug; + 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"); + + (window).require.config({ paths: { 'vs': `${basePath}/monaco-editor/min/vs` } }); + (window).require(['vs/editor/editor.main'], function () { + initializeMonacoEditor(element) + }); +} + +const initializeMonacoEditor = (element: any) => { + let debug = EditorContext.getEditorForElement(element).Debug; + 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 */); + var editorContext = EditorContext.registerEditorForElement(element, editor); + + debug.log("Getting Editor model"); + //editorContext.model = editor.getModel(); + + //// 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/ts-helpermethods/otherScriptsToBeOrganized.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts index 9a52eef179..261a64788d 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/otherScriptsToBeOrganized.ts @@ -1,17 +1,11 @@ /// -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. +const registerHoverProvider = function (element: any, languageId: string) { + var editorContext = EditorContext.getEditorForElement(element); -var registerHoverProvider = function (languageId: string) { return monaco.languages.registerHoverProvider(languageId, { provideHover: function (model, position) { - return callParentEventAsync("HoverProvider" + languageId, [JSON.stringify(position)]).then(result => { + return callParentEventAsync(element, "HoverProvider" + languageId, [JSON.stringify(position)]).then(result => { if (result) { return JSON.parse(result); } @@ -20,96 +14,102 @@ var registerHoverProvider = function (languageId: string) { }); }; -var addAction = function (action: monaco.editor.IActionDescriptor) { +const addAction = function (element: any, action: monaco.editor.IActionDescriptor) { + var editorContext = EditorContext.getEditorForElement(element); + action.run = function (ed) { - Parent.callAction("Action" + action.id) + editorContext.Accessor.callAction("Action" + action.id) }; - editor.addAction(action); + editorContext.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]); - } - } +const addCommand = function (element: any, keybindingStr, handlerName, context) { + var editorContext = EditorContext.getEditorForElement(element); - return callParentActionWithParameters(handlerName, objs); - }, - context - ); + return editorContext.editor.addCommand(parseInt(keybindingStr), function () { + const 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(JSON.stringify(arguments[i])); + } + } + editorContext.Accessor.callActionWithParameters(handlerName, objs); + }, context); }; -var createContext = function (context) { +const createContext = function (element: any, context) { + var editorContext = EditorContext.getEditorForElement(element); + if (context) { - contexts[context.key] = editor.createContextKey(context.key, context.defaultValue); + editorContext.contexts[context.key] = editorContext.editor.createContextKey(context.key, context.defaultValue); } }; -var updateContext = function (key, value) { - contexts[key].set(value); +const updateContext = function (element: any, key, value) { + var editorContext = EditorContext.getEditorForElement(element); + + editorContext.contexts[key].set(value); } -var updateContent = function (content) { +// link:CodeEditor.Properties.cs:updateContent +const updateContent = function (element: any, content) { + var editorContext = EditorContext.getEditorForElement(element); + // Need to ignore updates from us notifying of a change - if (content != model.getValue()) { - model.setValue(content); + if (content !== editorContext.model.getValue()) { + editorContext.model.setValue(content); } }; +const updateDecorations = function (element: any, newHighlights) { + var editorContext = EditorContext.getEditorForElement(element); - - - - - -var updateDecorations = function (newHighlights) { if (newHighlights) { - decorations = editor.deltaDecorations(decorations, newHighlights); + editorContext.decorations = editorContext.editor.deltaDecorations(editorContext.decorations, newHighlights); } else { - decorations = editor.deltaDecorations(decorations, []); + editorContext.decorations = editorContext.editor.deltaDecorations(editorContext.decorations, []); } }; -var updateStyle = function (innerStyle) { +const updateStyle = function (innerStyle) { var style = document.getElementById("dynamic"); style.innerHTML = innerStyle; }; -var getOptions = function (): monaco.editor.IEditorOptions { +const getOptions = async function (element: any): Promise { + var editorContext = EditorContext.getEditorForElement(element); + let opt = null; try { - var jopt = getParentJsonValue("Options"); - // console.log('Options: ' + jopt); - opt = JSON.parse(jopt); + opt = getParentValue(element, "Options"); } finally { } - if (opt != null && typeof opt === "object") { + 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); +const updateOptions = function (element: any, opt: monaco.editor.IEditorOptions) { + var editorContext = EditorContext.getEditorForElement(element); + + if (opt !== null && typeof opt === "object") { + editorContext.editor.updateOptions(opt); } }; -var updateLanguage = function (language) { - monaco.editor.setModelLanguage(model, language); +const updateLanguage = function (element: any, language) { + var editorContext = EditorContext.getEditorForElement(element); + monaco.editor.setModelLanguage(editorContext.model, language); }; -var changeTheme = function (theme: string, highcontrast) { - var newTheme = 'vs'; +const changeTheme = function (element: any, theme: string, highcontrast) { + var editorContext = EditorContext.getEditorForElement(element); + let newTheme = 'vs'; if (highcontrast == "True" || highcontrast == "true") { newTheme = 'hc-black'; } else if (theme == "Dark") { @@ -121,9 +121,10 @@ var changeTheme = function (theme: string, highcontrast) { -var keyDown = function (event) { +const keyDown = async function (element: any, event) { + var editorContext = EditorContext.getEditorForElement(element); //Debug.log("Key Down:" + event.keyCode + " " + event.ctrlKey); - var result = Keyboard.keyDown(event.keyCode, event.ctrlKey, event.shiftKey, event.altKey, event.metaKey); + const result = await editorContext.Keyboard.keyDown(event.keyCode, event.ctrlKey, event.shiftKey, event.altKey, event.metaKey); if (result) { event.cancelBubble = true; event.preventDefault(); diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeActionProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeActionProvider.ts new file mode 100644 index 0000000000..0a4b6ab244 --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeActionProvider.ts @@ -0,0 +1,40 @@ +/// + +function isTextEdit(edit: monaco.languages.IWorkspaceTextEdit | monaco.languages.IWorkspaceFileEdit): edit is monaco.languages.IWorkspaceTextEdit { + return (edit as monaco.languages.IWorkspaceTextEdit).textEdit !== undefined; +} + +const registerCodeActionProvider = function (element: any, languageId) { + var editorContext = EditorContext.getEditorForElement(element); + + return monaco.languages.registerCodeActionProvider(languageId, { + provideCodeActions: function (model, range, context, token) { + return callParentEventAsync(element, "ProvideCodeActions" + languageId, [JSON.stringify(range), JSON.stringify(context)]).then(result => { + if (result) { + const list: monaco.languages.CodeActionList = JSON.parse(result); + + // Need to add in the model.uri to any edits to connect the dots + if (list.actions && + list.actions.length > 0) { + list.actions.forEach((action) => { + if (action.edit && + action.edit.edits && + action.edit.edits.length > 0) { + action.edit.edits.forEach((inneredit) => { + if (isTextEdit(inneredit)) { + inneredit.resource = model.uri; + } + }); + } + }); + } + + // Add dispose method for IDisposable that Monaco is looking for. + list.dispose = () => { }; + + return list; + } + }); + }, + }); +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts index 2c0fc6ae42..ea8359ba44 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCodeLensProvider.ts @@ -1,19 +1,25 @@ /// -declare var Parent: ParentAccessor; -var registerCodeLensProvider = function (languageId) { +const registerCodeLensProvider = function (element: any, languageId) { + var editorContext = EditorContext.getEditorForElement(element); + return monaco.languages.registerCodeLensProvider(languageId, { provideCodeLenses: function (model, token) { - return callParentEventAsync("ProvideCodeLenses" + languageId, null).then(result => { + return callParentEventAsync(element, "ProvideCodeLenses" + languageId, []).then(result => { if (result) { - return JSON.parse(result); + const list: monaco.languages.CodeLensList = JSON.parse(result); + + // Add dispose method for IDisposable that Monaco is looking for. + list.dispose = () => {}; + + return list; } return null; }); }, resolveCodeLens: function (model, codeLens, token) { - return callParentEventAsync("ResolveCodeLens" + languageId, [JSON.stringify(codeLens)]).then(result => { + return callParentEventAsync(element, "ResolveCodeLens" + languageId, [JSON.stringify(codeLens)]).then(result => { if (result) { return JSON.parse(result); } diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts index a85a7648f0..24bb3fb848 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerColorProvider.ts @@ -1,17 +1,18 @@ /// -declare var Parent: ParentAccessor; -var registerColorProvider = function (languageId) { +const registerColorProvider = function (element: any, languageId) { + var editorContext = EditorContext.getEditorForElement(element); + return monaco.languages.registerColorProvider(languageId, { provideColorPresentations: function (model, colorInfo, token) { - return callParentEventAsync("ProvideColorPresentations" + languageId, [JSON.stringify(colorInfo)]).then(result => { + return callParentEventAsync(element, "ProvideColorPresentations" + languageId, [JSON.stringify(colorInfo)]).then(result => { if (result) { return JSON.parse(result); } }); }, provideDocumentColors: function (model, token) { - return callParentEventAsync("ProvideDocumentColors" + languageId, []).then(result => { + return callParentEventAsync(element, "ProvideDocumentColors" + languageId, []).then(result => { if (result) { return JSON.parse(result); } diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts index d10aa849a4..7df0d66358 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/registerCompletionItemProvider.ts @@ -1,23 +1,28 @@ /// -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 => { +const registerCompletionItemProvider = function (element: any, languageId, characters) { + var editorContext = EditorContext.getEditorForElement(element); + + return monaco.languages.registerCompletionItemProvider(languageId, { + triggerCharacters: characters, + provideCompletionItems: function (model, position, context, token) { + return callParentEventAsync(element, "CompletionItemProvider" + languageId, [JSON.stringify(position), JSON.stringify(context)]).then(result => { if (result) { - return JSON.parse(result); + const list: monaco.languages.CompletionList = JSON.parse(result); + + // Add dispose method for IDisposable that Monaco is looking for. + list.dispose = () => { }; + + return list; } - return null; - }), - resolveCompletionItem: (item, token) => callParentEventAsync("CompletionItemRequested" + languageId, - [JSON.stringify(item)]).then(result => { + }); + }, + resolveCompletionItem: function (item, token) { + return callParentEventAsync(element, "CompletionItemRequested" + languageId, [JSON.stringify(item)]).then(result => { if (result) { return JSON.parse(result); } - return null; - }) - }); + }); + } + }); +} \ No newline at end of file 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 index 4e672df567..cae137bf12 100644 --- 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 @@ -1,15 +1,18 @@ //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); - } +interface ParentAccessor { + callAction(name: string): boolean; + callActionWithParameters(name: string, parameters: string[]): boolean; + callEvent(name: string, parameters: string[]): Promise + close(); + getChildValue(name: string, child: string): Promise; + getJsonValue(name: string): Promise; + getValue(name: string): Promise; + setValue(name: string, value: any): Promise; + setValue(name: string, value: string, type: string): Promise; + setValueWithType(name: string, value: string, type: string); + callActionWithParameters(name: string, parameter1: string, parameter2: string): boolean; + callEvent(name: string, callbackMethod: string, parameter1: string, parameter2: string); + getJsonValue(name: string, 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 index 6f05e51791..42c2aaed74 100644 --- 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 @@ -29,10 +29,10 @@ const returnValueCallback = (returnId: string, returnValue: string) => { } const invokeAsyncMethod = (syncMethod: NumberCallback): Promise => { - if (nextAsync==null) { + if (nextAsync == null) { nextAsync = 0; } - if (asyncCallbackMap==null) { + if (asyncCallbackMap == null) { asyncCallbackMap = {}; } const promise = new Promise((resolve, reject) => { @@ -44,8 +44,7 @@ const invokeAsyncMethod = (syncMethod: NumberCallback): Promise => { } const replaceAll = (str: string, find: string, rep: string): string => { - if (find == "\\") - { + if (find == "\\") { find = "\\\\"; } return (`${str}`).replace(new RegExp(find, "g"), rep); @@ -70,8 +69,7 @@ const desantize = (parameter: string): string => { if (parameter == null) return parameter; const replacements = "&\\\"'{}:,%"; //System.Diagnostics.Debug.WriteLine($"Replacements: >{replacements}<"); - for (let i = 0; i < replacements.length; i++) - { + 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)); } @@ -91,34 +89,34 @@ const invokeWithReturnValue = (methodToInvoke: MethodWithReturnId): string => { return json; } -const getParentValue = (name: string): any => { - const jsonString = invokeWithReturnValue((returnId) => Parent.getJsonValue(name, returnId)); +const getParentValue = (element: any, name: string): any => { + const jsonString = invokeWithReturnValue((returnId) => EditorContext.getEditorForElement(element).Accessor.getJsonValue(name, returnId)); const obj = JSON.parse(jsonString); return obj; } -const getParentJsonValue = (name: string): string => - invokeWithReturnValue((returnId) => Parent.getJsonValue(name, returnId)) +const getParentJsonValue = (element: any, name: string): string => + invokeWithReturnValue((returnId) => EditorContext.getEditorForElement(element).Accessor.getJsonValue(name, returnId)) -const getThemeIsHighContrast = (): boolean => - invokeWithReturnValue((returnId) => Theme.getIsHighContrast(returnId)) == "true"; +const getThemeIsHighContrast = (element: any): boolean => + invokeWithReturnValue((returnId) => EditorContext.getEditorForElement(element).Theme.getIsHighContrast(returnId)) == "true"; -const getThemeCurrentThemeName = (): string => - invokeWithReturnValue((returnId) => Theme.getCurrentThemeName(returnId)); +const getThemeCurrentThemeName = (element: any): string => + invokeWithReturnValue((returnId) => EditorContext.getEditorForElement(element).Theme.getCurrentThemeName(returnId)); -const callParentEventAsync = (name: string, parameters: string[]): Promise => +const callParentEventAsync = (element: any, name: string, parameters: string[]): Promise => invokeAsyncMethod(async (promiseId) => { - let result = await Parent.callEvent(name, + let result = await EditorContext.getEditorForElement(element).Accessor.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); + console.log('Parent event result: ' + name + ' - ' + result); result = desantize(result); - //console.log('Desanitized: ' + name + ' - ' + result); + console.log('Desanitized: ' + name + ' - ' + result); } else { - //console.log('No Parent event result for ' + name); + console.log('No Parent event result for ' + name); } return result; 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 index d7cbf4076e..ba7a481f7a 100644 --- 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 @@ -5,13 +5,13 @@ "target": "ES2015", "lib": [ "ES2015", "DOM" ], "sourceMap": false, - "outFile": "..\\WasmScripts\\uno-monaco-helpers.js" + "outFile": "..\\..\\WasmScripts\\uno-monaco-helpers.js" }, "include": [ ".\\", - "..\\ts-helpermethods" + "..\\..\\ts-helpermethods" ], "exclude": [ - "..\\ts-helpermethods\\ts-helpermethods-uwp" + "..\\..\\ts-helpermethods\\ts-helpermethods-Windows" ] } \ 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 index 3082610cb4..062fa6106a 100644 --- 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 @@ -1,14 +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); - } +interface ParentAccessor { + callAction(name: string): boolean; + callActionWithParameters(name: string, parameters: string[]): boolean; + callEvent(name: string, parameters: string[]): Promise + close(); + getChildValue(name: string, child: string): Promise; + getJsonValue(name: string): Promise; + getValue(name: string): Promise; + setValue(name: string, value: any): Promise; + setValue(name: string, value: string, type: string): Promise; +} //} \ 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 index 32bddd1344..1a11bf2926 100644 --- 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 @@ -1,14 +1,17 @@ -var callParentEventAsync = function (name: string, parameters: string[]): Promise { +declare var Parent: ParentAccessor; + +var callParentEventAsync = function (element: any, name: string, parameters: string[]): Promise { return Parent.callEvent(name, parameters); } -var callParentActionWithParameters = function (name: string, parameters: string[]): boolean { +var callParentActionWithParameters = function (element: any, name: string, parameters: string[]): boolean { return Parent.callActionWithParameters(name, parameters); } -var getParentJsonValue = function (name: string): string { +var getParentJsonValue = function (element: any, name: string): Promise { return Parent.getJsonValue(name); } - - +var getParentValue = function (element: any, name: string): Promise { + return Parent.getJsonValue(name); +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/tsconfig.json b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/tsconfig.json new file mode 100644 index 0000000000..8de032db5e --- /dev/null +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/ts-helpermethods-Windows/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "ES2015", + "sourceMap": false + }, + "include": [ + ".\\", + "..\\..\\ts-helpermethods" + ], + "exclude": [ + "..\\..\\ts-helpermethods\\ts-helpermethods-Wasm" + ] +} \ No newline at end of file diff --git a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json deleted file mode 100644 index 6c7547f82c..0000000000 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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 index e742a81aa4..8635d81748 100644 --- a/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts +++ b/src/app/dev/DevToys.MonacoEditor/ts-helpermethods/updateSelectedContent.ts @@ -1,37 +1,32 @@ /// -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. +// link:CodeEditor.Properties.cs:updateSelectedContent +const updateSelectedContent = function (element: any, content) { + var editorContext = EditorContext.getEditorForElement(element); -var updateSelectedContent = function (content) { - let selection = editor.getSelection(); + let selection = editorContext.editor.getSelection(); // Need to ignore updates from us notifying of a change - if (content != model.getValueInRange(selection)) { - modifingSelection = true; + if (content != editorContext.model.getValueInRange(selection)) { + editorContext.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); + editorContext.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) + const newEndLineNumber = selection.startLineNumber + content.split('\r').length - 1; // TODO: Not sure if line end is situational/platform specific... investigate more. + const 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) { @@ -42,7 +37,7 @@ var updateSelectedContent = function (content) { // selection.selectionStartLineNumber = selection.endLineNumber; //} - modifingSelection = false; - editor.setSelection(selection); + editorContext.modifingSelection = false; + editorContext.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 6b19c40dbd..e182b7b5fe 100644 --- a/src/app/dev/DevToys.UI/App.xaml.cs +++ b/src/app/dev/DevToys.UI/App.xaml.cs @@ -147,6 +147,8 @@ private static void InitializeLogging() if (Debugger.IsAttached) { + builder.SetMinimumLevel(LogLevel.Debug); + // Generic Xaml events builder.AddFilter("Windows.UI.Xaml", LogLevel.Debug); builder.AddFilter("Windows.UI.Xaml.VisualStateGroup", LogLevel.Debug); diff --git a/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs b/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs index 395972bdda..41ebf614be 100644 --- a/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs +++ b/src/app/dev/DevToys.UI/Core/Threading/TaskExtension.cs @@ -44,6 +44,14 @@ internal static async void ForgetSafely(this Task task, Action? error } } + /// + /// Gets the result of the task synchronously, on the current thread. + /// + internal static void CompleteOnCurrentThread(this Task task) + { + task.GetAwaiter().GetResult(); + } + /// /// Gets the result of the task synchronously, on the current thread. /// diff --git a/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj b/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj index 940da7c32a..6d9065fa15 100644 --- a/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj +++ b/src/app/dev/platforms/DevToys.Wasm/DevToys.Wasm.csproj @@ -43,13 +43,6 @@ - - -