Skip to content

Quick Switch #1018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 202 commits into
base: dev
Choose a base branch
from
Open

Quick Switch #1018

wants to merge 202 commits into from

Conversation

taooceros
Copy link
Member

@taooceros taooceros commented Feb 10, 2022

Nothing more than quickswitch. We may integrate flow's path system to this feature instead of relying explorer.

Setup Quick Switch

  1. Quick switch key: Alt+G by default
  2. Quick switch automatically
  3. Quick switch window

Use Quick Switch

  1. Open explorer -> Open file dialog -> Use hotkey to navigate to that path.

  2. Open file dialog -> Query window (quick switch window) fixed under file dialog -> Click results to navigate to the selected path

Quick Switch API

Implement new api interfaces to let plugin be queried on quick switch window for dotnet plugins only.

public interface IAsyncQuickSwitch
{
    /// <summary>
    /// Asynchronous querying for quick switch window
    /// </summary>
    Task<List<QuickSwitchResult>> QueryQuickSwitchAsync(Query query, CancellationToken token);
}
public interface IQuickSwitch : IAsyncQuickSwitch
{
    /// <summary>
    /// Querying for quick switch window
    /// </summary>
    List<QuickSwitchResult> QueryQuickSwitch(Query query);

    Task<List<QuickSwitchResult>> IAsyncQuickSwitch.QueryQuickSwitchAsync(Query query, CancellationToken token) => Task.Run(() => QueryQuickSwitch(query));
}
/// <summary>
/// Describes a result of a <see cref="Query"/> executed by a plugin in quick switch window
/// </summary>
public class QuickSwitchResult : Result
{
    /// <summary>
    /// This holds the path which can be provided by plugin to be navigated to the
    /// file dialog when records in quick switch window is right clicked on a result.
    /// </summary>
    public required string QuickSwitchPath { get; init; }

    // ...
}

Additionally, Explorer plugin already supports quick switch.

Develop third party explorer & dialog

Check Flow.Launcher.Infrastructure.QuickSwitch.Interface.IQuickSwitchExplorer and Flow.Launcher.Infrastructure.QuickSwitch.Interface.IQuickSwitchDialog for more info.

Todos

  • https://github.com/listary/Listary.FileAppPlugin/blob/master/docs%2FGetting%20Started.md
  • Support opening file path.
  • Quick switch automatically issue for save as file dialog from @onesounds: For the Open dialog, the path changes correctly at the time the dialog is opened, even without switching focus to File Explorer.
    However, for the Save As dialog, the path does not apply immediately when the dialog is opened. It only works after switching focus to File Explorer and then returning.
  • Third party explorer.
  • Third party dialog.
  • In auto mode, the target program still closes when using "Open." (Previously reported issue) on some devices & apps from @onesounds.
  • Dialogs pop up on many other dialog windows.
  • When the bottom bar mode is active, if a dialog is closed and Flow is exited without being reopened, the window position is saved based on the bottom bar mode location. On the next launch, Flow opens at the last position used by the bottom bar mode.
  • In the "Save As" dialog, Flow automatically inputs the entire file name, which causes unintended saving.

@taooceros
Copy link
Member Author

Some threading issues seem appearing. Not sure the detailed reason.

@stefnotch
Copy link
Contributor

Okay, my main questions would be

  • What's the intended use-case for this? As in, when does a Flow plugin need to navigate in the actual file explorer, instead of opening a new one with the correct path.
  • Would opening a new file explorer window with the desired path, and closing the old one work? Or does that not handle certain cases? (e.g. the file browser/chooser )

@taooceros
Copy link
Member Author

Okay, my main questions would be

* What's the intended use-case for this? As in, when does a Flow plugin need to navigate in the actual file explorer, instead of opening a new one with the correct path.

I think the major use case is to sync the path of an opened explorer to a open file dialog to select a file easily.

Would opening a new file explorer window with the desired path, and closing the old one work? Or does that not handle certain cases? (e.g. the file browser/chooser )

Sry I don't get the idea.

@stefnotch
Copy link
Contributor

Okay, I understand what this is used for now. I'd have to dig a lot deeper into what the IUIAutomation can do to be able to improve this.

I think the rule of thumb is to avoid sending keyboard events, and instead always use an API if one exists. Keyboard events can be delayed and whatnot.

@taooceros
Copy link
Member Author

Okay, I understand what this is used for now. I'd have to dig a lot deeper into what the IUIAutomation can do to be able to improve this.

I think the rule of thumb is to avoid sending keyboard events, and instead always use an API if one exists. Keyboard events can be delayed and whatnot.

Yeah that's what I would like to see. It is possible to use PInvoke directly without IUIAutomation though, so it will be cool if you are familiar with that as well.

Another thing is the original listary seems implement this feature without changing the textbox and sending an enter signal, so I wonder whether you may have some clues about that.

@stefnotch
Copy link
Contributor

I tried searching for what I could, but that's apparently quite tricky to hook into. So I don't really have a better solution at the moment.

@taooceros
Copy link
Member Author

I tried searching for what I could, but that's apparently quite tricky to hook into. So I don't really have a better solution at the moment.

okay thanks🤣

@stefnotch
Copy link
Contributor

stefnotch commented Aug 17, 2022

There might be a alternate design:

So the file manager has the "quick access" sidebar. Flow could add its own entry there, and that entry always redirects to the currently open folder. An additional advantage might be that it's easier to discover this, compared to a keyboard shortcut.

Screenshot for context:

image

(Note: I have no idea how hard that would be to efficiently pull that off.)

@taooceros
Copy link
Member Author

