Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 145 additions & 9 deletions MonacoRoslynCompletionProvider/Api/CodeRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)
};
}

Expand Down Expand Up @@ -373,15 +394,16 @@ private static async Task<RunResult> 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;
Expand Down Expand Up @@ -427,6 +449,11 @@ private static async Task<RunResult> ExecuteInProcessAsync(

try
{
if (!string.IsNullOrWhiteSpace(nuget))
{
DownloadNugetPackages.DownloadAllPackages(nuget);
}

var nugetAssemblies = DownloadNugetPackages.LoadPackages(nuget);
loadContext = new CustomAssemblyLoadContext(nugetAssemblies);

Expand Down Expand Up @@ -614,7 +641,15 @@ private static async Task<RunResult> 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(
Expand Down Expand Up @@ -644,13 +679,19 @@ private static async Task<RunResult> 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)
{
Expand Down Expand Up @@ -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":
Expand All @@ -1287,6 +1334,95 @@ void AddDefaultPackages()
return map;
}

public static IReadOnlyList<Package> 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<Package> Packages, string Specification) PreparePackageReferences(IEnumerable<Package>? 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<Package>? packages)
{
if (packages == null)
{
return string.Empty;
}

var segments = new List<string>();

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<string, string?> 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))
Expand Down
58 changes: 40 additions & 18 deletions SharpPad/Controllers/CodeRunController.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -18,9 +20,13 @@ public class CodeRunController : ControllerBase
// 会话管理:存储活跃的会话和对应的取消令牌
private static readonly ConcurrentDictionary<string, CancellationTokenSource> _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<Package>(), request?.ProjectType);
if (request != null)
{
request.Packages = packages;
}

// 设置响应头,允许流式输出
Response.Headers.TryAdd("Content-Type", "text/event-stream;charset=utf-8");
Expand Down Expand Up @@ -116,10 +122,22 @@ public async Task Run([FromBody] MultiFileCodeRunRequest request)
}
cts?.Dispose();
}
}

private async Task OnOutputAsync(string output, ChannelWriter<string> writer, CancellationToken token)
{
}

private static (List<Package> Packages, string Specification) PreparePackages(IEnumerable<Package> 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<string> writer, CancellationToken token)
{
if (token.IsCancellationRequested) return;

try
Expand Down Expand Up @@ -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<Package>(), request?.ProjectType);
if (request != null)
{
request.Packages = packages;
}

ExeBuildResult result;

Expand Down
Loading
Loading