From 23ece4fad340fa9512a923e7cbcc28a669d86401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=B0=8F=E9=AB=98?= Date: Mon, 20 Oct 2025 15:55:43 +0800 Subject: [PATCH] Add Avalonia project support and package defaults --- .../Api/CodeRunner.cs | 154 +++++++++++++++++- SharpPad/Controllers/CodeRunController.cs | 58 +++++-- SharpPad/Controllers/CompletionController.cs | 105 +++++++++--- SharpPad/wwwroot/execution/runner.js | 18 +- SharpPad/wwwroot/fileSystem/fileManager.js | 2 +- SharpPad/wwwroot/index.html | 1 + 6 files changed, 281 insertions(+), 57 deletions(-) diff --git a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs index 7b1e556..c53fd25 100644 --- a/MonacoRoslynCompletionProvider/Api/CodeRunner.cs +++ b/MonacoRoslynCompletionProvider/Api/CodeRunner.cs @@ -124,6 +124,11 @@ private static string NormalizeProjectType(string type) return "winforms"; } + if (filtered.Contains("avalonia")) + { + return "avalonia"; + } + if (filtered.Contains("aspnet") || filtered.Contains("webapi") || filtered == "web") { return "webapi"; @@ -132,12 +137,28 @@ private static string NormalizeProjectType(string type) return "console"; } - private static (OutputKind OutputKind, bool RequiresStaThread) GetRunBehavior(string projectType) + private readonly struct RunBehavior + { + public RunBehavior(OutputKind outputKind, bool requiresStaThread, bool allowNonWindows) + { + OutputKind = outputKind; + RequiresStaThread = requiresStaThread; + AllowNonWindows = allowNonWindows; + } + + public OutputKind OutputKind { get; } + public bool RequiresStaThread { get; } + public bool AllowNonWindows { get; } + } + + private static RunBehavior GetRunBehavior(string projectType) { - return NormalizeProjectType(projectType) switch + var normalized = NormalizeProjectType(projectType); + return normalized switch { - "winforms" => (OutputKind.WindowsApplication, true), - _ => (OutputKind.ConsoleApplication, false) + "winforms" => new RunBehavior(OutputKind.WindowsApplication, requiresStaThread: true, allowNonWindows: false), + "avalonia" => new RunBehavior(OutputKind.WindowsApplication, requiresStaThread: true, allowNonWindows: true), + _ => new RunBehavior(OutputKind.ConsoleApplication, requiresStaThread: false, allowNonWindows: true) }; } @@ -373,15 +394,16 @@ private static async Task CompileAndExecuteAsync( var normalizedProjectType = NormalizeProjectType(projectType); var runBehavior = GetRunBehavior(projectType); - + // 检测是否需要 WinForms 支持 if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) { // 自动检测到 WinForms 代码时启用所需的运行时设置 - runBehavior = (OutputKind.WindowsApplication, true); + runBehavior = new RunBehavior(OutputKind.WindowsApplication, requiresStaThread: true, allowNonWindows: false); + normalizedProjectType = "winforms"; } - if (runBehavior.OutputKind == OutputKind.WindowsApplication && !OperatingSystem.IsWindows()) + if (!runBehavior.AllowNonWindows && !OperatingSystem.IsWindows()) { await onError(WindowsFormsHostRequirementMessage).ConfigureAwait(false); result.Error = WindowsFormsHostRequirementMessage; @@ -427,6 +449,11 @@ private static async Task ExecuteInProcessAsync( try { + if (!string.IsNullOrWhiteSpace(nuget)) + { + DownloadNugetPackages.DownloadAllPackages(nuget); + } + var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); loadContext = new CustomAssemblyLoadContext(nugetAssemblies); @@ -614,7 +641,15 @@ private static async Task ExecuteInIsolatedProcessAsync( // 检测是否需要 WinForms 支持 if (runBehavior.OutputKind != OutputKind.WindowsApplication && DetectWinFormsUsage(files?.Select(f => f?.Content))) { - runBehavior = (OutputKind.WindowsApplication, true); + runBehavior = new RunBehavior(OutputKind.WindowsApplication, requiresStaThread: true, allowNonWindows: false); + normalizedProjectType = "winforms"; + } + + if (!runBehavior.AllowNonWindows && !OperatingSystem.IsWindows()) + { + await onError(WindowsFormsHostRequirementMessage).ConfigureAwait(false); + result.Error = WindowsFormsHostRequirementMessage; + return result; } var parseOptions = new CSharpParseOptions( @@ -644,13 +679,19 @@ private static async Task ExecuteInIsolatedProcessAsync( EnsureStandardLibraryReferences(references); - if (runBehavior.OutputKind == OutputKind.WindowsApplication) + if (runBehavior.OutputKind == OutputKind.WindowsApplication && normalizedProjectType == "winforms") { EnsureWinFormsAssembliesLoaded(); TryAddWinFormsReferences(references); } var packageReferenceMap = BuildPackageReferenceMap(nuget, normalizedProjectType); + + if (!string.IsNullOrWhiteSpace(nuget)) + { + DownloadNugetPackages.DownloadAllPackages(nuget); + } + var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget); foreach (var package in nugetAssemblies) { @@ -1270,6 +1311,12 @@ void AddDefaultPackages() { switch (normalizedProjectType) { + case "avalonia": + Add("Avalonia", "11.3.4"); + Add("Avalonia.Desktop", "11.3.4"); + Add("Avalonia.Themes.Fluent", "11.3.4"); + Add("Avalonia.ReactiveUI", "11.3.4"); + break; case "aspnetcore": case "aspnetcorewebapi": case "webapi": @@ -1287,6 +1334,95 @@ void AddDefaultPackages() return map; } + public static IReadOnlyList GetDefaultPackages(string projectType) + { + var normalizedProjectType = NormalizeProjectType(projectType); + var map = BuildPackageReferenceMap(string.Empty, normalizedProjectType); + return map + .Select(entry => new Package(entry.Key, entry.Value ?? string.Empty)) + .ToList(); + } + + public static (List Packages, string Specification) PreparePackageReferences(IEnumerable? packages, string projectType) + { + var specificationInput = CreatePackageSpecification(packages); + var normalizedProjectType = NormalizeProjectType(projectType); + var referenceMap = BuildPackageReferenceMap(specificationInput, normalizedProjectType); + var specification = BuildPackageSpecificationString(referenceMap); + var packageList = referenceMap + .Select(entry => new Package(entry.Key, entry.Value ?? string.Empty)) + .ToList(); + + return (packageList, specification); + } + + private static string CreatePackageSpecification(IEnumerable? packages) + { + if (packages == null) + { + return string.Empty; + } + + var segments = new List(); + + foreach (var package in packages) + { + if (package == null || string.IsNullOrWhiteSpace(package.Id)) + { + continue; + } + + var builder = new StringBuilder(); + builder.Append(package.Id.Trim()); + + var version = NormalizePackageVersion(package.Version); + if (!string.IsNullOrWhiteSpace(version)) + { + builder.Append(','); + builder.Append(version); + } + + builder.Append(';'); + segments.Add(builder.ToString()); + } + + if (segments.Count == 0) + { + return string.Empty; + } + + return string.Join(" ", segments.Select(segment => $"{segment}{Environment.NewLine}")); + } + + private static string BuildPackageSpecificationString(IDictionary map) + { + if (map == null || map.Count == 0) + { + return string.Empty; + } + + var builder = new StringBuilder(); + foreach (var entry in map.OrderBy(e => e.Key, StringComparer.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(entry.Key)) + { + continue; + } + + builder.Append(entry.Key); + if (!string.IsNullOrWhiteSpace(entry.Value)) + { + builder.Append(','); + builder.Append(entry.Value); + } + + builder.Append(';'); + builder.Append(Environment.NewLine); + } + + return builder.ToString(); + } + private static string? NormalizePackageVersion(string? version) { if (string.IsNullOrWhiteSpace(version)) diff --git a/SharpPad/Controllers/CodeRunController.cs b/SharpPad/Controllers/CodeRunController.cs index 8d007c7..51cc976 100644 --- a/SharpPad/Controllers/CodeRunController.cs +++ b/SharpPad/Controllers/CodeRunController.cs @@ -1,13 +1,15 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using MonacoRoslynCompletionProvider; -using MonacoRoslynCompletionProvider.Api; -using Newtonsoft.Json; -using System.Text.Json; -using System.Threading.Channels; -using System.IO.Compression; -using System.Collections.Concurrent; -using static MonacoRoslynCompletionProvider.Api.CodeRunner; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using MonacoRoslynCompletionProvider; +using MonacoRoslynCompletionProvider.Api; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.IO.Compression; +using System.Linq; +using System.Text.Json; +using System.Threading.Channels; +using static MonacoRoslynCompletionProvider.Api.CodeRunner; namespace SharpPad.Controllers { @@ -18,9 +20,13 @@ public class CodeRunController : ControllerBase // 会话管理:存储活跃的会话和对应的取消令牌 private static readonly ConcurrentDictionary _activeSessions = new(); [HttpPost("run")] - public async Task Run([FromBody] MultiFileCodeRunRequest request) - { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + public async Task Run([FromBody] MultiFileCodeRunRequest request) + { + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } // 设置响应头,允许流式输出 Response.Headers.TryAdd("Content-Type", "text/event-stream;charset=utf-8"); @@ -116,10 +122,22 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request) } cts?.Dispose(); } - } - - private async Task OnOutputAsync(string output, ChannelWriter writer, CancellationToken token) - { + } + + private static (List Packages, string Specification) PreparePackages(IEnumerable packages, string projectType) + { + var (resolved, specification) = CodeRunner.PreparePackageReferences(packages, projectType); + + if (!string.IsNullOrWhiteSpace(specification)) + { + CodeRunner.DownloadPackage(specification); + } + + return (resolved, specification); + } + + private async Task OnOutputAsync(string output, ChannelWriter writer, CancellationToken token) + { if (token.IsCancellationRequested) return; try @@ -246,7 +264,11 @@ public async Task BuildExe([FromBody] ExeBuildRequest request) // 创建处理 Channel 的任务 var processTask = ProcessChannelAsync(channel.Reader, cts.Token); - string nugetPackages = string.Join(" ", request?.Packages?.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } ExeBuildResult result; diff --git a/SharpPad/Controllers/CompletionController.cs b/SharpPad/Controllers/CompletionController.cs index b5e88dd..36e75c9 100644 --- a/SharpPad/Controllers/CompletionController.cs +++ b/SharpPad/Controllers/CompletionController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using MonacoRoslynCompletionProvider; using MonacoRoslynCompletionProvider.Api; +using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; namespace SharpPad.Controllers @@ -13,7 +15,11 @@ public class CompletionController : ControllerBase [HttpPost("complete")] public async Task Complete([FromBody] TabCompletionRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } var tabCompletionResults = await MonacoRequestHandler.CompletionHandle(request, nugetPackages, request?.ProjectType); return Ok(tabCompletionResults); } @@ -21,7 +27,11 @@ public async Task Complete([FromBody] TabCompletionRequest reques [HttpPost("signature")] public async Task Signature([FromBody] SignatureHelpRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } var signatureHelpResult = await MonacoRequestHandler.SignatureHelpHandle(request, nugetPackages, request?.ProjectType); return Ok(signatureHelpResult); } @@ -29,7 +39,11 @@ public async Task Signature([FromBody] SignatureHelpRequest reque [HttpPost("hover")] public async Task Hover([FromBody] HoverInfoRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } var hoverInfoResult = await MonacoRequestHandler.HoverHandle(request, nugetPackages, request?.ProjectType); return Ok(hoverInfoResult); } @@ -37,7 +51,11 @@ public async Task Hover([FromBody] HoverInfoRequest request) [HttpPost("codeCheck")] public async Task CodeCheck([FromBody] MultiFileCodeCheckRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -50,7 +68,7 @@ public async Task CodeCheck([FromBody] MultiFileCodeCheckRequest var singleRequest = new CodeCheckRequest { Code = request?.Code, - Packages = request?.Packages, + Packages = packages, ProjectType = request?.ProjectType, }; var codeCheckResults = await MonacoRequestHandler.CodeCheckHandle(singleRequest, nugetPackages, request?.ProjectType); @@ -72,7 +90,11 @@ public IActionResult Format([FromBody] CodeFormatRequest request) [HttpPost("definition")] public async Task Definition([FromBody] DefinitionRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } var definitionResult = await MonacoRequestHandler.DefinitionHandle(request, nugetPackages, request?.ProjectType); return Ok(definitionResult); } @@ -80,7 +102,11 @@ public async Task Definition([FromBody] DefinitionRequest request [HttpPost("semanticTokens")] public async Task SemanticTokens([FromBody] SemanticTokensRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } var semanticTokensResult = await MonacoRequestHandler.SemanticTokensHandle(request, nugetPackages, request?.ProjectType); return Ok(semanticTokensResult); } @@ -88,7 +114,11 @@ public async Task SemanticTokens([FromBody] SemanticTokensRequest [HttpPost("multiFileSemanticTokens")] public async Task MultiFileSemanticTokens([FromBody] MultiFileSemanticTokensRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -100,7 +130,7 @@ public async Task MultiFileSemanticTokens([FromBody] MultiFileSem var singleRequest = new SemanticTokensRequest { Code = request?.Code ?? string.Empty, - Packages = request?.Packages ?? new List(), + Packages = packages, ProjectType = request?.ProjectType }; @@ -114,7 +144,7 @@ public IActionResult AddPackages([FromBody] AddPackagesRequest request) { try { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (_, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType, ensureDownloaded: false); CodeRunner.DownloadPackage(nugetPackages, request?.SourceKey); return Ok(new { @@ -138,7 +168,7 @@ public IActionResult RemovePackages([FromBody] RemovePackagesRequest request) { try { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (_, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType, ensureDownloaded: false); CodeRunner.RemovePackages(nugetPackages); return Ok(new { @@ -162,7 +192,12 @@ public async Task CodeActions([FromBody] CodeActionRequest reques { try { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } + var codeActionResults = await MonacoRequestHandler.CodeActionsHandle(request, nugetPackages, request?.ProjectType); return Ok(codeActionResults); } @@ -197,7 +232,11 @@ public IActionResult TestCoreLibXmlDocumentation() [HttpPost("multiFileComplete")] public async Task MultiFileComplete([FromBody] MultiFileTabCompletionRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -211,7 +250,7 @@ public async Task MultiFileComplete([FromBody] MultiFileTabComple { Code = request?.Code, Position = request?.Position ?? 0, - Packages = request?.Packages, + Packages = packages, ProjectType = request?.ProjectType, }; var tabCompletionResults = await MonacoRequestHandler.CompletionHandle(singleRequest, nugetPackages, request?.ProjectType); @@ -222,7 +261,11 @@ public async Task MultiFileComplete([FromBody] MultiFileTabComple [HttpPost("multiFileCodeCheck")] public async Task MultiFileCodeCheck([FromBody] MultiFileCodeCheckRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -235,7 +278,7 @@ public async Task MultiFileCodeCheck([FromBody] MultiFileCodeChec var singleRequest = new CodeCheckRequest { Code = request?.Code, - Packages = request?.Packages, + Packages = packages, ProjectType = request?.ProjectType }; var codeCheckResults = await MonacoRequestHandler.CodeCheckHandle(singleRequest, nugetPackages, request?.ProjectType); @@ -246,7 +289,11 @@ public async Task MultiFileCodeCheck([FromBody] MultiFileCodeChec [HttpPost("multiFileSignature")] public async Task MultiFileSignature([FromBody] MultiFileSignatureHelpRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -260,7 +307,7 @@ public async Task MultiFileSignature([FromBody] MultiFileSignatur { Code = request?.Code, Position = request?.Position ?? 0, - Packages = request?.Packages, + Packages = packages, ProjectType = request?.ProjectType }; var signatureHelpResult = await MonacoRequestHandler.SignatureHelpHandle(singleRequest, nugetPackages, request?.ProjectType); @@ -271,7 +318,11 @@ public async Task MultiFileSignature([FromBody] MultiFileSignatur [HttpPost("multiFileHover")] public async Task MultiFileHover([FromBody] MultiFileHoverInfoRequest request) { - string nugetPackages = string.Join(" ", request?.Packages.Select(p => $"{p.Id},{p.Version};{Environment.NewLine}") ?? []); + var (packages, nugetPackages) = PreparePackages(request?.Packages ?? Enumerable.Empty(), request?.ProjectType); + if (request != null) + { + request.Packages = packages; + } if (request?.IsMultiFile == true) { @@ -285,12 +336,24 @@ public async Task MultiFileHover([FromBody] MultiFileHoverInfoReq { Code = request?.Code, Position = request?.Position ?? 0, - Packages = request?.Packages, + Packages = packages, ProjectType = request?.ProjectType }; var hoverInfoResult = await MonacoRequestHandler.HoverHandle(singleRequest, nugetPackages, request?.ProjectType); return Ok(hoverInfoResult); } } + + private static (List Packages, string Specification) PreparePackages(IEnumerable packages, string projectType, bool ensureDownloaded = true) + { + var (resolved, specification) = CodeRunner.PreparePackageReferences(packages, projectType); + + if (ensureDownloaded && !string.IsNullOrWhiteSpace(specification)) + { + CodeRunner.DownloadPackage(specification); + } + + return (resolved, specification); + } } -} +} diff --git a/SharpPad/wwwroot/execution/runner.js b/SharpPad/wwwroot/execution/runner.js index f383e37..3461ae3 100644 --- a/SharpPad/wwwroot/execution/runner.js +++ b/SharpPad/wwwroot/execution/runner.js @@ -291,14 +291,16 @@ export class CodeRunner { // 检查文本是否包含 markdown 格式 getProjectTypeLabel(projectType) { - switch ((projectType || '').toLowerCase()) { - case 'winforms': - return 'WinForms 桌面'; - case 'webapi': - return 'ASP.NET Core Web API'; - default: - return 'Console 应用'; - } + switch ((projectType || '').toLowerCase()) { + case 'winforms': + return 'WinForms 桌面'; + case 'avalonia': + return 'Avalonia 桌面'; + case 'webapi': + return 'ASP.NET Core Web API'; + default: + return 'Console 应用'; + } } containsMarkdown(text) { diff --git a/SharpPad/wwwroot/fileSystem/fileManager.js b/SharpPad/wwwroot/fileSystem/fileManager.js index 1bf0338..2c9aa05 100644 --- a/SharpPad/wwwroot/fileSystem/fileManager.js +++ b/SharpPad/wwwroot/fileSystem/fileManager.js @@ -68,7 +68,7 @@ class FileManager { return allowed[0] || fallback; } - const allowedFallback = ['console', 'winforms', 'webapi']; + const allowedFallback = ['console', 'winforms', 'avalonia', 'webapi']; if (candidate && allowedFallback.includes(candidate)) { return candidate; } diff --git a/SharpPad/wwwroot/index.html b/SharpPad/wwwroot/index.html index c210725..74b5ca8 100644 --- a/SharpPad/wwwroot/index.html +++ b/SharpPad/wwwroot/index.html @@ -69,6 +69,7 @@