So you mean to add a entry that redirect to the most recent opened explorer path?🤔Interesting

@stefnotch
Copy link
Contributor

Yep, spot-on.

@taooceros
Copy link
Member Author

Yep, spot-on.

If that's the case, we may be able to create a plugin for it.

@taooceros
Copy link
Member Author

Do you have any docs for that?

@stefnotch
Copy link
Contributor

stefnotch commented Aug 17, 2022

@taooceros I haven't looked into this all that much (just a few cursory google searches)

Programmatic access

Apparently there's a way of programmatically adding folders to the quick access area.

https://stackoverflow.com/questions/30051634/is-it-possible-programmatically-add-folders-to-the-windows-10-quick-access-panel

Special Links folder

https://blogs.msmvps.com/kenlin/2017/06/14/537/

Steps:

  1. Enable a special, built-in folder by setting a value in the system registry. Anything in this folder will land in the "quick access".
  2. Put a shortcut in that folder. (A .lnk shortcut)
  3. And then always update the shortcut's path to point at the currently open file explorer.

Symbolic links or Hardlink

I bet there's some trickery that could be done with those

Extra harddrive

We could add an in-memory harddrive, mount it and provide a single shortcut in there.
This might be a tad tricky though, depending on whether there's an easy API/wrapper or not...

@mcthesw
Copy link

mcthesw commented Oct 15, 2022

Could this be done? I really love this feature.

@VictoriousRaptor VictoriousRaptor linked an issue Nov 2, 2022 that may be closed by this pull request
@VictoriousRaptor VictoriousRaptor linked an issue Nov 27, 2022 that may be closed by this pull request
@stefnotch
Copy link
Contributor

Yet another option would be to add a "switch to" context menu entry

Sort of like how 7zip has a dynamic context menu, except that we'd populate it with the titles of other explorer windows.
image

@stefnotch
Copy link
Contributor

stefnotch commented Jan 8, 2023

Apparently Windows 11 can add files to quick access. That might let us pin a program to quick access

Such a program could then update the list of files in the quick access window.

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
Flow.Launcher/ViewModel/MainViewModel.cs (6)

373-386: ⚠️ Potential issue

Unhandled exceptions in async navigation task.

The Quick Switch navigation task is launched without error handling, causing any exceptions to be silently dropped.

Apply this fix to properly handle exceptions during navigation:

-            _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+            _ = Task.Run(async () =>
+            {
+                try
+                {
+                    await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)
+                                     .ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    App.API.LogException(ClassName, "Quick switch navigation failed", ex);
+                }
+            });

467-477: ⚠️ Potential issue

Same unhandled exception issue in left-click handler.

Similar to the right-click handler, this fire-and-forget task doesn't handle exceptions.

Apply the same fix pattern here:

-            _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+            _ = Task.Run(async () =>
+            {
+                try
+                {
+                    await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)
+                                     .ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    App.API.LogException(ClassName, "Quick switch navigation failed", ex);
+                }
+            });

1766-1769: ⚠️ Potential issue

Add thread-safety to Quick Switch state variables.

These properties are accessed from multiple threads (UI thread, background tasks) without synchronization.

+private readonly object _quickSwitchLock = new();
+private nint _dialogWindowHandle = nint.Zero;
+private bool _isQuickSwitch = false;

-public nint DialogWindowHandle { get; private set; } = nint.Zero;
+public nint DialogWindowHandle 
+{ 
+   get 
+   {
+       lock (_quickSwitchLock)
+       {
+           return _dialogWindowHandle;
+       }
+   }
+   private set 
+   {
+       lock (_quickSwitchLock)
+       {
+           _dialogWindowHandle = value;
+       }
+   }
+}

-private bool _isQuickSwitch = false;
+private bool IsQuickSwitch 
+{ 
+   get 
+   {
+       lock (_quickSwitchLock)
+       {
+           return _isQuickSwitch;
+       }
+   }
+   set 
+   {
+       lock (_quickSwitchLock)
+       {
+           _isQuickSwitch = value;
+       }
+   }
+}

1784-1870: ⚠️ Potential issue

Fix token capture race condition in SetupQuickSwitchAsync.

Creating a new CancellationTokenSource and using its token inside a Task.Run lambda creates a race condition if another dialog arrives before the task runs.

// Create a new cancellation token source
_quickSwitchSource = new CancellationTokenSource();
+// Capture token locally to avoid race conditions
+var token = _quickSwitchSource.Token;

_ = Task.Run(() =>
    {
        try
        {
            // Check task cancellation
-           if (_quickSwitchSource.Token.IsCancellationRequested) return;
+           if (token.IsCancellationRequested) return;

            // Check dialog handle
            if (DialogWindowHandle == nint.Zero) return;

            // Wait 150ms to check if quick switch window gets the focus
            var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
            if (timeOut) return;

            // Bring focus back to the the dialog
            Win32Helper.SetForegroundWindow(DialogWindowHandle);
        }
        catch (Exception e)
        {
            App.API.LogException(ClassName, "Failed to focus on dialog window", e);
        }
    });

1874-1914: ⚠️ Potential issue

Change async void to Task return type.

Async void methods can cause unhandled exceptions and are harder to test and use properly.

-public async void ResetQuickSwitch()
+public async Task ResetQuickSwitch()
{
    if (DialogWindowHandle == nint.Zero) return;

    DialogWindowHandle = nint.Zero;
    _isQuickSwitch = false;

    if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus)
    {
        // Show or hide to change visibility
        if (_previousMainWindowVisibilityStatus)
        {
            Show();

            _ = ResetWindowAsync();
        }
        else
        {
            await ResetWindowAsync();

            Hide(false);
        }
    }
    else
    {
        if (_previousMainWindowVisibilityStatus)
        {
            // Only update the position
            Application.Current?.Dispatcher.Invoke(() =>
            {
                (Application.Current?.MainWindow as MainWindow).UpdatePosition();
            });

            _ = ResetWindowAsync();
        }
        else
        {
            _ = ResetWindowAsync();
        }
    }
}

