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 Folders => WorkspaceFolders; public IEnumerable Files => WorkspaceFiles; public event Action? ReadOnlyChanged; public event Action>? DiagnoticChanged; public event Action? RootChanged; public event Action? CurrentFileChanged; public event Action? SelectedFileChanged; public event Action? 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 WorkspaceFolders = []; private readonly List 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 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 GetQuickInfoCurrentFile(int line, int column) { if (CurrentFile is null) return null; return await adhocWorkspace.GetQuickInfoAsync(CurrentFile.Id, line, column); } public async Task> 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 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 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 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); } /// /// Recursively recalculates WarningCount, ErrorCount, and IsModified for a folder and all its children. /// 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(); } }