瀏覽代碼

Refactor editor init

bdbai 2 年之前
父節點
當前提交
751a3fa859

+ 1 - 2
Maple.App/App.cpp

@@ -122,7 +122,7 @@ fire_and_forget App::OnSuspending([[maybe_unused]] IInspectable const& sender, [
     // Save application state and stop any background activity
     const auto& saveModifiedContent = EditPage::SaveModifiedContent;
     const auto& saveMonacoModifiedContent = MonacoEditPage::SaveModifiedContent;
-    if (saveModifiedContent == nullptr && saveMonacoModifiedContent) {
+    if (saveModifiedContent == nullptr && saveMonacoModifiedContent == nullptr) {
         co_return;
     }
 
@@ -133,7 +133,6 @@ fire_and_forget App::OnSuspending([[maybe_unused]] IInspectable const& sender, [
     if (saveMonacoModifiedContent != nullptr)
     {
         co_await saveMonacoModifiedContent();
-        co_await 1500ms;
     }
     def.Complete();
 }

+ 2 - 2
Maple.App/MainPage.cpp

@@ -1,4 +1,4 @@
-#include "pch.h"
+#include "pch.h"
 #include "MainPage.h"
 #include "MainPage.g.cpp"
 #include <filesystem>
@@ -358,7 +358,7 @@ namespace winrt::Maple_App::implementation
         }
         if (targetPage.Name != xaml_typename<MonacoEditPage>().Name)
         {
-        MainContentFrame().BackStack().Clear();
+            MainContentFrame().BackStack().Clear();
         }
         MainContentFrame().Navigate(targetPage, item);
     }

+ 6 - 2
Maple.App/Maple.App.vcxproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.220929.3\build\native\Microsoft.Windows.CppWinRT.props')" />
   <Import Project="..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props" Condition="Exists('..\packages\Microsoft.UI.Xaml.2.8.1\build\native\Microsoft.UI.Xaml.props')" />
@@ -363,7 +363,11 @@
       <DestinationFolders>$(OutDir)/Config</DestinationFolders>
     </CopyFileToFolders>
     <None Include="Maple.App_TemporaryKey.pfx" />
-    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\*" />
+    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\editor*.js" />
+    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\editor.html" />
+    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\json*.js" />
+    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\*.css" />
+    <_WildCardCopyFileToFolders Include="MonacoEditor\dist\*.ttf" />
     <None Include="packages.config" />
     <None Include="PropertySheet.props" />
   </ItemGroup>

+ 72 - 37
Maple.App/MonacoEditPage.cpp

@@ -30,13 +30,12 @@ namespace winrt::Maple_App::implementation
     IAsyncAction MonacoEditPage::initializeWebView() {
         auto const lifetime{ get_strong() };
         auto const webview = WebView();
-        co_await webview.EnsureCoreWebView2Async();
-        if (std::exchange(m_webviewInitialized, true))
+        if (m_webviewState != MonacoEditPageWebViewState::Uninitialized)
         {
             co_return;
         }
-        auto const packagePath = Windows::ApplicationModel::Package::Current().InstalledPath();
-        auto const configPath = Windows::Storage::ApplicationData::Current().LocalFolder().Path() + L"\\config";
+        m_webviewState = MonacoEditPageWebViewState::AwaitingEditorReady;
+        co_await webview.EnsureCoreWebView2Async();
         webview.CoreWebView2().SetVirtualHostNameToFolderMapping(
             L"maple-monaco-editor-app-root.com",
             packagePath,
@@ -53,40 +52,29 @@ namespace winrt::Maple_App::implementation
     fire_and_forget MonacoEditPage::OnNavigatedTo(NavigationEventArgs const& e) {
         auto const lifetime{ get_strong() };
         auto const param = e.Parameter().as<Maple_App::ConfigViewModel>();
-        co_await initializeWebView();
-        SaveModifiedContent = [weak = weak_ref{ lifetime }]()->IAsyncAction
+        hstring fileName;
+        try
+        {
+            fileName = param.File().Name();
+        }
+        catch (...) {}
+        if (fileName == L"")
         {
-            if (auto const lifetime{ weak.get() })
-            {
-                lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.triggerSave()" });
-            }
             co_return;
-        };
-
-        if (m_webviewDOMLoaded)
+        }
+        m_currentFileName = fileName;
+        switch (m_webviewState)
         {
+        case MonacoEditPageWebViewState::Uninitialized:
+            co_await initializeWebView();
+            break;
+        case MonacoEditPageWebViewState::AwaitingEditorReady:
+            break;
+        case MonacoEditPageWebViewState::EditorReady:
             co_await lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`http://maple-monaco-editor-config-root.com/" } +
-                param.File().Name() +
+                fileName +
                 L"`)");
-        }
-        else {
-            if (!m_webviewDOMLoadedToken) {
-                m_webviewDOMLoadedToken = WebView().CoreWebView2().DOMContentLoaded([=](auto, auto)
-                    {
-                        lifetime->m_webviewDOMLoaded = true;
-                        lifetime->WebView().CoreWebView2().DOMContentLoaded(lifetime->m_webviewDOMLoadedToken);
-                        lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`" } +
-                            CONFIG_ROOT_VIRTUAL_HOSTW +
-                            lifetime->m_initialFileName +
-                            L"`)");
-                        lifetime->WebView().WebMessageReceived([](auto const&, auto const& args)
-                            {
-                                auto const json{ args.WebMessageAsJson() };
-                                handleEvent(json);
-                            });
-                    });
-            }
-            m_initialFileName = param.File().Name();
+            break;
         }
     }
 
@@ -98,15 +86,55 @@ namespace winrt::Maple_App::implementation
         co_return;
     }
 