1918-1933: ⚠️ Potential issue

Verify window handle validity in HideQuickSwitch.

The method assumes the DialogWindowHandle is valid if non-zero, but Windows handles can be reused or invalidated.

public void HideQuickSwitch()
{
    if (DialogWindowHandle != nint.Zero)
    {
+       // Verify the handle is still valid
+       if (!Win32Helper.IsWindow(DialogWindowHandle))
+       {
+           DialogWindowHandle = nint.Zero;
+           _isQuickSwitch = false;
+           return;
+       }
+       
        if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog)
        {
            // Warning: Main window is already in foreground
            // This is because if you click popup menus in other applications to hide quick switch window,
            // they can steal focus before showing main window
            if (MainWindowVisibilityStatus)
            {
                Hide();
            }
        }
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60bbf75 and 2b4f777.

📒 Files selected for processing (1)
  • Flow.Launcher/ViewModel/MainViewModel.cs (16 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (4)
Flow.Launcher/ViewModel/MainViewModel.cs (4)

56-56: LGTM: Added empty QuickSwitchResult list.

A read-only list of empty QuickSwitchResult objects - a good practice to avoid creating new collections unnecessarily.


1525-1540: LGTM: Well-structured ClearResults method.

The method properly handles the three main UI aspects: clearing results, resetting plugin icon, and hiding the progress bar.


1936-1956: LGTM: Well-structured ResetWindowAsync method.

This method properly handles all aspects of window reset: resetting history, closing previews, selecting results, and clearing query text.


2177-2177: Good resource management for QuickSwitch.

Properly disposing the QuickSwitch cancellation token source in the Dispose method prevents potential memory leaks.

This comment has been minimized.

This comment has been minimized.

@jjw24
Copy link
Member

jjw24 commented May 11, 2025

@Jack251970 I haven't looked at the code but for Files Explorer feature, can we separate it into another PR? Try to keep PR focused on one feature (ideally smaller chunks of a large PR so it's easier for review but I know that's not always applicable)

@Jack251970
Copy link
Contributor

Jack251970 commented May 11, 2025

@Jack251970 I haven't looked at the code but for Files Explorer feature, can we separate it into another PR? Try to keep PR focused on one feature (ideally smaller chunks of a large PR so it's easier for review but I know that's not always applicable)

Sorry we cannot do that. Files support is from Quick Switch feature (Quick Switch needs support for third-party explorers). I just refactor original FileExplorerHelper.cs to incorporate changes from Quick Switch.

@Jack251970
Copy link
Contributor

Well, I can remove all changes in FileExplorerHelper.cs and follow on with another PR after this is merged.

This comment has been minimized.

@Jack251970
Copy link
Contributor

@Jack251970 I haven't looked at the code but for Files Explorer feature, can we separate it into another PR? Try to keep PR focused on one feature (ideally smaller chunks of a large PR so it's easier for review but I know that's not always applicable)

Well, removed all changes for Files explorer support.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (7)
Flow.Launcher/ViewModel/MainViewModel.cs (7)

518-519: ⚠️ Potential issue

Add type checking to prevent casting exceptions

Casting directly to QuickSwitchResult without checking the type first could lead to InvalidCastException.

- var resultCopy = ((QuickSwitchResult)result).Clone();
- resultsCopy.Add(resultCopy);
+ if (result is QuickSwitchResult quickSwitchResult)
+ {
+     var resultCopy = quickSwitchResult.Clone();
+     resultsCopy.Add(resultCopy);
+ }
+ else
+ {
+     App.API.LogWarning(ClassName, $"Expected QuickSwitchResult but got {result.GetType().Name}");
+     var resultCopy = result.Clone();
+     resultsCopy.Add(resultCopy);
+ }

1782-1798: 🛠️ Refactor suggestion

Replace async void with async Task and add null check for Application.Current

You should avoid async void methods for better exception handling, and add null check for Application.Current to prevent potential NullReferenceException.

- public async Task SetupQuickSwitchAsync(nint handle)
+ public async Task SetupQuickSwitchAsync(nint handle)
  {
      if (handle == nint.Zero) return;

      // Only set flag & reset window once for one file dialog
      var dialogWindowHandleChanged = false;
      if (DialogWindowHandle != handle)
      {
          DialogWindowHandle = handle;
          _previousMainWindowVisibilityStatus = MainWindowVisibilityStatus;
          _isQuickSwitch = true;

          dialogWindowHandleChanged = true;

          // If don't give a time, Positioning will be weird
          await Task.Delay(300);
      }
  }

1846-1866: ⚠️ Potential issue

Capture cancellation token locally to avoid race conditions

Accessing _quickSwitchSource.Token inside the lambda could lead to race conditions if _quickSwitchSource is modified by another thread.

// Create a new cancellation token source
_quickSwitchSource = new CancellationTokenSource();
+var token = _quickSwitchSource.Token;

_ = Task.Run(() =>
    {
        try
        {
            // Check task cancellation
-           if (_quickSwitchSource.Token.IsCancellationRequested) return;
+           if (token.IsCancellationRequested) return;

            // Check dialog handle
            if (DialogWindowHandle == nint.Zero) return;

            // Wait 150ms to check if quick switch window gets the focus
            var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
            if (timeOut) return;

            // Bring focus back to the the dialog
            Win32Helper.SetForegroundWindow(DialogWindowHandle);
        }
        catch (Exception e)
        {
            App.API.LogException(ClassName, "Failed to focus on dialog window", e);
        }
    });

1872-1912: 🛠️ Refactor suggestion

Change ResetQuickSwitch from async void to async Task

Using async void methods can lead to unhandled exceptions and make the method harder to test.

-public async void ResetQuickSwitch()
+public async Task ResetQuickSwitch()
{
    if (DialogWindowHandle == nint.Zero) return;

    DialogWindowHandle = nint.Zero;
    _isQuickSwitch = false;

    // Rest of the method remains the same
}

1918-1925: 🛠️ Refactor suggestion

Verify dialog window handle is valid in HideQuickSwitch

The method should check if the handle is still valid before attempting to use it.

public void HideQuickSwitch()
{
    if (DialogWindowHandle != nint.Zero)
    {
+        // Verify the handle is still valid
+        if (!Win32Helper.IsWindow(DialogWindowHandle))
+        {
+            DialogWindowHandle = nint.Zero;
+            _isQuickSwitch = false;
+            return;
+        }
        
        if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog)
        {
            // Rest of the method remains the same
        }
    }
}

476-477: ⚠️ Potential issue

Add error handling for path navigation in OpenResultAsync

Similar to the previous comment, this Task.Run call lacks error handling.

- _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () => 
+ {
+     try 
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to navigate to path", ex);
+     }
+ });

