Make AllFiles threadsafe

This commit is contained in:
Matt Parker
2025-11-23 14:49:45 +10:00
parent 8ee1499894
commit a6fc3c8976
10 changed files with 35 additions and 23 deletions

View File

@@ -709,7 +709,7 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
.Select(async (Document doc, CancellationToken ct) =>
{
var text = await doc.GetTextAsync(ct);
var sharpFile = _sharpIdeSolutionModel!.AllFiles.Single(f => f.Path == doc.FilePath);
var sharpFile = _sharpIdeSolutionModel!.AllFiles[doc.FilePath!];
return (sharpFile, text.ToString());
})
.ToListAsync(cancellationToken);
@@ -723,8 +723,8 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
if (semanticModel is null) return null;
var enclosingSymbol = ReferenceLocationExtensions.GetEnclosingMethodOrPropertyOrField(semanticModel, referenceLocation);
var lineSpan = referenceLocation.Location.GetMappedLineSpan();
var file = _sharpIdeSolutionModel!.AllFiles.SingleOrDefault(f => f.Path == lineSpan.Path);
var result = new IdeReferenceLocationResult(referenceLocation, file!, enclosingSymbol);
var file = _sharpIdeSolutionModel!.AllFiles[lineSpan.Path];
var result = new IdeReferenceLocationResult(referenceLocation, file, enclosingSymbol);
return result;
}

View File

