diff --git a/GlitchyEditor/src/Multithreading/BackgroundTask.bf b/GlitchyEditor/src/Multithreading/BackgroundTask.bf index f3b75e9..2b7fd2e 100644 --- a/GlitchyEditor/src/Multithreading/BackgroundTask.bf +++ b/GlitchyEditor/src/Multithreading/BackgroundTask.bf @@ -50,7 +50,7 @@ abstract class BackgroundTask public bool Ended => Finished || Aborted; - public bool DeleteWhenStopped { get; set; } + public bool DeleteWhenEnded { get; set; } public abstract RunResult Run(); @@ -63,10 +63,7 @@ abstract class BackgroundTask public void Pause() { - if (State == .Ready || State == .Running) - { - State = .Paused; - } + _taskManager.Pause(this); } public void Abort() diff --git a/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf b/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf index b4702ff..386a68e 100644 --- a/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf +++ b/GlitchyEditor/src/Multithreading/BackgroundTaskManager.bf @@ -20,6 +20,8 @@ class BackgroundTaskManager private append BackgroundTaskList _waitingTasks = .(); private append Monitor _queueLock = .(); + private append Dictionary _deferredStateChanges = .(); + public void Init() { _run = true; @@ -54,6 +56,25 @@ class BackgroundTaskManager BackgroundTask.RunResult runResult = task.Run(); _runningTask = null; + if (_deferredStateChanges.TryGetValue(task, let desiredState)) + { + // If the task already finished or aborted by itself, we don't care about the desired state, because it no longer applies. + if (runResult != .Finished && runResult != .Abort) + { + switch (desiredState) + { + case .Paused: + runResult = .Pause; + case .Aborted: + runResult = .Abort; + default: + Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}."); + } + } + + _deferredStateChanges.Remove(task); + } + switch (runResult) { case .Continue: @@ -71,14 +92,14 @@ class BackgroundTaskManager case .Finished: task.State = .Finished; - if (task.DeleteWhenStopped) + if (task.DeleteWhenEnded) { delete task; } case .Abort: task.State = .Aborted; - if (task.DeleteWhenStopped) + if (task.DeleteWhenEnded) { delete task; } @@ -123,21 +144,10 @@ class BackgroundTaskManager Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(!task.Ended); - Application.Instance.InvokeOnMainThread(new () => - { - if (task.State == .Paused) - { - task.State = .Ready; - - using (_queueLock.Enter()) - { - Log.EngineLogger.AssertDebug(!_readyQueue.Contains(task)); - // We assume this was a user interaction so this task will get some priority. - _readyQueue.AddFirst(task); - } - } - return true; - }); + if (task.State == .Paused) + { + _deferredStateChanges.Add(task, .Ready); + } } internal void Pause(BackgroundTask task) @@ -145,20 +155,10 @@ class BackgroundTaskManager Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(!task.Ended); - Application.Instance.InvokeOnMainThread(new () => - { - if (task.State == .Ready) - { - task.State = .Ready; - - using (_queueLock.Enter()) - { - Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task)); - _waitingTasks.AddLast(task); - } - } - return true; - }); + if (task.State == .Ready || task.State == .Running) + { + _deferredStateChanges.Add(task, .Paused); + } } internal void AbortTask(BackgroundTask task) @@ -166,24 +166,10 @@ class BackgroundTaskManager Log.EngineLogger.AssertDebug(task._taskManager == this); Log.EngineLogger.AssertDebug(!task.Ended); - Application.Instance.InvokeOnMainThread(new () => - { - if (task.State != .Aborted) - { - task.State = .Aborted; - - using (_queueLock.Enter()) - { - _waitingTasks.Remove(task); - } - - if (task.DeleteWhenStopped) - { - delete task; - } - } - return true; - }); + if (task.State != .Aborted) + { + _deferredStateChanges.Add(task, .Aborted); + } } public void ImGuiRender() @@ -211,6 +197,59 @@ class BackgroundTaskManager wtask.OnRenderPopup(); wtask = wtask._next; } + + for (var (task, desiredState) in _deferredStateChanges) + { + bool deleteEntry = true; + + switch (desiredState) + { + case .Ready: + task.State = .Ready; + // We assume this was a user interaction so this task will get priority. + _readyQueue.AddFirst(task); + case .Paused: + if (task.Ready) + { + Log.EngineLogger.AssertDebug(!_waitingTasks.Contains(task)); + _waitingTasks.AddLast(task); + } + else if (task.Running) + { + // The task is running, we have to hope that it cooperates and handle the rest in the worker thread. + deleteEntry = false; + } + + task.State = .Paused; + case .Aborted: + if (task.Ready || task.Paused) + { + _waitingTasks.Remove(task); + + task.State = .Aborted; + + if (task.DeleteWhenEnded) + { + delete task; + task = null; + } + } + else if (task.Running) + { + task.State = .Aborted; + + // The task is running, we have to hope that it cooperates and handle the rest in the worker thread. + deleteEntry = false; + } + default: + Log.EngineLogger.Error($"Tasks can't explicitly switch to state {desiredState}."); + } + + if (deleteEntry) + { + @task.Remove(); + } + } } } } \ No newline at end of file diff --git a/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf index ea691f2..661e739 100644 --- a/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf +++ b/GlitchyEditor/src/Multithreading/CopyBackgroundTask.bf @@ -3,6 +3,7 @@ using System.IO; using System.Collections; using System.Diagnostics; using ImGui; +using System.Threading; namespace GlitchyEditor.Multithreading; @@ -14,20 +15,25 @@ class CopyBackgroundTask : BackgroundTask private int _totalEntriesToCopy; private append Queue _pathsToCopy = .() ~ ClearAndDeleteItems!(_); private append Queue _scanQueue = .() ~ ClearAndDeleteItems!(_); + /// Contains the file paths, that will definitely exist after copying. This is used for file renaming during scanning. + private append HashSet _targetPaths = .(); - private enum OverwriteMode + private enum ConflictResolutionMode { None, OverwriteFile = 1, KeepBoth = 2, CombineDirectories = 4, - Skip = 8 + SkipFiles = 8, + SkipDirectories = 16, + SkipNotFound = 32, + IgnoreUnexpectedErrors = 64 } - private OverwriteMode _nextFileMode; - private OverwriteMode _allFilesMode; + private ConflictResolutionMode _nextFileMode; + private ConflictResolutionMode _allFilesMode; - private OverwriteMode CurrentFileMode => _nextFileMode | _allFilesMode; + private ConflictResolutionMode CurrentFileMode => _nextFileMode | _allFilesMode; private class CopyInfo { @@ -35,14 +41,14 @@ class CopyBackgroundTask : BackgroundTask // If this entry is actually inside a directory we copy, this contains the path of this entry inside this directory. public append String SubPath = .(); public append String TargetPath = .(); - public OverwriteMode OverwriteMode; + public ConflictResolutionMode ConflictResolutionMode; [AllowAppend] - public this(StringView sourcePath, StringView targetPath, OverwriteMode overwriteMode) + public this(StringView sourcePath, StringView targetPath, ConflictResolutionMode conflictResolutionMode) { SourcePath.Set(sourcePath); TargetPath.Set(targetPath); - OverwriteMode = overwriteMode; + ConflictResolutionMode = conflictResolutionMode; } } @@ -70,9 +76,10 @@ class CopyBackgroundTask : BackgroundTask { case None; case TargetDirectoryIsNotDirectory; - case SourceDoesntExist(String SourcePath); case TargetFileExists(String FileName); case TargetDirectoryExists(String DirectoryName); + case EntryNotFound(String SourcePath); + case UnexpectedError(String Message); public void Dispose() { @@ -80,12 +87,14 @@ class CopyBackgroundTask : BackgroundTask { case .None: case .TargetDirectoryIsNotDirectory: - case .SourceDoesntExist(let SourcePath): - delete SourcePath; case .TargetFileExists(let FileName): delete FileName; case .TargetDirectoryExists(let DirectoryName): delete DirectoryName; + case .EntryNotFound(let SourcePath): + delete SourcePath; + case .UnexpectedError(let Message): + delete Message; } } } @@ -101,10 +110,16 @@ class CopyBackgroundTask : BackgroundTask } } + void AddToCopyQueue(CopyInfo entry, StringView targetPath, ConflictResolutionMode conflictResolutionMode) + { + entry.TargetPath.Set(targetPath); + entry.ConflictResolutionMode = conflictResolutionMode; + _pathsToCopy.Add(entry); + _targetPaths.Add(entry.TargetPath); + } + while (!_scanQueue.IsEmpty && Running) { - //Thread.Sleep(1000); - currentEntry = _scanQueue.PopFront(); let sourcePath = (StringView)currentEntry.SourcePath; @@ -117,49 +132,90 @@ class CopyBackgroundTask : BackgroundTask if (Directory.Exists(sourcePath)) { - if (Directory.Exists(targetPath)) + bool skip = false; + + if (Directory.Exists(targetPath) || _targetPaths.Contains(targetPath)) { - switch (CurrentFileMode) + if (CurrentFileMode.HasFlag(.KeepBoth)) + { + Path.FindFreePath(_targetDirectoryPath, fileName, "", targetPath, fileName, _targetPaths); + } + else if (CurrentFileMode.HasFlag(.SkipDirectories)) + { + skip = true; + } + else if (Enum.HasAnyFlag(CurrentFileMode, .CombineDirectories)) + { + // Intentionally do nothing. + } + else { - case .KeepBoth, .CombineDirectories, .Skip: - default: return .Err(.TargetDirectoryExists(new String(fileName))); } } - if (!CurrentFileMode.HasFlag(.Skip)) + if (skip) { - _pathsToCopy.Add(currentEntry); - currentEntry.OverwriteMode = CurrentFileMode; + delete currentEntry; + } + else + { + // Current node is ready, add it to copy-queue. + AddToCopyQueue(currentEntry, targetPath, CurrentFileMode); + + // Add nested files and directories to the scan-queue. for (FileFindEntry e in Directory.Enumerate(currentEntry.SourcePath)) { let entryPath = scope String(); e.GetFilePath(entryPath); - let newEntry = new CopyInfo(entryPath, "", .None); + let newEntry = new CopyInfo(entryPath, targetPath, .None); _scanQueue.Add(newEntry); + Path.Combine(newEntry.SubPath, currentEntry.SubPath, fileName); } } + _nextFileMode = .None; } else if (File.Exists(sourcePath)) { - if (File.Exists(targetPath)) + bool skip = false; + + if (File.Exists(targetPath) || _targetPaths.Contains(targetPath)) { - switch (CurrentFileMode) + if (CurrentFileMode.HasFlag(.KeepBoth)) + { + let fileExtension = scope String(); + Path.GetExtension(targetPath, fileExtension); + + StringView fileNameWithoutExtension = fileName.Substring(0..<^fileExtension.Length); + + Path.FindFreePath(_targetDirectoryPath, fileNameWithoutExtension, fileExtension, targetPath, fileName, _targetPaths); + } + else if (CurrentFileMode.HasFlag(.SkipFiles)) + { + skip = true; + } + else if (Enum.HasAnyFlag(CurrentFileMode, .OverwriteFile)) + { + // Intentionally do nothing. + } + else { - case .KeepBoth, .OverwriteFile, .Skip: - default: return .Err(.TargetFileExists(new String(fileName))); } } - - if (!CurrentFileMode.HasFlag(.Skip)) + + if (skip) { - _pathsToCopy.Add(currentEntry); - currentEntry.OverwriteMode = CurrentFileMode; + delete currentEntry; } + else + { + AddToCopyQueue(currentEntry, targetPath, CurrentFileMode); + } + _nextFileMode = .None; } } @@ -175,78 +231,76 @@ class CopyBackgroundTask : BackgroundTask { _currentError.Dispose(); _currentError = .None; - if (CollectFilesToCopy() case .Err(out _currentError)) + if (CollectFilesToCopy() case .Err(out _currentError) || Paused) { return .Pause; } while (!_pathsToCopy.IsEmpty && Running) { - //Thread.Sleep(1000); CopyInfo currentPath = _pathsToCopy.PopFront(); - defer + if (CopyPath(currentPath) case .Err(out _currentError) || Paused) { - delete currentPath; + return .Pause; } - CopyPath(currentPath); + delete currentPath; } return .Finished; } - private Result CopyPath(CopyInfo copyInfo) + private Result CopyPath(CopyInfo copyInfo) { if (Directory.Exists(copyInfo.SourcePath)) { - //bool forceOverwrite = false; if (Directory.Exists(copyInfo.TargetPath)) { - switch (CurrentFileMode) + if (copyInfo.ConflictResolutionMode.HasFlag(.CombineDirectories)) { - case .KeepBoth: - String fileName = scope .(); - Path.GetFileName(copyInfo.SourcePath, fileName); - - Path.FindFreePath(_targetDirectoryPath, fileName, "", copyInfo.TargetPath..Clear()); - case .CombineDirectories: + // We don't have to do anything, because we will be using the target directory. return .Ok; - default: - return .Err;//(.TargetDirectoryExists); + } + else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"The target directory already exists, but it wasn't properly handled during the scanning phase.")); } } - Directory.CreateDirectory(copyInfo.TargetPath); + if (Directory.CreateDirectory(copyInfo.TargetPath) case .Err(let err) && + !CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"Failed to create Directory {copyInfo.TargetPath}: {err}")); + } } else if (File.Exists(copyInfo.SourcePath)) { bool forceOverwrite = false; if (File.Exists(copyInfo.TargetPath)) { - switch (copyInfo.OverwriteMode) + if (copyInfo.ConflictResolutionMode.HasFlag(.OverwriteFile)) { - case .KeepBoth: - String fileExtension = scope .(); - Path.GetExtension(copyInfo.TargetPath, fileExtension); - - String fileName = scope .(); - Path.GetFileName(copyInfo.SourcePath, fileName); - - Path.FindFreePath(_targetDirectoryPath, fileName.Substring(0..<^fileExtension.Length), fileExtension, copyInfo.TargetPath..Clear()); - case .OverwriteFile: forceOverwrite = true; - default: - return .Err;//(.TargetFileExists); + } + else if (!CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"The target file already exists, but it wasn't properly handled during the scanning phase.")); } } - - File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite); + + if (File.Copy(copyInfo.SourcePath, copyInfo.TargetPath, forceOverwrite) case .Err(let err) && + !CurrentFileMode.HasFlag(.IgnoreUnexpectedErrors)) + { + return .Err(.UnexpectedError(new $"Failed to copy File {copyInfo.TargetPath}: {err}")); + } } else { - // TODO error - return .Err; + if (copyInfo.ConflictResolutionMode.HasFlag(.SkipNotFound)) + return .Ok; + else + return .Err(.EntryNotFound(new String(copyInfo.SourcePath))); } return .Ok; @@ -254,7 +308,9 @@ class CopyBackgroundTask : BackgroundTask public override void OnRenderPopup() { - if (ImGui.Begin("Copying files...")) + String title = scope .("Copying files..."); + + if (ImGui.Begin(title, null, .NoDocking | .NoCollapse | .Modal | .NoResize | .AlwaysAutoResize)) { if (ScanningFiles) { @@ -267,89 +323,188 @@ class CopyBackgroundTask : BackgroundTask ImGui.ProgressBar(copiedFiles / _totalEntriesToCopy, .(-1, 0), scope $"Copied {copiedFiles} / {_totalEntriesToCopy} files."); } + switch (_currentError) { - case .None: - // Do nothing - case .TargetDirectoryIsNotDirectory: - // This shouldn't be possible - Debug.Break(); - case .SourceDoesntExist(let SourcePath): - // This isn't good! case .TargetFileExists(let fileName): ImGui.PushStyleColor(.Text, 0xFF0000FF); ImGui.TextUnformatted(scope $"A file with the name \"{fileName}\" already exists."); ImGui.PopStyleColor(); - - if (ImGui.Button("Overwrite")) - { - _nextFileMode = .OverwriteFile; - Continue(); - } - ImGui.AttachTooltip("Overwrites the existing file. Will ask again if another conflict occurs."); - - if (ImGui.Button("Overwrite All")) - { - _allFilesMode = .OverwriteFile; - Continue(); - } - ImGui.AttachTooltip("Overwrites all existing files."); - - if (ImGui.Button("Keep both")) - { - _nextFileMode = .KeepBoth; - Continue(); - } - ImGui.AttachTooltip("Keeps both the existing file as well as the new one, differentiates by adding a number to the name. Will ask again if another conflict occurs."); - - if (ImGui.Button("Keep all")) - { - _allFilesMode = .KeepBoth; - Continue(); - } - ImGui.AttachTooltip("Keeps both the existing files as well as the new ones, differentiates by adding a number to the name."); - - if (ImGui.Button("Cancel")) - { - Abort(); - } case .TargetDirectoryExists(let directoryName): ImGui.PushStyleColor(.Text, 0xFF0000FF); - ImGui.TextUnformatted(scope $"A file with the name \"{directoryName}\" already exists."); + ImGui.TextUnformatted(scope $"A directory with the name \"{directoryName}\" already exists."); ImGui.PopStyleColor(); - - if (ImGui.Button("Combine")) + case .EntryNotFound(let SourcePath): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"\"{SourcePath}\" doesn't exist."); + ImGui.PopStyleColor(); + case .UnexpectedError(let Message): + ImGui.PushStyleColor(.Text, 0xFF0000FF); + ImGui.TextUnformatted(scope $"Unexpected error: {Message}"); + ImGui.PopStyleColor(); + default: + // Do nothing + } + + if (ImGui.BeginTable("buttonTable", 2)) + { + switch (_currentError) { - _nextFileMode = .CombineDirectories; - Continue(); - } - ImGui.AttachTooltip("Merges the copied directory into the target directory. Will ask again if another conflict occurs."); + case .None: + // Do nothing + case .TargetDirectoryIsNotDirectory: + // This shouldn't be possible + Debug.Break(); + case .TargetFileExists(let fileName): + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); - if (ImGui.Button("Combine All")) - { - _allFilesMode = .CombineDirectories; - Continue(); - } - ImGui.AttachTooltip("Merges all copied directories into the target directories."); + if (ImGui.Button("Overwrite the target file.")) + { + _nextFileMode = .OverwriteFile; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Overwrite all conflicting target files.")) + { + _allFilesMode |= .OverwriteFile; + Continue(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); - if (ImGui.Button("Keep both")) - { - _nextFileMode = .KeepBoth; - Continue(); - } - ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict. Will ask again if another conflict occurs."); + if (ImGui.Button("Rename copied file.")) + { + _nextFileMode = .KeepBoth; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Rename all conflicting copied files.")) + { + _allFilesMode |= .KeepBoth; + Continue(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); - if (ImGui.Button("Keep all")) - { - _allFilesMode = .KeepBoth; - Continue(); + if (ImGui.Button("Skip this file.")) + { + _nextFileMode = .SkipFiles; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Skip all conflicting files.")) + { + _allFilesMode |= .SkipFiles; + Continue(); + } + + case .TargetDirectoryExists(let directoryName): + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + + if (ImGui.Button("Combine with target.")) + { + _nextFileMode |= .CombineDirectories; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Combine all conflicting directories.")) + { + _allFilesMode |= .CombineDirectories; + Continue(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + + if (ImGui.Button("Rename copied directory.")) + { + _nextFileMode |= .KeepBoth; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Rename all conflicting directories.")) + { + _allFilesMode |= .KeepBoth; + Continue(); + } + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + + if (ImGui.Button("Skip this directory.")) + { + _nextFileMode |= .SkipDirectories; + Continue(); + } + ImGui.TableSetColumnIndex(1); + if (ImGui.Button("Skip all conflicting directories.")) + { + _allFilesMode |= .SkipFiles; + Continue(); + } + case .EntryNotFound(let SourcePath): + if (ImGui.Button("Skip")) + { + _nextFileMode |= .SkipNotFound; + Continue(); + } + ImGui.SameLine(); + if (ImGui.Button("Skip all")) + { + _allFilesMode |= .SkipNotFound; + Continue(); + } + case .UnexpectedError(let Message): + if (ImGui.Button("Retry")) + { + Continue(); + } + ImGui.SameLine(); + if (ImGui.Button("Ignore")) + { + _nextFileMode |= .IgnoreUnexpectedErrors; + Continue(); + } + ImGui.SameLine(); + if (ImGui.Button("Ignore all unexpected")) + { + _allFilesMode |= .IgnoreUnexpectedErrors; + Continue(); + } } - ImGui.AttachTooltip("Renames the copied directory, so that they no longer conflict."); + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + + if (_currentError case .None) + { + if (Paused) + { + if (ImGui.Button("Continue")) + { + Continue(); + } + } + else + { + if (ImGui.Button("Pause")) + { + Pause(); + } + } + + ImGui.SameLine(); + } + if (ImGui.Button("Cancel")) { Abort(); } + ImGui.EndTable(); } ImGui.End(); diff --git a/GlitchyEngine/src/Extension/System/Collections/HashSet.bf b/GlitchyEngine/src/Extension/System/Collections/HashSet.bf new file mode 100644 index 0000000..387d398 --- /dev/null +++ b/GlitchyEngine/src/Extension/System/Collections/HashSet.bf @@ -0,0 +1,22 @@ +namespace System.Collections; + +extension HashSet : ICollection where T : IHashable +{ + public void ICollection.Add(T item) + { + Add(item); + } + + public void CopyTo(Span span) + { + int i = 0; + for (T item in this) + { + span[i] = item; + i++; + + if (i >= span.Length) + break; + } + } +} diff --git a/GlitchyEngine/src/Extension/System/Enum.bf b/GlitchyEngine/src/Extension/System/Enum.bf index f39a137..54930f2 100644 --- a/GlitchyEngine/src/Extension/System/Enum.bf +++ b/GlitchyEngine/src/Extension/System/Enum.bf @@ -19,4 +19,9 @@ extension Enum else ClearFlag(ref value, flag); } + + public static bool HasAnyFlag(T value, T flags) where T : enum + { + return (value.Underlying & flags.Underlying) != default; + } } diff --git a/GlitchyEngine/src/Extension/System/IO/Path.bf b/GlitchyEngine/src/Extension/System/IO/Path.bf index 8551a4c..cadb8f5 100644 --- a/GlitchyEngine/src/Extension/System/IO/Path.bf +++ b/GlitchyEngine/src/Extension/System/IO/Path.bf @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Collections; namespace System.IO; @@ -34,8 +35,8 @@ extension Path path.Remove(0, 1); } - - public static void Combine(String target, params StringView[] components) + // Compared to the original Combine, this one makes sure we don't add multiple seperators. Also makes sure, the path contains only the main Separator char. + new public static void Combine(String target, params StringView[] components) { for (var component in components) { @@ -54,25 +55,24 @@ extension Path * @param wantedFileName The wanted name of the file. If it already exists a number will be put behind it. * @param fileExtension The file extension. This should contain the dot. * @param outFreePath The string that will contain the resulting filepath. Note: This will be cleared before writing the file name. + * @param outFreeFilename Optional, if set to a string, the final file name that is free will be stored in this varialbe. + * @param blockedPaths Optional, can be used to provide a collection of paths that will be considered as existing. */ - public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath) + public static void FindFreePath(StringView targetDirectory, StringView wantedName, StringView fileExtension, String outFreePath, String outFreeFilename = null, ICollection blockedPaths = null) { int fileNumber = 0; - String currentFileName = scope $"{wantedName}{fileExtension}"; + String currentFileName = outFreeFilename ?? scope .(); + currentFileName.SetF($"{wantedName}{fileExtension}"); while (true) { Path.Combine(outFreePath..Clear(), targetDirectory, currentFileName); - FileInfo targetInfo = scope FileInfo(outFreePath); - - if (!targetInfo.Exists) - { + if (!File.Exists(outFreePath) && !Directory.Exists(outFreePath) && !(blockedPaths?.Contains(outFreePath) ?? false)) break; - } fileNumber++; - currentFileName..Clear().AppendF($"{wantedName} ({fileNumber}){fileExtension}"); + currentFileName.SetF($"{wantedName} ({fileNumber}){fileExtension}"); } } } \ No newline at end of file