374-386: ⚠️ Potential issue

Add error handling for path navigation in LoadContextMenu

The Task.Run() call is a fire-and-forget operation without any error handling. If navigation fails, exceptions will be lost.

- _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () => 
+ {
+     try 
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to navigate to path", ex);
+     }
+ });
🧹 Nitpick comments (3)
Flow.Launcher/ViewModel/MainViewModel.cs (3)

55-56: Consider adding XML documentation to the new _emptyQuickSwitchResult field

Similar to the _emptyResult field above it, adding a comment would help explain its purpose and usage.

-        private readonly IReadOnlyList<QuickSwitchResult> _emptyQuickSwitchResult = new List<QuickSwitchResult>();
+        /// <summary>
+        /// Empty result list for QuickSwitch queries that return no results
+        /// </summary>
+        private readonly IReadOnlyList<QuickSwitchResult> _emptyQuickSwitchResult = new List<QuickSwitchResult>();

1478-1482: Consider extracting query method selection to a separate method

The conditional expression is complex and could be more readable as a separate method.

- IReadOnlyList<Result> results = currentIsQuickSwitch ?
-     await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) :
-         currentIsHomeQuery ?
-             await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
-             await PluginManager.QueryForPluginAsync(plugin, query, token);
+ IReadOnlyList<Result> results = await GetPluginResultsAsync(plugin, query, token, currentIsQuickSwitch, currentIsHomeQuery);

+ // Add this method elsewhere in the class
+ private async Task<IReadOnlyList<Result>> GetPluginResultsAsync(
+     PluginPair plugin, 
+     Query query, 
+     CancellationToken token, 
+     bool isQuickSwitch, 
+     bool isHomeQuery)
+ {
+     if (isQuickSwitch)
+     {
+         return await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token);
+     }
+     else if (isHomeQuery)
+     {
+         return await PluginManager.QueryHomeForPluginAsync(plugin, query, token);
+     }
+     else
+     {
+         return await PluginManager.QueryForPluginAsync(plugin, query, token);
+     }
+ }

1934-1949: Simplify ResetWindowAsync using ConfigureAwait

For improved performance when continuing the task, add ConfigureAwait(false) to the Task.Delay call.

private async Task ResetWindowAsync()
{
    lastHistoryIndex = 1;

    if (ExternalPreviewVisible)
    {
-       await CloseExternalPreviewAsync();
+       await CloseExternalPreviewAsync().ConfigureAwait(false);
    }

    if (!QueryResultsSelected())
    {
        SelectedResults = Results;
    }

-   await ChangeQueryTextAsync(string.Empty, true);
+   await ChangeQueryTextAsync(string.Empty, true).ConfigureAwait(false);
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4f777 and 50f42a8.

📒 Files selected for processing (1)
  • Flow.Launcher/ViewModel/MainViewModel.cs (17 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Report (PR)
  • GitHub Check: build
🔇 Additional comments (1)
Flow.Launcher/ViewModel/MainViewModel.cs (1)

1317-1321: Good decision to skip home page for quick switch window

This is a good UX decision - the home page would be confusing in a quick switch context where users are looking to navigate somewhere specific.

Comment on lines +1809 to +1813
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow).UpdatePosition();
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add null check for mainWindow

After getting the mainWindow from Application.Current?.MainWindow, you should check if it's not null before using it.

Application.Current?.Dispatcher.Invoke(() =>
{
-    (Application.Current?.MainWindow as MainWindow).UpdatePosition();
+    var mainWindow = Application.Current?.MainWindow as MainWindow;
+    mainWindow?.UpdatePosition();
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Application.Current?.Dispatcher.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow).UpdatePosition();
});
Application.Current?.Dispatcher.Invoke(() =>
{
var mainWindow = Application.Current?.MainWindow as MainWindow;
mainWindow?.UpdatePosition();
});

@Jack251970 Jack251970 changed the title Quick Switch & Files Support Quick Switch May 11, 2025

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (9)
Flow.Launcher/ViewModel/MainViewModel.cs (9)

467-493: ⚠️ Potential issue

Apply the same error handling pattern to this navigation task.

