Initial commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.JSInterop;
|
||||
using RobotNet10.ScriptEditor.Helpers.Code;
|
||||
using RobotNet10.ScriptEditor.Helpers.Webcil;
|
||||
|
||||
namespace RobotNet10.ScriptEditor.Services;
|
||||
|
||||
public class ScriptResourceResolver
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IJSRuntime? jsRuntime;
|
||||
private readonly Lazy<Task<Dictionary<string, string>>> _resourceMappings;
|
||||
|
||||
public ScriptResourceResolver(HttpClient client, IJSRuntime? jsRuntime = null)
|
||||
{
|
||||
httpClient = client;
|
||||
this.jsRuntime = jsRuntime;
|
||||
_resourceMappings = new Lazy<Task<Dictionary<string, string>>>(FetchResourcesAsync);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MetadataReference>> GetMetadataReferences(string[] wasModules, string[] docModules)
|
||||
{
|
||||
var metadataReferences = new List<MetadataReference>();
|
||||
foreach (var wasModule in wasModules)
|
||||
{
|
||||
var docModule = $"{wasModule}.xml";
|
||||
if(!docModules.Contains(docModule))
|
||||
{
|
||||
docModule = string.Empty;
|
||||
}
|
||||
metadataReferences.Add(await GetMetadataReferenceAsync(wasModule, docModule));
|
||||
}
|
||||
return metadataReferences;
|
||||
}
|
||||
|
||||
private async Task<PortableExecutableReference> GetMetadataReferenceAsync(string wasModule, string docModule)
|
||||
{
|
||||
await using var stream = await httpClient.GetStreamAsync(await ResolveResource($"{wasModule}.wasm"));
|
||||
var peBytes = await WebcilConverterUtil.ConvertFromWebcilAsync(stream);
|
||||
|
||||
using var peStream = new MemoryStream(peBytes);
|
||||
if (string.IsNullOrEmpty(docModule))
|
||||
{
|
||||
return MetadataReference.CreateFromStream(peStream, MetadataReferenceProperties.Assembly);
|
||||
}
|
||||
else
|
||||
{
|
||||
var docBuf = await httpClient.GetByteArrayAsync($"docs/{docModule}");
|
||||
return MetadataReference.CreateFromStream(peStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(docBuf));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveResource(string logicalName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(logicalName))
|
||||
throw new ArgumentException("Logical name cannot be null or empty.", nameof(logicalName));
|
||||
|
||||
var baseUri = httpClient.BaseAddress?.ToString().TrimEnd('/') ?? "";
|
||||
|
||||
// Strategy 1: Try JavaScript interop to get resource path from Blazor runtime (NET 10+)
|
||||
if (jsRuntime != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsPath = await jsRuntime.InvokeAsync<string>("robotnet.blazor.getResourcePath", logicalName);
|
||||
if (!string.IsNullOrEmpty(jsPath) && await TryResourceExists(jsPath))
|
||||
{
|
||||
return jsPath;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JavaScript function might not be available, continue to other strategies
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Try direct path (NET 10+ common case)
|
||||
var directPath = $"{baseUri}/_framework/{logicalName}";
|
||||
if (await TryResourceExists(directPath))
|
||||
{
|
||||
return directPath;
|
||||
}
|
||||
|
||||
// Strategy 3: Try to get mapping from boot file (for NET 9 and earlier, or if direct path fails)
|
||||
var resources = await _resourceMappings.Value;
|
||||
if (resources.TryGetValue(logicalName, out var hashedName))
|
||||
{
|
||||
var hashedPath = $"{baseUri}/_framework/{hashedName}";
|
||||
if (await TryResourceExists(hashedPath))
|
||||
{
|
||||
return hashedPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
$"Resource '{logicalName}' not found. " +
|
||||
$"Tried: JavaScript interop, direct path '{directPath}', " +
|
||||
$"and boot configuration mapping. " +
|
||||
$"In .NET 10, resources may be embedded in dotnet.js. " +
|
||||
$"Please ensure JavaScript function 'robotnet.blazor.getResourcePath' is available.");
|
||||
}
|
||||
|
||||
private async Task<bool> TryResourceExists(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await httpClient.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Head, path),
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> FetchResourcesAsync()
|
||||
{
|
||||
// In NET 10+, boot files are no longer used - resources are accessed directly
|
||||
// This method is kept for backward compatibility with NET 9 and earlier
|
||||
// Return empty dictionary to indicate we should use direct paths
|
||||
var baseUri = httpClient.BaseAddress?.ToString().TrimEnd('/') ?? "";
|
||||
|
||||
// Try blazor.boot.config.json first (some NET 10 preview versions)
|
||||
var bootConfigUrl = $"{baseUri}/_framework/blazor.boot.config.json";
|
||||
try
|
||||
{
|
||||
var bootConfigContent = await httpClient.GetStringAsync(bootConfigUrl);
|
||||
return ParseBootConfigJson(bootConfigContent);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// Fallback to blazor.boot.json (NET 9 and earlier)
|
||||
var bootJsonUrl = $"{baseUri}/_framework/blazor.boot.json";
|
||||
try
|
||||
{
|
||||
var bootJsonContent = await httpClient.GetStringAsync(bootJsonUrl);
|
||||
return ParseBootJson(bootJsonContent);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// NET 10+: No boot file exists, use direct paths
|
||||
// Return empty dictionary - ResolveResource will use direct path
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseBootConfigJson(string jsonContent)
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonContent);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var allResources = new Dictionary<string, string>();
|
||||
|
||||
// NET 10+ uses different structure - check multiple possible locations
|
||||
if (root.TryGetProperty("resources", out var resources))
|
||||
{
|
||||
// Try to get fingerprinting resources (maps logical name -> hashed name)
|
||||
if (resources.TryGetProperty("fingerprinting", out var fingerprinting))
|
||||
{
|
||||
foreach (var prop in fingerprinting.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for assembly resources directly (maps assembly name -> hashed name)
|
||||
if (resources.TryGetProperty("assembly", out var assembly))
|
||||
{
|
||||
foreach (var prop in assembly.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wasmNative resources (for .wasm files)
|
||||
if (resources.TryGetProperty("wasmNative", out var wasmNative))
|
||||
{
|
||||
foreach (var prop in wasmNative.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check root level for direct mappings (some NET 10 versions might use this)
|
||||
if (root.TryGetProperty("fingerprinting", out var rootFingerprinting))
|
||||
{
|
||||
foreach (var prop in rootFingerprinting.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseBootJson(string jsonContent)
|
||||
{
|
||||
var bootJson = System.Text.Json.JsonSerializer.Deserialize<BlazorBootJson>(jsonContent);
|
||||
if (bootJson?.Resources?.Fingerprinting == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid blazor.boot.json structure.");
|
||||
}
|
||||
|
||||
// Combine all relevant resources into one dictionary for easy lookup
|
||||
var allResources = new Dictionary<string, string>();
|
||||
|
||||
foreach (var resource in bootJson.Resources.Fingerprinting.Where(resource => !allResources.ContainsKey(resource.Value)))
|
||||
{
|
||||
allResources.Add(resource.Value, resource.Key);
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using RobotNet10.ScriptEditor.Helpers;
|
||||
using RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
|
||||
using RobotNet10.ScriptEditor.Models;
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
|
||||
namespace RobotNet10.ScriptEditor.Services;
|
||||
|
||||
internal class ScriptWorkspace : IDisposable
|
||||
{
|
||||
public IEnumerable<ScriptFolder> Folders => WorkspaceFolders;
|
||||
public IEnumerable<ScriptFile> Files => WorkspaceFiles;
|
||||
|
||||
public event Action? ReadOnlyChanged;
|
||||
public event Action<IEnumerable<Diagnostic>>? DiagnoticChanged;
|
||||
public event Action? RootChanged;
|
||||
public event Action<ScriptFile?>? CurrentFileChanged;
|
||||
public event Action<ScriptFile?>? SelectedFileChanged;
|
||||
public event Action<ScriptFolder?>? SelectedFolderChanged;
|
||||
|
||||
public ScriptFile? CurrentFile { get; private set; }
|
||||
|
||||
private ScriptFile? _selectedFile;
|
||||
private ScriptFolder? _selectedFolder;
|
||||
private bool _isUpdatingSelection = false;
|
||||
|
||||
public ScriptFile? SelectedFile
|
||||
{
|
||||
get => _selectedFile;
|
||||
set
|
||||
{
|
||||
if (_selectedFile == value) return;
|
||||
if (_isUpdatingSelection) return;
|
||||
|
||||
_isUpdatingSelection = true;
|
||||
try
|
||||
{
|
||||
if (value is not null && CurrentFile != value)
|
||||
{
|
||||
CurrentFile = value;
|
||||
CurrentFileChanged?.Invoke(CurrentFile);
|
||||
}
|
||||
|
||||
var oldFolder = _selectedFile?.Parent;
|
||||
_selectedFile = value;
|
||||
|
||||
// Only update SelectedFolder if it's different
|
||||
if (_selectedFile?.Parent != oldFolder)
|
||||
{
|
||||
if (_selectedFolder != _selectedFile?.Parent)
|
||||
{
|
||||
_selectedFolder = _selectedFile?.Parent;
|
||||
SelectedFolderChanged?.Invoke(_selectedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
SelectedFileChanged?.Invoke(_selectedFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingSelection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptFolder? SelectedFolder
|
||||
{
|
||||
get => _selectedFolder;
|
||||
set
|
||||
{
|
||||
if (_selectedFolder == value) return;
|
||||
if (_isUpdatingSelection) return;
|
||||
|
||||
_isUpdatingSelection = true;
|
||||
try
|
||||
{
|
||||
_selectedFolder = value;
|
||||
|
||||
// Only clear SelectedFile if it's not null and not related to the new folder
|
||||
if (_selectedFile != null && _selectedFile.Parent != _selectedFolder)
|
||||
{
|
||||
_selectedFile = null;
|
||||
SelectedFileChanged?.Invoke(null);
|
||||
}
|
||||
|
||||
SelectedFolderChanged?.Invoke(_selectedFolder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingSelection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
private bool _isReadOnly;
|
||||
|
||||
public bool IsReadOnly
|
||||
{
|
||||
get => _isReadOnly;
|
||||
set
|
||||
{
|
||||
if (_isReadOnly == value) return;
|
||||
|
||||
_isReadOnly = value;
|
||||
ReadOnlyChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly CSharpParseOptions WorkspaceParseOptions = CSharpParseOptions.Default.WithKind(SourceCodeKind.Script).WithLanguageVersion(LanguageVersion.Latest);
|
||||
private readonly List<ScriptFolder> WorkspaceFolders = [];
|
||||
private readonly List<ScriptFile> WorkspaceFiles = [];
|
||||
private readonly AdhocWorkspace adhocWorkspace = new();
|
||||
private readonly ProjectId ProjectId = ProjectId.CreateNewId();
|
||||
private readonly System.Timers.Timer DiagnosticTimer = new(1000) { AutoReset = false };
|
||||
|
||||
public ScriptWorkspace()
|
||||
{
|
||||
DiagnosticTimer.Elapsed += DiagnosticTimer_Elapsed;
|
||||
}
|
||||
|
||||
public void Initialize(IEnumerable<MetadataReference> references, string[] usingNamespaces, Type globalType, ScriptFolderDto rootFolder)
|
||||
{
|
||||
if (IsInitialized) throw new InvalidOperationException("Workspace đã được khởi tạo");
|
||||
|
||||
var preCode = $"{BuildDevelopGlobalsScript(typeof(IScriptGlobals))}\n{BuildDevelopGlobalsScript(globalType)}";
|
||||
|
||||
var csharpCompilationOptions = new CSharpCompilationOptions(
|
||||
OutputKind.DynamicallyLinkedLibrary,
|
||||
usings: usingNamespaces,
|
||||
metadataImportOptions: MetadataImportOptions.All,
|
||||
reportSuppressedDiagnostics: true);
|
||||
//.WithEmitDebugInformation(false); // Disable debug info to avoid Mono debugger agent assertions
|
||||
|
||||
var projectInfo = ProjectInfo.Create(ProjectId, VersionStamp.Create(), "ScriptEditor", "ScriptEditorAssembly", LanguageNames.CSharp)
|
||||
.WithMetadataReferences(references)
|
||||
.WithCompilationOptions(csharpCompilationOptions)
|
||||
.WithParseOptions(WorkspaceParseOptions);
|
||||
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution.AddProject(projectInfo);
|
||||
var preScriptDocId = DocumentId.CreateNewId(ProjectId);
|
||||
updatedSolution = updatedSolution.AddDocument(preScriptDocId, "PreScript.cs", SourceText.From(preCode), ["/"], "/PreScript.cs");
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution))
|
||||
throw new InvalidOperationException("Add pre script document thất bại");
|
||||
|
||||
WorkspaceFiles.AddRange(rootFolder.Files.Select(file => CreateWrokspaceFile(file)));
|
||||
WorkspaceFiles.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
WorkspaceFolders.AddRange(rootFolder.Folders.Select(folder => CreateWorkspaceFolder(folder)));
|
||||
WorkspaceFolders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
_ = Task.Run(DiagnosticProject);
|
||||
RootChanged?.Invoke();
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
public async Task ReinitializeAsync(ScriptFolderDto rootFolder)
|
||||
{
|
||||
if (!IsInitialized) throw new InvalidOperationException("Workspace chưa được khởi tạo");
|
||||
|
||||
// Xóa tất cả documents hiện tại (trừ PreScript.cs)
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution;
|
||||
var project = updatedSolution.GetProject(ProjectId);
|
||||
if (project is not null)
|
||||
{
|
||||
foreach (var doc in project.Documents)
|
||||
{
|
||||
if (doc.Name != "PreScript.cs")
|
||||
{
|
||||
updatedSolution = updatedSolution.RemoveDocument(doc.Id);
|
||||
}
|
||||
}
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution))
|
||||
throw new InvalidOperationException("Xóa documents cũ thất bại");
|
||||
}
|
||||
|
||||
// Clear workspace files và folders
|
||||
WorkspaceFiles.Clear();
|
||||
WorkspaceFolders.Clear();
|
||||
CurrentFile = null;
|
||||
SelectedFile = null;
|
||||
SelectedFolder = null;
|
||||
|
||||
// Reload lại từ rootFolder mới
|
||||
WorkspaceFiles.AddRange(rootFolder.Files.Select(file => CreateWrokspaceFile(file)));
|
||||
WorkspaceFiles.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
WorkspaceFolders.AddRange(rootFolder.Folders.Select(folder => CreateWorkspaceFolder(folder)));
|
||||
WorkspaceFolders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
|
||||
// Chạy diagnostic ngay lập tức để phân tích lỗi của source code sau khi restore
|
||||
await DiagnosticProject();
|
||||
|
||||
RootChanged?.Invoke();
|
||||
CurrentFileChanged?.Invoke(CurrentFile);
|
||||
}
|
||||
|
||||
public void WriteDocument(string text)
|
||||
{
|
||||
if (CurrentFile is null) return;
|
||||
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution;
|
||||
updatedSolution = updatedSolution.WithDocumentText(CurrentFile.Id, SourceText.From(text));
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Cập nhật project ban đầu thất bại");
|
||||
|
||||
CurrentFile.Code = text;
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Start();
|
||||
}
|
||||
|
||||
public string FormatCode(string code)
|
||||
{
|
||||
var tree = CSharpSyntaxTree.ParseText(code, WorkspaceParseOptions);
|
||||
var root = tree.GetRoot();
|
||||
var formattedRoot = Microsoft.CodeAnalysis.Formatting.Formatter.Format(root, adhocWorkspace);
|
||||
return formattedRoot.ToFullString();
|
||||
}
|
||||
|
||||
public async Task<string?> GetQuickInfoCurrentFile(int line, int column)
|
||||
{
|
||||
if (CurrentFile is null) return null;
|
||||
|
||||
return await adhocWorkspace.GetQuickInfoAsync(CurrentFile.Id, line, column);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BlazorMonaco.Languages.CompletionItem>> GetCompletionsCurrentFile(int line, int column, int kind, char? triggerCharacter)
|
||||
{
|
||||
if (CurrentFile is null) return [];
|
||||
|
||||
return await adhocWorkspace.GetCompletionAsync(CurrentFile.Id, line, column, kind, triggerCharacter);
|
||||
}
|
||||
|
||||
public async Task<SignatureHelpResult?> GetSignatureHelpCurrentFile(int line, int column)
|
||||
{
|
||||
if (CurrentFile is null) return null;
|
||||
|
||||
var document = adhocWorkspace.CurrentSolution.GetDocument(CurrentFile.Id);
|
||||
if (document is null) return null;
|
||||
|
||||
return await document.GetSignatureHelpAsync(line, column);
|
||||
}
|
||||
|
||||
public void AddFile(ScriptFileDto fileDto, ScriptFolder? parent = null)
|
||||
{
|
||||
var workspaceFile = CreateWrokspaceFile(fileDto, parent);
|
||||
|
||||
if (parent is null)
|
||||
{
|
||||
WorkspaceFiles.Add(workspaceFile);
|
||||
WorkspaceFiles.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.AddFiles(workspaceFile);
|
||||
}
|
||||
|
||||
RootChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void AddFolder(ScriptFolderDto folderDto, ScriptFolder? parent = null)
|
||||
{
|
||||
var workspaceFolder = CreateWorkspaceFolder(folderDto, parent);
|
||||
|
||||
if (parent is null)
|
||||
{
|
||||
WorkspaceFolders.Add(workspaceFolder);
|
||||
WorkspaceFolders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.AddFolders(workspaceFolder);
|
||||
}
|
||||
|
||||
RootChanged?.Invoke();
|
||||
}
|
||||
|
||||
public ScriptFile? FindFileByPath(string path)
|
||||
{
|
||||
// Search in root files
|
||||
foreach (var file in WorkspaceFiles)
|
||||
{
|
||||
if (file.Path == path) return file;
|
||||
}
|
||||
|
||||
// Search in folders recursively
|
||||
foreach (var folder in WorkspaceFolders)
|
||||
{
|
||||
var found = FindFileInFolder(folder, path);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ScriptFolder? FindFolderByPath(string path)
|
||||
{
|
||||
// Search in root folders
|
||||
foreach (var folder in WorkspaceFolders)
|
||||
{
|
||||
if (folder.Path == path) return folder;
|
||||
|
||||
var found = FindFolderInFolder(folder, path);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ScriptFile? FindFileInFolder(ScriptFolder folder, string path)
|
||||
{
|
||||
foreach (var file in folder.Files)
|
||||
{
|
||||
if (file.Path == path) return file;
|
||||
}
|
||||
|
||||
foreach (var subFolder in folder.Folders)
|
||||
{
|
||||
var found = FindFileInFolder(subFolder, path);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ScriptFolder? FindFolderInFolder(ScriptFolder folder, string path)
|
||||
{
|
||||
foreach (var subFolder in folder.Folders)
|
||||
{
|
||||
if (subFolder.Path == path) return subFolder;
|
||||
|
||||
var found = FindFolderInFolder(subFolder, path);
|
||||
if (found != null) return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveFile(ScriptFile file)
|
||||
{
|
||||
if (file.Parent is null)
|
||||
{
|
||||
WorkspaceFiles.Remove(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
file.Parent.RemoveFile(file);
|
||||
}
|
||||
|
||||
if (CurrentFile == file)
|
||||
{
|
||||
CurrentFile = null;
|
||||
CurrentFileChanged?.Invoke(CurrentFile);
|
||||
}
|
||||
|
||||
if (SelectedFile == file)
|
||||
{
|
||||
SelectedFile = null;
|
||||
}
|
||||
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution;
|
||||
updatedSolution = updatedSolution.RemoveDocument(file.Id);
|
||||
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution))
|
||||
throw new InvalidOperationException($"Xóa file {file.Path} trong workspace thất bại");
|
||||
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Start();
|
||||
|
||||
RootChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void RemoveFolder(ScriptFolder folder)
|
||||
{
|
||||
if (folder.Parent is null)
|
||||
{
|
||||
WorkspaceFolders.Remove(folder);
|
||||
}
|
||||
else
|
||||
{
|
||||
folder.Parent.RemoveFolder(folder);
|
||||
}
|
||||
|
||||
// Clear selection if selected file/folder is in the deleted folder
|
||||
if (SelectedFile != null && SelectedFile.Path.StartsWith(folder.Path + System.IO.Path.DirectorySeparatorChar))
|
||||
{
|
||||
SelectedFile = null;
|
||||
}
|
||||
|
||||
if (SelectedFolder != null && (SelectedFolder.Path == folder.Path || SelectedFolder.Path.StartsWith(folder.Path + System.IO.Path.DirectorySeparatorChar)))
|
||||
{
|
||||
SelectedFolder = null;
|
||||
}
|
||||
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution;
|
||||
RemoveFolderFromWorkspace(updatedSolution, folder);
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution))
|
||||
throw new InvalidOperationException($"Xóa folder {folder.Path} trong workspace thất bại");
|
||||
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Start();
|
||||
|
||||
RootChanged?.Invoke();
|
||||
}
|
||||
|
||||
private ScriptFile CreateWrokspaceFile(ScriptFileDto file, ScriptFolder? parent = null)
|
||||
{
|
||||
var filePath = System.IO.Path.Combine(parent?.Path ?? "", file.Name);
|
||||
|
||||
Solution updatedSolution = adhocWorkspace.CurrentSolution;
|
||||
var newId = DocumentId.CreateNewId(ProjectId);
|
||||
|
||||
updatedSolution = updatedSolution.AddDocument(newId, file.Name, SourceText.From(file.Code), parent?.Path.Split("/"), filePath);
|
||||
|
||||
if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Tạo document mới thất bại");
|
||||
if (!string.IsNullOrEmpty(file.Code))
|
||||
{
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Start();
|
||||
}
|
||||
var scriptFile = new ScriptFile(newId, file, parent);
|
||||
return scriptFile;
|
||||
}
|
||||
|
||||
private ScriptFolder CreateWorkspaceFolder(ScriptFolderDto folder, ScriptFolder? parent = null)
|
||||
{
|
||||
var folderPath = System.IO.Path.Combine(parent?.Path ?? "", folder.Name);
|
||||
var model = new ScriptFolder(folder, parent);
|
||||
// Materialize the Select to avoid multiple enumerations
|
||||
var files = folder.Files.Select(file => CreateWrokspaceFile(file, model)).ToList();
|
||||
model.AddFiles(files);
|
||||
// Materialize the Select to avoid multiple enumerations
|
||||
var subfolders = folder.Folders.Select(dir => CreateWorkspaceFolder(dir, model)).ToList();
|
||||
model.AddFolders(subfolders);
|
||||
return model;
|
||||
}
|
||||
|
||||
private void RemoveFolderFromWorkspace(Solution updatedSolution, ScriptFolder folder)
|
||||
{
|
||||
foreach (var dir in folder.Folders)
|
||||
{
|
||||
RemoveFolderFromWorkspace(updatedSolution, dir);
|
||||
}
|
||||
foreach (var file in folder.Files)
|
||||
{
|
||||
if (CurrentFile == file)
|
||||
{
|
||||
CurrentFile = null;
|
||||
CurrentFileChanged?.Invoke(CurrentFile);
|
||||
}
|
||||
updatedSolution = updatedSolution.RemoveDocument(file.Id);
|
||||
}
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Start();
|
||||
}
|
||||
|
||||
private async Task DiagnosticProject()
|
||||
{
|
||||
var editorProject = adhocWorkspace.CurrentSolution.GetProject(ProjectId);
|
||||
if (editorProject == null) return;
|
||||
|
||||
var compilation = await editorProject.GetCompilationAsync();
|
||||
var diagnostics = compilation?.GetDiagnostics() ?? [];
|
||||
|
||||
foreach (var file in Files)
|
||||
{
|
||||
await GetDiagnosticsToModel(editorProject, diagnostics, file);
|
||||
}
|
||||
|
||||
foreach (var folder in Folders)
|
||||
{
|
||||
await GetDiagnosticsToModel(editorProject, diagnostics, folder);
|
||||
}
|
||||
|
||||
if (CurrentFile is not null)
|
||||
{
|
||||
DiagnoticChanged?.Invoke(CurrentFile.Diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task GetDiagnosticsToModel(Project project, IEnumerable<Diagnostic> diagnostics, ScriptFile file)
|
||||
{
|
||||
var document = project.Solution.GetDocument(file.Id);
|
||||
if (document == null) return;
|
||||
|
||||
var syntaxTree = await document.GetSyntaxTreeAsync();
|
||||
if (syntaxTree == null) return;
|
||||
|
||||
file.Diagnostics = diagnostics.Where(d => d.Location.IsInSource && d.Location.SourceTree == syntaxTree);
|
||||
}
|
||||
|
||||
private static async Task GetDiagnosticsToModel(Project project, IEnumerable<Diagnostic> diagnostics, ScriptFolder folder)
|
||||
{
|
||||
foreach (var file in folder.Files)
|
||||
{
|
||||
await GetDiagnosticsToModel(project, diagnostics, file);
|
||||
}
|
||||
foreach (var subFolder in folder.Folders)
|
||||
{
|
||||
await GetDiagnosticsToModel(project, diagnostics, subFolder);
|
||||
}
|
||||
|
||||
// Recalculate totals for this folder after all children have been updated
|
||||
RecalculateFolderTotals(folder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively recalculates WarningCount, ErrorCount, and IsModified for a folder and all its children.
|
||||
/// </summary>
|
||||
private static void RecalculateFolderTotals(ScriptFolder folder)
|
||||
{
|
||||
folder.RecalculateTotals();
|
||||
}
|
||||
|
||||
private void DiagnosticTimer_Elapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
Task.Run(DiagnosticProject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Stop and dispose the diagnostic timer
|
||||
DiagnosticTimer.Stop();
|
||||
DiagnosticTimer.Elapsed -= DiagnosticTimer_Elapsed;
|
||||
DiagnosticTimer.Dispose();
|
||||
|
||||
// Dispose the adhoc workspace
|
||||
adhocWorkspace.Dispose();
|
||||
|
||||
// Clear event handlers to prevent memory leaks
|
||||
ReadOnlyChanged = null;
|
||||
DiagnoticChanged = null;
|
||||
RootChanged = null;
|
||||
|
||||
// Clear collections
|
||||
WorkspaceFiles.Clear();
|
||||
WorkspaceFolders.Clear();
|
||||
|
||||
// Reset state
|
||||
IsInitialized = false;
|
||||
SelectedFile = null;
|
||||
SelectedFolder = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static string BuildDevelopGlobalsScript(Type glovalType)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Build properties
|
||||
foreach (var property in glovalType.GetProperties())
|
||||
{
|
||||
if (property.CanRead)
|
||||
{
|
||||
var setter = property.CanWrite ? "set; " : "";
|
||||
sb.AppendLine($"{ScriptHelpers.ToString(property.PropertyType)} {property.Name} {{ get => throw new System.NotImplementedException(); {setter}}}");
|
||||
}
|
||||
else if (property.CanWrite)
|
||||
{
|
||||
sb.AppendLine($"{ScriptHelpers.ToString(property.PropertyType)} {property.Name} {{ set => throw new System.NotImplementedException(); }}");
|
||||
}
|
||||
}
|
||||
|
||||
// Build fields
|
||||
foreach (var field in glovalType.GetFields())
|
||||
{
|
||||
sb.AppendLine($"{ScriptHelpers.ToString(field.FieldType)} {field.Name};");
|
||||
}
|
||||
|
||||
// Build methods
|
||||
foreach (var method in glovalType.GetMethods())
|
||||
{
|
||||
if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue;
|
||||
|
||||
var parameters = string.Join(',', method.GetParameters().Select(parameter => ScriptHelpers.ToString(parameter)));
|
||||
sb.AppendLine($"{ScriptHelpers.ToString(method.ReturnType)} {method.Name}({parameters}) => throw new System.NotImplementedException();");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user