-    IAsyncAction MonacoEditPage::handleEvent(hstring const& eventJson)
+    fire_and_forget MonacoEditPage::WebView_WebMessageReceived(
+        MUXC::WebView2 const& sender,
+        CoreWebView2WebMessageReceivedEventArgs const& args
+    )
     {
+        auto const lifetime{ get_strong() };
         nlohmann::json doc;
+        bool hasError{};
         try {
-            doc = nlohmann::json::parse(to_string(eventJson));
+            doc = nlohmann::json::parse(to_string(args.WebMessageAsJson()));
         }
-        catch (...) {}
+        catch (...)
+        {
+            hasError = true;
+        }
+        if (hasError)
+        {
+            co_return;
+        }
+
+        std::string const& cmd = doc["cmd"];
+        if (cmd == "editorReady")
+        {
+            m_webviewState = MonacoEditPageWebViewState::EditorReady;
+            co_await WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.loadFile(`" } +
+                CONFIG_ROOT_VIRTUAL_HOSTW +
+                m_currentFileName +
+                L"`)");
+            SaveModifiedContent = [weak = weak_ref{ lifetime }]()->IAsyncAction
+            {
+                if (auto const lifetime{ weak.get() })
+                {
+                    struct awaiter : std::suspend_always
+                    {
+                        void await_suspend(
+                            std::coroutine_handle<> handle)
+                        {
+                            lifetime->m_fileSaveHandle = handle;
+                        }
 
-        if (doc["cmd"] == "save")
+                        com_ptr<MonacoEditPage> lifetime;
+                    };
+                    lifetime->WebView().ExecuteScriptAsync(hstring{ L"window.mapleHostApi.requestSaveCurrent()" });
+                    co_await awaiter{ .lifetime = lifetime };
+                }
+                co_return;
+            };
+        }
+        else if (cmd == "save")
         {
             try {
                 auto const configDir{ co_await ApplicationData::Current().LocalFolder().CreateFolderAsync(L"config", CreationCollisionOption::OpenIfExists) };
@@ -124,7 +152,14 @@ namespace winrt::Maple_App::implementation
                     });
             }
             catch (...) {}
+            co_await resume_foreground(Dispatcher());
+            if (auto const fileSaveHandle{ std::exchange(m_fileSaveHandle, nullptr) }) {
+                fileSaveHandle();
+            }
         }
         co_return;
+
     }
+
 }
+

+ 17 - 7
Maple.App/MonacoEditPage.h

@@ -7,11 +7,19 @@
 using namespace winrt::Windows::Foundation;
 using namespace winrt::Windows::UI::Xaml;
 using namespace winrt::Windows::UI::Xaml::Navigation;