Similar to the previous comment, this navigation task needs proper error handling to prevent silent failures.

- _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () => 
+ {
+     try 
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath).ConfigureAwait(false);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to jump to path", ex);
+     }
+ });

503-526: ⚠️ Potential issue

Add type checking to prevent InvalidCastException.

The direct cast to QuickSwitchResult assumes all results are of that type, which could throw an exception if misused.

if (isQuickSwitch)
{
    foreach (var result in results.ToList())
    {
        if (token.IsCancellationRequested) break;

-       var resultCopy = ((QuickSwitchResult)result).Clone();
-       resultsCopy.Add(resultCopy);
+       if (result is QuickSwitchResult quickSwitchResult)
+       {
+           var resultCopy = quickSwitchResult.Clone();
+           resultsCopy.Add(resultCopy);
+       }
+       else
+       {
+           App.API.LogWarning(ClassName, $"Expected QuickSwitchResult but got {result.GetType().Name}");
+           var resultCopy = result.Clone();
+           resultsCopy.Add(resultCopy);
+       }
    }
}

1764-1770: ⚠️ Potential issue

Add thread-safe access to QuickSwitch properties.

These properties are accessed from multiple threads (UI thread, Task.Run threads) without proper synchronization, which could lead to race conditions and unexpected behavior.

+ private readonly object _quickSwitchLock = new();
- public nint DialogWindowHandle { get; private set; } = nint.Zero;
+ private nint _dialogWindowHandle = nint.Zero;
+ public nint DialogWindowHandle 
+ { 
+     get 
+     {
+         lock (_quickSwitchLock)
+         {
+             return _dialogWindowHandle;
+         }
+     }
+     private set 
+     {
+         lock (_quickSwitchLock)
+         {
+             _dialogWindowHandle = value;
+         }
+     }
+ }

- private bool _isQuickSwitch = false;
+ private bool _isQuickSwitch = false;
+ private bool IsQuickSwitch 
+ { 
+     get 
+     {
+         lock (_quickSwitchLock)
+         {
+             return _isQuickSwitch;
+         }
+     }
+     set 
+     {
+         lock (_quickSwitchLock)
+         {
+             _isQuickSwitch = value;
+         }
+     }
+ }

1782-1798: ⚠️ Potential issue

Change the async void method to return Task.

Async void methods can lead to unhandled exceptions that crash the application. Return a Task to allow callers to handle exceptions.

- public async Task SetupQuickSwitchAsync(nint handle)
+ public async Task SetupQuickSwitchAsync(nint handle)

Also, consider adding more comprehensive logging to track the quick switch initialization process:

 if (handle == nint.Zero) return;

+ App.API.LogDebug(ClassName, $"Setting up quick switch for handle: {handle}");
 // Only set flag & reset window once for one file dialog
 var dialogWindowHandleChanged = false;
 if (DialogWindowHandle != handle)
 {
     DialogWindowHandle = handle;
     _previousMainWindowVisibilityStatus = MainWindowVisibilityStatus;
     _isQuickSwitch = true;

     dialogWindowHandleChanged = true;
+    App.API.LogDebug(ClassName, "Dialog window handle changed, resetting quick switch window");

     // If don't give a time, Positioning will be weird
     await Task.Delay(300);
 }

1809-1813: ⚠️ Potential issue

Add null check before using mainWindow.

After getting the mainWindow from Application.Current?.MainWindow, you should check if it's not null before using it.

Application.Current?.Dispatcher.Invoke(() =>
{
-   (Application.Current?.MainWindow as MainWindow).UpdatePosition();
+   var mainWindow = Application.Current?.MainWindow as MainWindow;
+   mainWindow?.UpdatePosition();
});

1837-1867: ⚠️ Potential issue

Capture cancellation token locally to avoid race conditions.

The lambda captures the token source directly, which could lead to race conditions if another dialog arrives and replaces _quickSwitchSource.

// Create a new cancellation token source
_quickSwitchSource = new CancellationTokenSource();
+ var token = _quickSwitchSource.Token;

_ = Task.Run(() =>
    {
        try
        {
            // Check task cancellation
-           if (_quickSwitchSource.Token.IsCancellationRequested) return;
+           if (token.IsCancellationRequested) return;

            // Check dialog handle
            if (DialogWindowHandle == nint.Zero) return;

            // Wait 150ms to check if quick switch window gets the focus
            var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
            if (timeOut) return;

            // Bring focus back to the the dialog
            Win32Helper.SetForegroundWindow(DialogWindowHandle);
        }
        catch (Exception e)
        {
            App.API.LogException(ClassName, "Failed to focus on dialog window", e);
        }
    });

1872-1912: ⚠️ Potential issue

Change async void method to return Task for proper exception handling.

Async void methods can lead to unhandled exceptions that crash the application. Return a Task to allow proper exception handling.

- public async void ResetQuickSwitch()
+ public async Task ResetQuickSwitch()
{
    if (DialogWindowHandle == nint.Zero) return;

+   App.API.LogDebug(ClassName, "Resetting quick switch");
    DialogWindowHandle = nint.Zero;
    _isQuickSwitch = false;

    if (_previousMainWindowVisibilityStatus != MainWindowVisibilityStatus)
    {
        // Show or hide to change visibility
        if (_previousMainWindowVisibilityStatus)
        {
            Show();

            _ = ResetWindowAsync();
        }
        else
        {
            await ResetWindowAsync();

            Hide(false);
        }
    }
    else
    {
        if (_previousMainWindowVisibilityStatus)
        {
            // Only update the position
            Application.Current?.Dispatcher.Invoke(() =>
            {
-               (Application.Current?.MainWindow as MainWindow).UpdatePosition();
+               var mainWindow = Application.Current?.MainWindow as MainWindow;
+               mainWindow?.UpdatePosition();
            });

            _ = ResetWindowAsync();
        }
        else
        {
            _ = ResetWindowAsync();
        }
    }
}