@@ -28,14 +28,14 @@ public class IdeFileExternalChangeHandler
private async Task OnFileRenamed(string oldFilePath, string newFilePath)
{
var sharpIdeFile = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == oldFilePath);
var sharpIdeFile = SolutionModel.AllFiles.GetValueOrDefault(oldFilePath);
if (sharpIdeFile is null) return;
await _sharpIdeSolutionModificationService.RenameFile(sharpIdeFile, Path.GetFileName(newFilePath));
}
private async Task OnFileDeleted(string filePath)
{
var sharpIdeFile = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == filePath);
var sharpIdeFile = SolutionModel.AllFiles.GetValueOrDefault(filePath);
if (sharpIdeFile is null) return;
await _sharpIdeSolutionModificationService.RemoveFile(sharpIdeFile);
}
@@ -90,7 +90,7 @@ public class IdeFileExternalChangeHandler
private async Task OnFileCreated(string filePath)
{
var sharpIdeFile = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == filePath);
var sharpIdeFile = SolutionModel.AllFiles.GetValueOrDefault(filePath);
if (sharpIdeFile is not null)
{
// It was likely already created via a parent folder creation
@@ -110,7 +110,7 @@ public class IdeFileExternalChangeHandler
private async Task OnFileChanged(string filePath)
{
var sharpIdeFile = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == filePath);
var sharpIdeFile = SolutionModel.AllFiles.GetValueOrDefault(filePath);
if (sharpIdeFile is null) return;
if (sharpIdeFile.SuppressDiskChangeEvents is true) return;
if (sharpIdeFile.LastIdeWriteTime is not null)
@@ -123,7 +123,7 @@ public class IdeFileExternalChangeHandler
}
}
_logger.LogInformation("IdeFileExternalChangeHandler: Changed - '{FilePath}'", filePath);
var file = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == filePath);
var file = SolutionModel.AllFiles.GetValueOrDefault(filePath);
if (file is not null)
{
await _fileChangedService.SharpIdeFileChanged(file, await File.ReadAllTextAsync(file.Path), FileChangeType.ExternalChange);

View File

@@ -1,14 +1,16 @@
using System.Collections.Concurrent;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
namespace SharpIDE.Application.Features.FileWatching;
/// Does not do any file system operations, only modifies the in-memory solution model
public class SharpIdeSolutionModificationService(FileChangedService fileChangedService)
public class SharpIdeSolutionModificationService(FileChangedService fileChangedService, ILogger<SharpIdeSolutionModificationService> logger)
{
private readonly FileChangedService _fileChangedService = fileChangedService;
private readonly ILogger<SharpIdeSolutionModificationService> _logger = logger;
public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
@@ -24,7 +26,11 @@ public class SharpIdeSolutionModificationService(FileChangedService fileChangedS
parentNode.Folders.Insert(correctInsertionPosition, sharpIdeFolder);
SolutionModel.AllFolders.AddRange((IEnumerable<SharpIdeFolder>)[sharpIdeFolder, ..allFolders]);
SolutionModel.AllFiles.AddRange(allFiles);
foreach (var sharpIdeFile in allFiles)
{
var success = SolutionModel.AllFiles.TryAdd(sharpIdeFile.Path, sharpIdeFile);
if (success is false) _logger.LogWarning("File {filePath} already exists in SolutionModel.AllFiles when adding directory {directoryPath}", sharpIdeFile.Path, addedDirectoryPath);
}
foreach (var file in allFiles)
{
await _fileChangedService.SharpIdeFileAdded(file, await File.ReadAllTextAsync(file.Path));
@@ -55,7 +61,11 @@ public class SharpIdeSolutionModificationService(FileChangedService fileChangedS
var filesToRemove = foldersToRemove.SelectMany(f => f.Files).ToList();
SolutionModel.AllFiles.RemoveRange(filesToRemove);
foreach (var sharpIdeFile in filesToRemove)
{
var success = SolutionModel.AllFiles.TryRemove(sharpIdeFile.Path, out _);
if (success is false) _logger.LogWarning("File {filePath} not found in SolutionModel.AllFiles when removing directory {directoryPath}", sharpIdeFile.Path, folder.Path);
}
SolutionModel.AllFolders.RemoveRange(foldersToRemove);
foreach (var file in filesToRemove)
{
@@ -138,7 +148,8 @@ public class SharpIdeSolutionModificationService(FileChangedService fileChangedS
var correctInsertionPosition = GetInsertionPosition(parentNode, sharpIdeFile);
parentNode.Files.Insert(correctInsertionPosition, sharpIdeFile);
SolutionModel.AllFiles.Add(sharpIdeFile);
var success = SolutionModel.AllFiles.TryAdd(sharpIdeFile.Path, sharpIdeFile);
if (success is false) _logger.LogWarning("File {filePath} already exists in SolutionModel.AllFiles when creating file", sharpIdeFile.Path);
await _fileChangedService.SharpIdeFileAdded(sharpIdeFile, contents);
return sharpIdeFile;
}
@@ -192,7 +203,8 @@ public class SharpIdeSolutionModificationService(FileChangedService fileChangedS
{
var parentFolderOrProject = (IFolderOrProject)file.Parent;
parentFolderOrProject.Files.Remove(file);
SolutionModel.AllFiles.Remove(file);
var success = SolutionModel.AllFiles.TryRemove(file.Path, out _);
if (success is false) _logger.LogWarning("File {filePath} not found in SolutionModel.AllFiles when removing file", file.Path);
await _fileChangedService.SharpIdeFileRemoved(file);
}

View File

@@ -18,7 +18,7 @@ public class SearchService(ILogger<SearchService> logger)
}
var timer = Stopwatch.StartNew();
var files = solutionModel.AllFiles;
var files = solutionModel.AllFiles.Values.ToList();
ConcurrentBag<FindInFilesSearchResult> results = [];
await Parallel.ForEachAsync(files, cancellationToken, async (file, ct) =>
{
@@ -52,7 +52,7 @@ public class SearchService(ILogger<SearchService> logger)
}
var timer = Stopwatch.StartNew();
var files = solutionModel.AllFiles;
var files = solutionModel.AllFiles.Values.ToList();
ConcurrentBag<FindFilesSearchResult> results = [];
await Parallel.ForEachAsync(files, cancellationToken, async (file, ct) =>
{

View File

@@ -55,7 +55,7 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode, ISo
public required ObservableHashSet<SharpIdeProjectModel> Projects { get; set; }
public required ObservableHashSet<SharpIdeSolutionFolder> SlnFolders { get; set; }
public required HashSet<SharpIdeProjectModel> AllProjects { get; set; } // TODO: this isn't thread safe
public required HashSet<SharpIdeFile> AllFiles { get; set; } // TODO: this isn't thread safe
public required ConcurrentDictionary<string, SharpIdeFile> AllFiles { get; set; }
public required HashSet<SharpIdeFolder> AllFolders { get; set; } // TODO: this isn't thread safe
public bool Expanded { get; set; }
@@ -72,7 +72,7 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode, ISo
Projects = new ObservableHashSet<SharpIdeProjectModel>(intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, allFolders, this)));
SlnFolders = new ObservableHashSet<SharpIdeSolutionFolder>(intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, allFolders, this)));
AllProjects = allProjects.ToHashSet();
AllFiles = allFiles.ToHashSet();
AllFiles = new ConcurrentDictionary<string, SharpIdeFile>(allFiles.DistinctBy(s => s.Path).ToDictionary(s => s.Path));
AllFolders = allFolders.ToHashSet();
}
}

View File

@@ -22,7 +22,7 @@ public static class VsPersistenceMapper
{
// Assumes solution file is at git repo root
var filePath = new FileInfo(Path.Combine(solutionModel.DirectoryPath, entry.FilePath)).FullName; // used to normalise path separators
var fileInSolution = solutionModel.AllFiles.SingleOrDefault(f => f.Path.Equals(filePath, StringComparison.OrdinalIgnoreCase));
var fileInSolution = solutionModel.AllFiles.GetValueOrDefault(filePath);
if (fileInSolution is null) continue;
var mappedGitStatus = entry.State switch

View File

@@ -126,7 +126,7 @@ public partial class CodeEditorPanel : MarginContainer
if (executionStopInfo.FilePath != currentSharpIdeFile?.Path)
{
var file = Solution.AllFiles.Single(s => s.Path == executionStopInfo.FilePath);
var file = Solution.AllFiles[executionStopInfo.FilePath];
await GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelAsync(file, null).ConfigureAwait(false);
}
var lineInt = executionStopInfo.Line - 1; // Debugging is 1-indexed, Godot is 0-indexed

View File

@@ -44,7 +44,7 @@ public partial class SharpIdeCodeEdit
var referenceLocation = locations[0];
var referenceLineSpan = referenceLocation.Location.GetMappedLineSpan();
var sharpIdeFile = Solution!.AllFiles.SingleOrDefault(f => f.Path == referenceLineSpan.Path);
var sharpIdeFile = Solution!.AllFiles.GetValueOrDefault(referenceLineSpan.Path);
if (sharpIdeFile is null)
{
GD.Print($"Reference file not found in solution: {referenceLineSpan.Path}");
@@ -82,7 +82,7 @@ public partial class SharpIdeCodeEdit
// Lets jump to the definition
var definitionLocation = locations[0];
var definitionLineSpan = definitionLocation.GetMappedLineSpan();
var sharpIdeFile = Solution!.AllFiles.SingleOrDefault(f => f.Path == definitionLineSpan.Path);
var sharpIdeFile = Solution!.AllFiles.GetValueOrDefault(definitionLineSpan.Path);
if (sharpIdeFile is null)
{
GD.Print($"Definition file not found in solution: {definitionLineSpan.Path}");

View File

@@ -119,7 +119,7 @@ public partial class ProblemsPanel : Control
private void OpenDocumentContainingDiagnostic(Diagnostic diagnostic)
{
var fileLinePositionSpan = diagnostic.Location.GetMappedLineSpan();
var file = Solution!.AllFiles.Single(f => f.Path == fileLinePositionSpan.Path);
var file = Solution!.AllFiles[fileLinePositionSpan.Path];
var linePosition = new SharpIdeFileLinePosition(fileLinePositionSpan.StartLinePosition.Line, fileLinePositionSpan.StartLinePosition.Character);
GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(file, linePosition);
}

View File

@@ -164,7 +164,7 @@ public partial class IdeRoot : Control
var previousTabs = Singletons.AppState.RecentSlns.Single(s => s.FilePath == solutionModel.FilePath).IdeSolutionState.OpenTabs;
var filesToOpen = previousTabs
.Select(s => (solutionModel.AllFiles.SingleOrDefault(f => f.Path == s.FilePath), new SharpIdeFileLinePosition(s.CaretLine, s.CaretColumn), s.IsSelected))
.Select(s => (solutionModel.AllFiles.GetValueOrDefault(s.FilePath), new SharpIdeFileLinePosition(s.CaretLine, s.CaretColumn), s.IsSelected))
.Where(s => s.Item1 is not null)
.OfType<(SharpIdeFile file, SharpIdeFileLinePosition linePosition, bool isSelected)>()
.ToList();