+namespace MUXC = winrt::Microsoft::UI::Xaml::Controls;
+using winrt::Microsoft::Web::WebView2::Core::CoreWebView2WebMessageReceivedEventArgs;
 
 namespace winrt::Maple_App::implementation
 {
-    constexpr wchar_t const *CONFIG_ROOT_VIRTUAL_HOSTW = L"http://maple-monaco-editor-config-root.com/";
-    constexpr char const *CONFIG_ROOT_VIRTUAL_HOST = "http://maple-monaco-editor-config-root.com/";
+    constexpr wchar_t const* CONFIG_ROOT_VIRTUAL_HOSTW = L"http://maple-monaco-editor-config-root.com/";
+    constexpr char const* CONFIG_ROOT_VIRTUAL_HOST = "http://maple-monaco-editor-config-root.com/";
+    enum class MonacoEditPageWebViewState
+    {
+        Uninitialized,
+        AwaitingEditorReady,
+        EditorReady,
+    };
     struct MonacoEditPage : MonacoEditPageT<MonacoEditPage>
     {
         MonacoEditPage();
@@ -20,16 +28,18 @@ namespace winrt::Maple_App::implementation
         fire_and_forget OnNavigatedFrom(NavigationEventArgs const& e);
         fire_and_forget Page_Loaded(IInspectable const& sender, RoutedEventArgs const& e);
         void Page_Unloaded(IInspectable const& sender, RoutedEventArgs const& e);
+        fire_and_forget WebView_WebMessageReceived(MUXC::WebView2 const& sender, CoreWebView2WebMessageReceivedEventArgs const& args);
 
         inline static std::function<IAsyncAction()> SaveModifiedContent{ nullptr };
     private:
-        static IAsyncAction handleEvent(hstring const& eventJson);
+        auto const static inline packagePath = Windows::ApplicationModel::Package::Current().InstalledPath();
+        auto const static inline configPath = Windows::Storage::ApplicationData::Current().LocalFolder().Path() + L"\\config";
+
         IAsyncAction initializeWebView();
 
-        bool m_webviewInitialized{};
-        bool m_webviewDOMLoaded{};
-        event_token m_webviewDOMLoadedToken{};
-        hstring m_initialFileName{};
+        MonacoEditPageWebViewState m_webviewState{ MonacoEditPageWebViewState::Uninitialized };
+        std::coroutine_handle<> m_fileSaveHandle{ nullptr };
+        hstring m_currentFileName{};
     };
 }
 

+ 1 - 1
Maple.App/MonacoEditPage.xaml

@@ -12,6 +12,6 @@
     NavigationCacheMode="Required">
 
     <Grid>
-        <muxc:WebView2 x:Name="WebView" />
+        <muxc:WebView2 x:Name="WebView" WebMessageReceived="WebView_WebMessageReceived" />
     </Grid>
 </Page>

+ 3 - 2
Maple.App/MonacoEditor/src/editor.html

@@ -19,7 +19,8 @@
 
 </style>
 
-<p id="loading">Loading...</p>
-<div id="container" style="width:100%;height:100%;border:1px solid grey"></div>
+<div id="container" style="width:100%;height:100%;border:1px solid grey">
+    <p id="loading">Loading...</p>
+</div>
 
 <script type="module" src="./init.ts"></script>

+ 52 - 59
Maple.App/MonacoEditor/src/init.ts

@@ -6,13 +6,14 @@ import { validateModel } from './validate'
 import { definitionProvider } from './definition'
 import { referenceProvider, renameProvider } from './reference'
 import { hoverProvider } from './hover'