1916-1931: ⚠️ Potential issue

Add window handle validity check in HideQuickSwitch.

The method assumes that if DialogWindowHandle is not zero, it's still valid, but Windows handles can be reused or invalidated.

public void HideQuickSwitch()
{
    if (DialogWindowHandle != nint.Zero)
    {
+       // Verify the handle is still valid
+       if (!Win32Helper.IsWindow(DialogWindowHandle))
+       {
+           DialogWindowHandle = nint.Zero;
+           _isQuickSwitch = false;
+           return;
+       }
        
        if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog)
        {
            // Warning: Main window is already in foreground
            // This is because if you click popup menus in other applications to hide quick switch window,
            // they can steal focus before showing main window
            if (MainWindowVisibilityStatus)
            {
                Hide();
            }
        }
    }
}

372-386: ⚠️ Potential issue

Add error handling to the path navigation task.

The navigation task is fire-and-forget, meaning exceptions won't be caught, which could lead to silent failures when jumping to paths.

- _ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () => 
+ {
+     try 
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath).ConfigureAwait(false);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to jump to path", ex);
+     }
+ });
🧹 Nitpick comments (3)
Flow.Launcher/ViewModel/MainViewModel.cs (3)

55-56: Add a method for creating an empty quick switch result list.

Consider adding a method to create this empty list to maintain symmetry with _emptyResult usage. This would make the code more maintainable if the creation logic changes in the future.

- private readonly IReadOnlyList<QuickSwitchResult> _emptyQuickSwitchResult = new List<QuickSwitchResult>();
+ private readonly IReadOnlyList<QuickSwitchResult> _emptyQuickSwitchResult => EmptyQuickSwitchResultList();
+ 
+ private static IReadOnlyList<QuickSwitchResult> EmptyQuickSwitchResultList()
+ {
+     return new List<QuickSwitchResult>();
+ }

1474-1478: Consider using a more explicit conditional structure for readability.

The nested ternary operation is hard to read. A more explicit if-else structure would make the logic clearer.

- IReadOnlyList<Result> results = currentIsQuickSwitch ?
-     await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) :
-         currentIsHomeQuery ?
-             await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
-             await PluginManager.QueryForPluginAsync(plugin, query, token);
+ IReadOnlyList<Result> results;
+ if (currentIsQuickSwitch)
+ {
+     results = await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token);
+ }
+ else if (currentIsHomeQuery)
+ {
+     results = await PluginManager.QueryHomeForPluginAsync(plugin, query, token);
+ }
+ else
+ {
+     results = await PluginManager.QueryForPluginAsync(plugin, query, token);
+ }

1934-1949: Consider adding cancellation handling to ResetWindowAsync.

The ResetWindowAsync method could be interrupted by application termination or other events. Adding cancellation support would make it more robust.

- private async Task ResetWindowAsync()
+ private async Task ResetWindowAsync(CancellationToken cancellationToken = default)
{
+   try
+   {
        lastHistoryIndex = 1;

        if (ExternalPreviewVisible)
        {
-           await CloseExternalPreviewAsync();
+           await CloseExternalPreviewAsync().ConfigureAwait(false);
        }

        if (!QueryResultsSelected())
        {
            SelectedResults = Results;
        }

-       await ChangeQueryTextAsync(string.Empty, true);
+       await ChangeQueryTextAsync(string.Empty, true).ConfigureAwait(false);
+   }
+   catch (Exception ex) when (!(ex is OperationCanceledException))
+   {
+       App.API.LogException(ClassName, "Error resetting window", ex);
+   }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d44f6e9 and 76d52a9.

📒 Files selected for processing (2)
  • Flow.Launcher/MainWindow.xaml.cs (11 hunks)
  • Flow.Launcher/ViewModel/MainViewModel.cs (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Flow.Launcher/MainWindow.xaml.cs
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (1)
Flow.Launcher/ViewModel/MainViewModel.cs (1)

2170-2170: Ensure cancellation token source disposal.

This change correctly disposes the QuickSwitch cancellation token source.

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (9)
Flow.Launcher/ViewModel/MainViewModel.cs (9)

371-384: ⚠️ Potential issue

Add error handling for fire-and-forget JumpToPath task

The JumpToPathAsync task is launched without any error handling, which could lead to unhandled exceptions.

Win32Helper.SetForegroundWindow(DialogWindowHandle);
-_ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+_ = Task.Run(async () => {
+    try 
+    {
+        await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)
+                         .ConfigureAwait(false);
+    }
+    catch (Exception ex)
+    {
+        App.API.LogException(ClassName, "Quick switch navigation failed", ex);
+    }
+});

464-476: ⚠️ Potential issue

Add error handling for JumpToPath in OpenResultAsync method

Similar to the previous comment, the JumpToPathAsync task is launched without error handling.

Win32Helper.SetForegroundWindow(DialogWindowHandle);
-_ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+_ = Task.Run(async () => {
+    try 
+    {
+        await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath)
+                         .ConfigureAwait(false);
+    }
+    catch (Exception ex)
+    {
+        App.API.LogException(ClassName, "Quick switch navigation failed", ex);
+    }
+});

501-527: ⚠️ Potential issue

Add type checking to prevent InvalidCastException in DeepCloneResults

Direct casting to QuickSwitchResult could throw an exception if the result is not of that type.

