XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.UI.Dispatching;
using Microsoft.Web.WebView2.Core;
using XFEExtension.NetCore.WinUIHelper.Interface.Services;
using XFEExtension.NetCore.WinUIHelper.Utilities;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Infrastructure.Compilation;

namespace XFEWorkflow.Controls;

public sealed class CodeEditor : UserControl, IDisposable
{
    private readonly WebView2 web = new();
    private readonly IWebViewService service = ServiceManager.GetService<IWebViewService>();
    private readonly CodeCompiler compiler = new();
    private readonly DispatcherQueueTimer analysisTimer;
    private long analysisVersion;
    private bool analysisEnabled, analyzing, disposed;
    private Task analysisTask = Task.CompletedTask;
    private string text = "";
    private string documentId = Guid.NewGuid().ToString("N");
    private bool ready, initialized;
    private double fontSize = 14;
    private readonly TaskCompletionSource loadedEditor = new(TaskCreationOptions.RunContinuationsAsynchronously);
    internal Task Ready => loadedEditor.Task;
    internal bool IsAnalyzing => analyzing;
    internal Task AnalysisTask => analysisTask;
    internal Task<string> ProbeAsync(string script) => web.ExecuteScriptAsync(script).AsTask();
    internal Task CaptureAsync(string path) => WebViewCapture.SaveAsync(web.CoreWebView2, path, fromSurface: true);
    public bool IsNodeBody { get; set; }
    private bool readOnly;
    public bool IsReadOnly { get => readOnly; set { readOnly = value; SendDocument(); } }
    public string FileName { get; set; } = "workflow.cs";
    public double EditorFontSize => fontSize;
    private IEnumerable<CapabilityDescriptor> capabilities = [];
    public IEnumerable<CapabilityDescriptor> Capabilities { get => capabilities; set { capabilities = value.ToArray(); compiler.CapabilityBindingsSource = PluginMethodBindings.Generate(capabilities); } }
    public event Action<string>? TextChanged;
    public event Action<string>? EditorCommandInvoked;
    public event Action<IReadOnlyList<WorkflowDiagnostic>>? DiagnosticsChanged;
    public bool IsAnalysisEnabled
    {
        get => analysisEnabled;
        set
        {
            if (analysisEnabled == value || disposed) return;
            analysisEnabled = value;
            QueueAnalysis();
        }
    }
    public CodeEditor()
    {
        analysisTimer = DispatcherQueue.CreateTimer();
        analysisTimer.Interval = TimeSpan.FromMilliseconds(400);
        analysisTimer.IsRepeating = false;
        analysisTimer.Tick += (_, _) => { if (!analyzing) analysisTask = AnalyzeAsync(); };
        Content = web; service.Initialize(web);
        Loaded += async (_, _) =>
        {
            if (initialized) return; initialized = true;
            try
            {
                var environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, Path.Combine(Services.AppServices.Current.Paths.Root, "EditorProfile"),
                    new CoreWebView2EnvironmentOptions { AdditionalBrowserArguments = "--disable-features=CalculateNativeWinOcclusion" });
                if (disposed) return;
                await web.EnsureCoreWebView2Async(environment);
                if (disposed) return;
                web.CoreWebView2.SetVirtualHostNameToFolderMapping("editor.xfeworkflow.local", Path.Combine(AppContext.BaseDirectory, "Assets", "Editor"), CoreWebView2HostResourceAccessKind.DenyCors);
                web.CoreWebView2.Settings.AreDevToolsEnabled = false;
                web.CoreWebView2.WebMessageReceived += MessageReceived;
                web.CoreWebView2.NavigationStarting += (_, e) => { if (!e.Uri.StartsWith("https://editor.xfeworkflow.local/", StringComparison.Ordinal)) e.Cancel = true; };
                web.Source = new Uri("https://editor.xfeworkflow.local/index.html");
            }
            catch (Exception ex) { if (!disposed) { loadedEditor.TrySetException(ex); DiagnosticsChanged?.Invoke([new("编辑器初始化失败:" + ex.Message)]); } }
        };
    }
    public string Text
    {
        get => text;
        set
        {
            if (disposed) return;
            text = value; documentId = Guid.NewGuid().ToString("N");
            SendDocument(); QueueAnalysis();
        }
    }
    private void Send(object value)
    { if (ready && !disposed) web.CoreWebView2.PostWebMessageAsJson(JsonSerializer.Serialize(value, WorkflowJson.Options)); }
    private void SendDocument() => Send(new { type = "set", text, documentId, readOnly, capabilities = Capabilities.Select(x => new { x.Id, x.Name }) });
    private async void MessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e)
    {
        if (disposed || !e.Source.StartsWith("https://editor.xfeworkflow.local/", StringComparison.Ordinal)) return;
        try
        {
            var message = JsonNode.Parse(e.WebMessageAsJson)!.AsObject();
            switch (message.String("type"))
            {
                case "ready": ready = true; SendDocument(); QueueAnalysis(); loadedEditor.TrySetResult(); break;
                case "change": if (readOnly || message.String("documentId") != documentId) break; text = message.String("text"); TextChanged?.Invoke(text); QueueAnalysis(); break;
                case "command": if (!readOnly && message.String("documentId") == documentId && message.String("command") is "run" or "stop" or "stepOver" or "stepInto" or "breakpoint") EditorCommandInvoked?.Invoke(message.String("command")); break;
                case "complete":
                    var id = message.String("id");
                    var completionDocument = message.String("documentId");
                    if (completionDocument != documentId || !analysisEnabled) { Send(new { type = "completion", id, items = Array.Empty<CodeCompletion>() }); break; }
                    var version = analysisVersion;
                    var items = await compiler.CompleteAsync(text, IsNodeBody, message.Integer("position"));
                    Send(new { type = "completion", id, items = IsCurrentAnalysis(version) ? items : [] });
                    break;
            }
        }
        catch (OperationCanceledException) { }
        catch (Exception ex) { if (!disposed && analysisEnabled) DiagnosticsChanged?.Invoke([new(ex.Message)]); }
    }
    private void QueueAnalysis()
    {
        analysisVersion++;
        analysisTimer.Stop();
        if (analysisEnabled && !disposed) analysisTimer.Start();
    }
    private bool IsCurrentAnalysis(long version) => !disposed && analysisEnabled && version == analysisVersion;
    private async Task AnalyzeAsync()
    {
        if (disposed || !analysisEnabled || analyzing) return;
        analyzing = true;
        var version = analysisVersion;
        var source = text; var body = IsNodeBody; var file = FileName; var document = documentId;
        try
        {
            // Let the current Roslyn pass finish; switching documents only invalidates its result.
            // A single worker and the timer coalesce edits without throwing from Roslyn workers.
            var diagnostics = await compiler.AnalyzeAsync(source, body, file);
            if (!IsCurrentAnalysis(version)) return;
            DiagnosticsChanged?.Invoke(diagnostics);
            Send(new { type = "diagnostics", documentId = document, items = diagnostics });
        }
        catch (Exception ex) { if (IsCurrentAnalysis(version)) DiagnosticsChanged?.Invoke([new(ex.Message)]); }
        finally
        {
            analyzing = false;
            if (!disposed && analysisEnabled && version != analysisVersion)
            { analysisTimer.Stop(); analysisTimer.Start(); }
        }
    }
    public void GoTo(int line, int column = 1) { if (ready) Send(new { type = "goto", line, column }); }
    public async Task<string> GetTextAsync()
    {
        if (!ready) return text;
        var expectedDocument = documentId;
        var snapshot = JsonNode.Parse(await web.ExecuteScriptAsync("({documentId, text: editor.getValue()})"))?.AsObject();
        return documentId == expectedDocument && snapshot?.String("documentId") == expectedDocument ? snapshot.String("text") : text;
    }
    public async Task EditCommandAsync(string command)
    { if (ready && command is "undo" or "redo") await web.ExecuteScriptAsync("editor.trigger('toolbar', '" + command + "', null);"); }
    public async Task ScaleFontAsync(double factor)
    {
        fontSize = Math.Clamp(fontSize * factor, 10, 28);
        if (ready) await web.ExecuteScriptAsync("editor.updateOptions({fontSize:" + fontSize.ToString(System.Globalization.CultureInfo.InvariantCulture) + "});");
    }
    public void Dispose()
    {
        if (disposed) return;
        disposed = true; analysisVersion++; analysisTimer.Stop();
        compiler.Dispose(); web.Close();
    }
}