+import { reportEditorReady, saveFile } from './rpc'
+import { Mutex } from './mutex'
 
 declare global {
     interface Window {
-        pendingLoadFile: string | undefined
         mapleHostApi: {
-            loadFile: (path: string) => void
-            triggerSave: () => void
+            loadFile: (url: string) => void
+            requestSaveCurrent: () => void
         }
         MonacoEnvironment?: monaco.Environment | undefined,
         chrome: {
@@ -23,18 +24,6 @@ declare global {
     }
 }
 
-function webviewInitPrelude() {
-    window.pendingLoadFile = undefined;
-    window.mapleHostApi = {
-        loadFile(url) {
-            console.log('pending loader triggered')
-            window.pendingLoadFile = url
-        },
-        triggerSave() { }
-    }
-    console.log('pending loader injected')
-}
-
 function initLang() {
     self.MonacoEnvironment = {
         getWorkerUrl: function (moduleId, label) {
@@ -58,7 +47,7 @@ function initLang() {
     const editor = monaco.editor.create(document.getElementById('container')!, {
         wordBasedSuggestions: false,
     })
-    const editorStates = new Map()
+    const editorStates: Map<string, monaco.editor.ICodeEditorViewState> = new Map()
 
     window.addEventListener('resize', _ => {
         window.requestAnimationFrame(() => {
@@ -66,72 +55,76 @@ function initLang() {
         })
     })
 
-    let currentFile = ''
-    async function loadFile(url) {
-        triggerSave()
+    let currentFileLock = new Mutex('')
+    async function loadFile(url: string) {
+        requestSaveCurrent()
+        await currentFileLock.withLock((currentFile, setCurrentFile) => {
+            return loadFileInner(url, currentFile, setCurrentFile)
+        })
+    }
+    async function loadFileInner(url: string, currentFile: string, setCurrentFile: (f: string) => void) {
         if (currentFile) {
-            editorStates.set(currentFile, editor.saveViewState())
+            const state = editor.saveViewState()
+            if (state) {
+                editorStates.set(currentFile, state)
+            }
         }
 
         // FIXME: destroy previous model?
         const parsedUri = monaco.Uri.parse(url)
         let maybeModel = monaco.editor.getModel(parsedUri)
-        do {
-            if (maybeModel) {
-                editor.setModel(maybeModel)
-                editor.restoreViewState(editorStates.get(url))
-            } else {
-                const res = await fetch(url)
-                const text = await res.text()
-                // Race condition
-                maybeModel = monaco.editor.getModel(parsedUri)
-                if (maybeModel) {
-                    continue
-                }
+        if (maybeModel) {
+            editor.setModel(maybeModel)
+            const state = editorStates.get(url)
+            if (state) {
+                editor.restoreViewState(state)
+            }
+        } else {
+            const res = await fetch(url)
+            const text = await res.text()
 
-                let model: monaco.editor.ITextModel
-                if (url.endsWith('.json')) {
-                    model = monaco.editor.createModel(text, 'json', parsedUri)
-                } else {
-                    model = monaco.editor.createModel(text, 'leafConf', parsedUri)
-                }
-                editor.setModel(model)
+            let model: monaco.editor.ITextModel
+            if (url.endsWith('.json')) {
+                model = monaco.editor.createModel(text, 'json', parsedUri)
+            } else {
+                model = monaco.editor.createModel(text, 'leafConf', parsedUri)
                 validateModel(model)
                 model.onDidChangeContent(() => validateModel(model))
-                console.log('loaded url: ' + url)
             }
-        } while (false)
-        currentFile = url
+            editor.setModel(model)
+            console.log('loaded url: ' + url)
+        }
+        setCurrentFile(url)
+    }
+    function getCurrent(): Promise<{ url: string, text: string } | undefined> {
+        return currentFileLock.withLock((currentFile, _) => {
+            if (!currentFile) {
+                return
+            }
+            const text = editor.getValue()
+            return { url: currentFile, text }
+        })
     }
-    function triggerSave() {
-        if (!currentFile) {
+    async function requestSaveCurrent() {
+        const current = await getCurrent()
+        if (current === undefined) {
             return
         }
-        const text = editor.getValue()
-
-        window.chrome.webview.postMessage({
-            cmd: 'save',
-            path: currentFile,
-            text,
-        })
+        saveFile(current.url, current.text)
     }
-    window.mapleHostApi.loadFile = loadFile
-    window.mapleHostApi.triggerSave = triggerSave
+    window.mapleHostApi = { loadFile, requestSaveCurrent }
 
-    if (window.pendingLoadFile) {
-        loadFile(window.pendingLoadFile)
-    }
     window.addEventListener('focusout', () => {
-        triggerSave()
+        requestSaveCurrent()
     })
 
     editor.addAction({
         id: 'save-action',
         label: 'Save Current File',
         keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
-        run: () => void triggerSave(),
+        run: () => requestSaveCurrent(),
     })
 }
 
-webviewInitPrelude()
 initLang()
+reportEditorReady()

+ 48 - 0
Maple.App/MonacoEditor/src/mutex.ts

@@ -0,0 +1,48 @@
+type Queue = (() => void)[]
+type Setter<T> = (t: T) => void
+
+export class Mutex<T> {
+    queue: Queue | undefined = undefined
+    constructor(public data: T) { }
+    lock(): Promise<MutexGuard<T>> {
+        if (this.queue === undefined) {
+            this.queue = []
+            return Promise.resolve(new MutexGuard(this))
+        }
+        const q = this.queue
+        return new Promise((resolve, _reject) => {
+            q.push(() => resolve(new MutexGuard(this)))
+        })
+    }
+    async withLock<U>(f: (data: T, setter: Setter<T>) => U): Promise<Awaited<U>> {
+        const guard = await this.lock()
+        try {
+            const ret = await f(guard.data, v => void (guard.data = v))
+            return ret
+        } finally {
+            guard.unlock()
+        }
+    }
+}
+
+export class MutexGuard<T> {
+    constructor(private m: Mutex<T>) { }
+    get data() {
+        return this.m.data
+    }
+    set data(val) {
+        this.m.data = val
+    }
+    unlock(): void {
+        const q = this.m.queue
+        if (q === undefined) {
+            throw new Error('unlocking unlocked mutex')
+        }
+        const next = q.shift()
+        if (next === undefined) {
+            this.m.queue = undefined
+        } else {
+            next()
+        }
+    }
+}

+ 13 - 0
Maple.App/MonacoEditor/src/rpc.ts

@@ -0,0 +1,13 @@
+export function reportEditorReady() {
+    window.chrome.webview.postMessage({
+        cmd: 'editorReady',
+    })
+}
+
+export function saveFile(url: string, text: string) {
+    window.chrome.webview.postMessage({
+        cmd: 'save',
+        path: url,
+        text,
+    })
+}