if (isQuickSwitch)
{
    foreach (var result in results.ToList())
    {
        if (token.IsCancellationRequested) break;

-       var resultCopy = ((QuickSwitchResult)result).Clone();
-       resultsCopy.Add(resultCopy);
+       if (result is QuickSwitchResult quickSwitchResult)
+       {
+           var resultCopy = quickSwitchResult.Clone();
+           resultsCopy.Add(resultCopy);
+       }
+       else
+       {
+           App.API.LogWarning(ClassName, $"Expected QuickSwitchResult but got {result.GetType().Name}");
+           var resultCopy = result.Clone();
+           resultsCopy.Add(resultCopy);
+       }
    }
}

1766-1774: ⚠️ Potential issue

Make shared state variables thread-safe

The fields DialogWindowHandle and _isQuickSwitch are accessed from multiple threads without synchronization, which could lead to race conditions.

+private readonly object _quickSwitchLock = new();
+private nint _dialogWindowHandleValue = nint.Zero;
+private bool _isQuickSwitchValue = false;

-public nint DialogWindowHandle { get; private set; } = nint.Zero;
+public nint DialogWindowHandle 
+{ 
+    get 
+    {
+        lock (_quickSwitchLock)
+        {
+            return _dialogWindowHandleValue;
+        }
+    }
+    private set 
+    {
+        lock (_quickSwitchLock)
+        {
+            _dialogWindowHandleValue = value;
+        }
+    }
+}

-private bool _isQuickSwitch = false;
+private bool _isQuickSwitch 
+{ 
+    get 
+    {
+        lock (_quickSwitchLock)
+        {
+            return _isQuickSwitchValue;
+        }
+    }
+    set 
+    {
+        lock (_quickSwitchLock)
+        {
+            _isQuickSwitchValue = value;
+        }
+    }
+}

1841-1872: ⚠️ Potential issue

Fix potential race condition with cancellation token

There's a potential race condition with the cancellation token if a new task is started before the previous one is cancelled.

// Cancel the previous quick switch task
_quickSwitchSource?.Cancel();

// Create a new cancellation token source
_quickSwitchSource = new CancellationTokenSource();
+var token = _quickSwitchSource.Token;

_ = Task.Run(() =>
    {
        try
        {
            // Check task cancellation
-           if (_quickSwitchSource.Token.IsCancellationRequested) return;
+           if (token.IsCancellationRequested) return;

            // Check dialog handle
            if (DialogWindowHandle == nint.Zero) return;

            // Wait 150ms to check if quick switch window gets the focus
            var timeOut = !SpinWait.SpinUntil(() => !Win32Helper.IsForegroundWindow(DialogWindowHandle), 150);
            if (timeOut) return;

            // Bring focus back to the the dialog
            Win32Helper.SetForegroundWindow(DialogWindowHandle);
        }
        catch (Exception e)
        {
            App.API.LogException(ClassName, "Failed to focus on dialog window", e);
        }
    });

1813-1817: ⚠️ Potential issue

Add null check for MainWindow before calling UpdatePosition

There's no null check before accessing MainWindow, which could lead to a NullReferenceException.

Application.Current?.Dispatcher.Invoke(() =>
{
-    (Application.Current?.MainWindow as MainWindow).UpdatePosition();
+    var mainWindow = Application.Current?.MainWindow as MainWindow;
+    mainWindow?.UpdatePosition();
});

1876-1877: 🛠️ Refactor suggestion

Use Task return type instead of async void

Using async void is not recommended for non-event handlers as it can lead to unhandled exceptions.

-public async void ResetQuickSwitch()
+public async Task ResetQuickSwitch()

1904-1908: ⚠️ Potential issue

Add null check for MainWindow in ResetQuickSwitch

Similar to the previous issue, there's no null check before accessing MainWindow.

Application.Current?.Dispatcher.Invoke(() =>
{
-    (Application.Current?.MainWindow as MainWindow).UpdatePosition();
+    var mainWindow = Application.Current?.MainWindow as MainWindow;
+    mainWindow?.UpdatePosition();
});

1920-1935: 🛠️ Refactor suggestion

Add checks for window handle validity in HideQuickSwitch

The method only checks if the handle is non-zero, but not if it's still valid. Windows handles can be reused or invalidated.

public void HideQuickSwitch()
{
    if (DialogWindowHandle != nint.Zero)
    {
+        // Verify the handle is still valid
+        if (!Win32Helper.IsWindow(DialogWindowHandle))
+        {
+            DialogWindowHandle = nint.Zero;
+            _isQuickSwitch = false;
+            return;
+        }
+        
        if (QuickSwitch.QuickSwitchWindowPosition == QuickSwitchWindowPositions.UnderDialog)
        {
            // Warning: Main window is already in foreground
            // This is because if you click popup menus in other applications to hide quick switch window,
            // they can steal focus before showing main window
            if (MainWindowVisibilityStatus)
            {
                Hide();
            }
        }
    }
}
🧹 Nitpick comments (2)
Flow.Launcher/ViewModel/MainViewModel.cs (2)

55-55: Unused field _emptyQuickSwitchResult

This field is defined but never used in the codebase. Consider removing it to avoid dead code.

-private readonly IReadOnlyList<QuickSwitchResult> _emptyQuickSwitchResult = new List<QuickSwitchResult>();

1472-1476: Consider improving readability of nested ternary operators

The nested ternary operators make this code harder to read and maintain.

-IReadOnlyList<Result> results = currentIsQuickSwitch ?
-    await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token) :
-        currentIsHomeQuery ?
-            await PluginManager.QueryHomeForPluginAsync(plugin, query, token) :
-            await PluginManager.QueryForPluginAsync(plugin, query, token);
+IReadOnlyList<Result> results;
+if (currentIsQuickSwitch)
+{
+    results = await PluginManager.QueryQuickSwitchForPluginAsync(plugin, query, token);
+}
+else if (currentIsHomeQuery)
+{
+    results = await PluginManager.QueryHomeForPluginAsync(plugin, query, token);
+}
+else
+{
+    results = await PluginManager.QueryForPluginAsync(plugin, query, token);
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45ac0af and c20bb30.

📒 Files selected for processing (4)
  • Flow.Launcher.Core/Plugin/PluginManager.cs (2 hunks)
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs (4 hunks)
  • Flow.Launcher/MainWindow.xaml.cs (11 hunks)
  • Flow.Launcher/ViewModel/MainViewModel.cs (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Flow.Launcher.Core/Plugin/PluginManager.cs
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs
  • Flow.Launcher/MainWindow.xaml.cs
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build

Copy link

@check-spelling-bot Report

🔴 Please review

See the 📂 files view, the 📜action log, or 📝 job summary for details.

❌ Errors and Warnings Count
❌ check-file-path 1
❌ forbidden-pattern 25
⚠️ non-alpha-in-dictionary 13

See ❌ Event descriptions for more information.

Forbidden patterns 🙅 (2)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

s.b. workaround(s)

\bwork[- ]arounds?\b

Reject duplicate words

\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\s\g{-1}\s
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
Flow.Launcher/ViewModel/MainViewModel.cs (4)

464-477: 🛠️ Refactor suggestion

Add error handling for the QuickSwitch path navigation.

Similar to the issue in LoadContextMenu, this Task.Run for navigation is also fire-and-forget with no error handling.

_ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () =>
+ {
+     try
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to navigate to path in OpenResultAsync", ex);
+     }
+ });

511-512: ⚠️ Potential issue

Add type checking before casting to QuickSwitchResult.

The direct cast to QuickSwitchResult assumes all items in the collection are of that type. This could throw InvalidCastException if misused.

-var resultCopy = ((QuickSwitchResult)result).Clone();
-resultsCopy.Add(resultCopy);
+if (result is QuickSwitchResult quickSwitchResult)
+{
+    var resultCopy = quickSwitchResult.Clone();
+    resultsCopy.Add(resultCopy);
+}
+else
+{
+    App.API.LogWarning(ClassName, $"Expected QuickSwitchResult in DeepCloneResults but got {result.GetType().Name}");
+    // Add regular clone as fallback
+    var resultCopy = result.Clone();
+    resultsCopy.Add(resultCopy);
+}

1908-1948: ⚠️ Potential issue

Change async void to return Task for proper exception handling.

Using async void for the ResetQuickSwitch method prevents proper exception handling and can lead to application crashes if exceptions occur.

-public async void ResetQuickSwitch()
+public async Task ResetQuickSwitch()
{
    if (DialogWindowHandle == nint.Zero) return;

    DialogWindowHandle = nint.Zero;
    _isQuickSwitch = false;

    // Rest of the method...
}

371-384: 🛠️ Refactor suggestion

Add error handling for the QuickSwitch path navigation.

The Task.Run for navigation is fire-and-forget with no error handling, which could lead to unhandled exceptions if the navigation fails.

_ = Task.Run(() => QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath));
+ _ = Task.Run(async () =>
+ {
+     try
+     {
+         await QuickSwitch.JumpToPathAsync(DialogWindowHandle, quickSwitchResult.QuickSwitchPath);
+     }
+     catch (Exception ex)
+     {
+         App.API.LogException(ClassName, "Failed to navigate to path in LoadContextMenu", ex);
+     }
+ });
🧹 Nitpick comments (1)
Flow.Launcher/ViewModel/MainViewModel.cs (1)

1818-1845: Consider simplifying window positioning delay logic.

The 300ms delay before updating the position is used to ensure the window is positioned correctly, but this is a brittle approach that might not work consistently across different systems.

Consider using a more robust window positioning strategy:

  1. Use window message hooks to detect when the dialog has finished positioning
  2. Use a callback approach instead of a fixed delay
  3. Implement a progressive retry mechanism with shorter delays

Alternatively, document the reason for this specific delay value with a comment if it's based on testing across different systems.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c20bb30 and 06baec5.

📒 Files selected for processing (3)
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs (4 hunks)
  • Flow.Launcher/MainWindow.xaml.cs (11 hunks)
  • Flow.Launcher/ViewModel/MainViewModel.cs (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Flow.Launcher/MainWindow.xaml.cs
  • Flow.Launcher.Infrastructure/UserSettings/Settings.cs
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (4)
Flow.Launcher/ViewModel/MainViewModel.cs (4)

55-55: Good addition to support QuickSwitchResult objects.

Adding a dedicated empty list for QuickSwitchResult objects is consistent with the existing pattern for regular Result objects.


1309-1315: Good addition to skip home page for quick switch window.

This is a sensible special case handling - showing the home page during quick switch would likely confuse users and interfere with the focused path switching experience.


1477-1482: Great implementation of plugin querying logic for different modes.

The conditional expression effectively handles three distinct query modes (quick switch, home, and standard queries) with clear branching logic that routes to the appropriate plugin manager method.


2209-2209: Proper disposal of QuickSwitch resources.

Good practice to dispose of the cancellation token source when the object is disposed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
20 min review bug Something isn't working enhancement New feature or request kind/ux related to user experience listary from listary
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Quick Switch like Listary output search result to file selection dialog?
7 participants