From da4d0f2e757a814994c6ad273d96f428631ea54a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 20:21:44 -0500 Subject: [PATCH 01/30] add vim mode to the search bar --- .../UserSettings/Settings.cs | 14 + Flow.Launcher.Test/VimEngineTest.cs | 172 ++++ Flow.Launcher.Test/VimMotionEngineTest.cs | 94 ++ Flow.Launcher/MainWindow.xaml | 24 + Flow.Launcher/MainWindow.xaml.cs | 10 + .../Views/SettingsPaneGeneral.xaml | 10 + Flow.Launcher/VimMode/VimEngine.cs | 51 ++ Flow.Launcher/VimMode/VimManager.cs | 859 ++++++++++++++++++ Flow.Launcher/VimMode/VimMotionEngine.cs | 109 +++ Flow.Launcher/packages.lock.json | 4 +- README.md | 537 ++--------- 11 files changed, 1445 insertions(+), 439 deletions(-) create mode 100644 Flow.Launcher.Test/VimEngineTest.cs create mode 100644 Flow.Launcher.Test/VimMotionEngineTest.cs create mode 100644 Flow.Launcher/VimMode/VimEngine.cs create mode 100644 Flow.Launcher/VimMode/VimManager.cs create mode 100644 Flow.Launcher/VimMode/VimMotionEngine.cs diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index fd535f21a6e..3df742125ad 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -59,6 +59,20 @@ public void Save() _storage.Save(); } + private bool _enableVimMode = false; + public bool EnableVimMode + { + get => _enableVimMode; + set + { + if (_enableVimMode != value) + { + _enableVimMode = value; + OnPropertyChanged(); + } + } + } + public string Hotkey { get; set; } = $"{KeyConstant.Alt} + {KeyConstant.Space}"; private string _openResultModifiers = KeyConstant.Alt; diff --git a/Flow.Launcher.Test/VimEngineTest.cs b/Flow.Launcher.Test/VimEngineTest.cs new file mode 100644 index 00000000000..de2712f5817 --- /dev/null +++ b/Flow.Launcher.Test/VimEngineTest.cs @@ -0,0 +1,172 @@ +using NUnit.Framework; +using System.Collections.Generic; +using Flow.Launcher.VimMode; + +namespace Flow.Launcher.Test +{ + [TestFixture] + public class VimEngineTest + { + [Test] + public void DefaultModeIsInsert() + { + var engine = new VimEngine(); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + } + + [Test] + public void SwitchToInsertFromNormalFiresEvent() + { + var engine = new VimEngine(); + engine.SwitchToNormal(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToInsert(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(modes, Is.EqualTo(new[] { VimModes.Insert })); + } + + [Test] + public void SwitchToInsertWhenAlreadyInsertDoesNotFire() + { + var engine = new VimEngine(); + var fired = false; + engine.ModeChanged += _ => fired = true; + + engine.SwitchToInsert(); + + Assert.That(fired, Is.False); + } + + [Test] + public void SwitchToNormalFiresEvent() + { + var engine = new VimEngine(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToNormal(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Normal)); + Assert.That(modes, Is.EqualTo(new[] { VimModes.Normal })); + } + + [Test] + public void SwitchToNormalWhenAlreadyNormalDoesNotFire() + { + var engine = new VimEngine(); + engine.SwitchToNormal(); + var fired = false; + engine.ModeChanged += _ => fired = true; + + engine.SwitchToNormal(); + + Assert.That(fired, Is.False); + } + + [Test] + public void SwitchToVisualFiresEvent() + { + var engine = new VimEngine(); + engine.SwitchToNormal(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToVisual(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Visual)); + Assert.That(modes, Is.EqualTo(new[] { VimModes.Visual })); + } + + [Test] + public void SwitchToVisualWhenAlreadyVisualDoesNotFire() + { + var engine = new VimEngine(); + engine.SwitchToVisual(); + var fired = false; + engine.ModeChanged += _ => fired = true; + + engine.SwitchToVisual(); + + Assert.That(fired, Is.False); + } + + [Test] + public void SwitchToVisualLineFiresEvent() + { + var engine = new VimEngine(); + engine.SwitchToVisual(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToVisualLine(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.VisualLine)); + Assert.That(modes, Is.EqualTo(new[] { VimModes.VisualLine })); + } + + [Test] + public void SwitchToVisualLineWhenAlreadyDoesNotFire() + { + var engine = new VimEngine(); + engine.SwitchToVisualLine(); + var fired = false; + engine.ModeChanged += _ => fired = true; + + engine.SwitchToVisualLine(); + + Assert.That(fired, Is.False); + } + + [Test] + public void FullModeCycleTest() + { + var engine = new VimEngine(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToNormal(); + engine.SwitchToVisual(); + engine.SwitchToNormal(); + engine.SwitchToInsert(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(modes, Is.EqualTo(new[] { + VimModes.Normal, VimModes.Visual, VimModes.Normal, VimModes.Insert + })); + } + + [Test] + public void VisualLineCycleTest() + { + var engine = new VimEngine(); + var modes = new List(); + engine.ModeChanged += m => modes.Add(m); + + engine.SwitchToNormal(); + engine.SwitchToVisualLine(); + engine.SwitchToVisual(); + engine.SwitchToVisualLine(); + engine.SwitchToNormal(); + engine.SwitchToInsert(); + + Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(modes, Is.EqualTo(new[] { + VimModes.Normal, VimModes.VisualLine, VimModes.Visual, + VimModes.VisualLine, VimModes.Normal, VimModes.Insert + })); + } + + [Test] + public void NoSubscriberDoesNotThrow() + { + var engine = new VimEngine(); + Assert.DoesNotThrow(() => engine.SwitchToNormal()); + Assert.DoesNotThrow(() => engine.SwitchToVisual()); + Assert.DoesNotThrow(() => engine.SwitchToVisualLine()); + Assert.DoesNotThrow(() => engine.SwitchToInsert()); + } + } +} diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs new file mode 100644 index 00000000000..18a72c044e8 --- /dev/null +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using Flow.Launcher.VimMode; + +namespace Flow.Launcher.Test +{ + [TestFixture] + public class VimMotionEngineTest + { + private const string Sample = "hello world_foo bar"; + + [Test] + public void MoveLeftTest() + { + Assert.That(VimMotionEngine.MoveLeft(5, Sample.Length), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MoveLeft(0, Sample.Length), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveLeft(2, Sample.Length), Is.EqualTo(1)); + } + + [Test] + public void MoveRightTest() + { + Assert.That(VimMotionEngine.MoveRight(5, Sample.Length), Is.EqualTo(6)); + Assert.That(VimMotionEngine.MoveRight(Sample.Length, Sample.Length), Is.EqualTo(Sample.Length)); + Assert.That(VimMotionEngine.MoveRight(Sample.Length - 1, Sample.Length), Is.EqualTo(Sample.Length)); + } + + [Test] + public void MoveStartOfLineTest() + { + Assert.That(VimMotionEngine.MoveStartOfLine(), Is.EqualTo(0)); + } + + [Test] + public void MoveEndOfLineTest() + { + Assert.That(VimMotionEngine.MoveEndOfLine(Sample.Length), Is.EqualTo(Sample.Length)); + Assert.That(VimMotionEngine.MoveEndOfLine(0), Is.EqualTo(0)); + } + + [Test] + public void MoveFirstNonBlankTest() + { + Assert.That(VimMotionEngine.MoveFirstNonBlank(" hello"), Is.EqualTo(3)); + Assert.That(VimMotionEngine.MoveFirstNonBlank("hello"), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveFirstNonBlank(" "), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveFirstNonBlank(""), Is.EqualTo(0)); + } + + [Test] + public void MoveNextWordTest() + { + Assert.That(VimMotionEngine.MoveNextWord(Sample, 0), Is.EqualTo(6)); + Assert.That(VimMotionEngine.MoveNextWord(Sample, 6), Is.EqualTo(16)); + Assert.That(VimMotionEngine.MoveNextWord(Sample, Sample.Length), Is.EqualTo(Sample.Length)); + Assert.That(VimMotionEngine.MoveNextWord(" abc", 0), Is.EqualTo(3)); + } + + [Test] + public void MovePrevWordTest() + { + Assert.That(VimMotionEngine.MovePrevWord(Sample, 6), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MovePrevWord("hello world", 8), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MovePrevWord(Sample, 0), Is.EqualTo(0)); + } + + [Test] + public void MoveEndWordTest() + { + Assert.That(VimMotionEngine.MoveEndWord(Sample, 0), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MoveEndWord(Sample, 5), Is.EqualTo(14)); + Assert.That(VimMotionEngine.MoveEndWord(Sample, Sample.Length - 1), Is.EqualTo(Sample.Length)); + } + + [Test] + public void FindCharForwardTest() + { + Assert.That(VimMotionEngine.FindCharForward(Sample, 0, 'o', false), Is.EqualTo(4)); + Assert.That(VimMotionEngine.FindCharForward(Sample, 0, 'o', till: true), Is.EqualTo(3)); + Assert.That(VimMotionEngine.FindCharForward(Sample, 0, 'z', false), Is.EqualTo(0)); + Assert.That(VimMotionEngine.FindCharForward(Sample, Sample.Length - 1, 'o', false), Is.EqualTo(Sample.Length - 1)); + Assert.That(VimMotionEngine.FindCharForward(Sample, 4, 'o', false), Is.EqualTo(7)); + } + + [Test] + public void FindCharBackwardTest() + { + Assert.That(VimMotionEngine.FindCharBackward(Sample, 7, 'o', false), Is.EqualTo(4)); + Assert.That(VimMotionEngine.FindCharBackward(Sample, 7, 'o', till: true), Is.EqualTo(5)); + Assert.That(VimMotionEngine.FindCharBackward(Sample, Sample.Length - 1, 'z', false), Is.EqualTo(Sample.Length - 1)); + Assert.That(VimMotionEngine.FindCharBackward(Sample, 0, 'o', false), Is.EqualTo(0)); + Assert.That(VimMotionEngine.FindCharBackward(Sample, 4, 'o', false), Is.EqualTo(4)); + } + } +} diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 2950d2f6555..604caa7cbb1 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -350,6 +350,30 @@ X2="0" Y1="0" Y2="0" /> + + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 4866c0deae6..eefd293c0bf 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -58,6 +58,7 @@ public partial class MainWindow : IDisposable // Window Context Menu private readonly ContextMenu _contextMenu = new(); private readonly MainViewModel _viewModel; + private readonly Flow.Launcher.VimMode.VimManager _vimManager; // Window Event: Key Event private bool _isArrowKeyPressed = false; @@ -96,6 +97,8 @@ public MainWindow() InitializeComponent(); UpdatePosition(); + _vimManager = new Flow.Launcher.VimMode.VimManager(this, _viewModel, QueryTextBox, VimBlockCaret, VimModeText, _settings); + SyncSoundEffectsState(); RegisterSoundEffectsEvent(); DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); @@ -439,6 +442,12 @@ private async void OnDeactivated(object sender, EventArgs e) private void OnKeyDown(object sender, KeyEventArgs e) { var specialKeyState = GlobalHotkey.CheckModifiers(); + + if (_vimManager != null && _vimManager.HandlePreviewKeyDown(e)) + { + return; + } + switch (e.Key) { case Key.Down: @@ -1546,6 +1555,7 @@ protected virtual void Dispose(bool disposing) UnregisterSoundEffectsEvent(); DisposeSoundEffects(); _viewModel.ActualApplicationThemeChanged -= ViewModel_ActualApplicationThemeChanged; + _vimManager?.Dispose(); } _disposed = true; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 5779071534f..6c26d471b67 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -82,6 +82,16 @@ OnContent="{DynamicResource enable}" /> + + + + ModeChanged; + + public void SwitchToInsert() + { + var oldMode = CurrentMode; + CurrentMode = VimModes.Insert; + if (oldMode != VimModes.Insert) + ModeChanged?.Invoke(CurrentMode); + } + + public void SwitchToNormal() + { + var oldMode = CurrentMode; + CurrentMode = VimModes.Normal; + if (oldMode != VimModes.Normal) + ModeChanged?.Invoke(CurrentMode); + } + + public void SwitchToVisual() + { + var oldMode = CurrentMode; + CurrentMode = VimModes.Visual; + if (oldMode != VimModes.Visual) + ModeChanged?.Invoke(CurrentMode); + } + + public void SwitchToVisualLine() + { + var oldMode = CurrentMode; + CurrentMode = VimModes.VisualLine; + if (oldMode != VimModes.VisualLine) + ModeChanged?.Invoke(CurrentMode); + } + } +} diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs new file mode 100644 index 00000000000..77f651d65c9 --- /dev/null +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -0,0 +1,859 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Flow.Launcher.ViewModel; + +namespace Flow.Launcher.VimMode +{ + public class VimManager : IDisposable + { + private bool _disposed; + private readonly MainWindow _mainWindow; + private readonly MainViewModel _viewModel; + private readonly TextBox _queryTextBox; + private readonly VimEngine _vimEngine; + private readonly System.Windows.Shapes.Rectangle _vimBlockCaret; + private readonly TextBlock _vimModeText; + private string _pendingCommand = ""; + private string _awaitingCharCommand = ""; + private string _lastFindCmd = ""; + private char _lastFindChar = '\0'; + private DateTime _lastEscapeTime = DateTime.MinValue; + private int _visualAnchor; + private int _visualCaret; + private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; + + public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryTextBox, System.Windows.Shapes.Rectangle vimBlockCaret, TextBlock vimModeText, Flow.Launcher.Infrastructure.UserSettings.Settings settings) + { + _mainWindow = mainWindow; + _viewModel = viewModel; + _queryTextBox = queryTextBox; + _vimBlockCaret = vimBlockCaret; + _vimModeText = vimModeText; + _settings = settings; + + _vimEngine = new VimEngine(); + _vimEngine.ModeChanged += VimEngine_ModeChanged; + _mainWindow.Loaded += MainWindow_Loaded; + _viewModel.PropertyChanged += ViewModel_PropertyChanged; + } + + private void MainWindow_Loaded(object sender, RoutedEventArgs e) + { + _queryTextBox.PreviewTextInput += QueryTextBox_PreviewTextInput; + _queryTextBox.SelectionChanged += QueryTextBox_SelectionChanged; + _queryTextBox.TextChanged += QueryTextBox_TextChanged; + + // Initial state + UpdateIndicatorAsync(_vimEngine.CurrentMode); + } + + private void QueryTextBox_SelectionChanged(object sender, RoutedEventArgs e) + { + UpdateCaretPosition(); + } + + private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + UpdateCaretPosition(); + } + + private void UpdateCaretPosition() + { + if (_vimEngine.CurrentMode != VimModes.Insert && _vimBlockCaret != null) + { + try + { + int index = (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + ? _visualCaret + : _queryTextBox.CaretIndex; + var rect = _queryTextBox.GetRectFromCharacterIndex(index); + if (!rect.IsEmpty) + { + var m = _queryTextBox.Margin; + _vimBlockCaret.Margin = new Thickness(rect.Left + m.Left, rect.Top + m.Top, 0, 0); + _vimBlockCaret.Width = Math.Max(rect.Width, 8); + _vimBlockCaret.Height = rect.Height; + } + } + catch (Exception) + { + // Ignore layout exceptions + } + } + } + + private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MainViewModel.MainWindowVisibilityStatus)) + { + if (_viewModel.MainWindowVisibilityStatus) + { + _mainWindow.Dispatcher.BeginInvoke(new Action(() => + { + if (_vimEngine.CurrentMode != VimModes.Insert) + { + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToInsert(); + } + })); + } + } + } + + private void VimEngine_ModeChanged(VimModes mode) + { + UpdateIndicatorAsync(mode); + } + + private void UpdateIndicatorAsync(VimModes mode) + { + if (_mainWindow.Dispatcher.CheckAccess()) + { + ApplyModeUI(mode); + } + else + { + _mainWindow.Dispatcher.BeginInvoke(new Action(() => ApplyModeUI(mode))); + } + } + + private void ApplyModeUI(VimModes mode) + { + InputMethod.SetIsInputMethodSuspended(_queryTextBox, mode != VimModes.Insert); + + string label = mode switch + { + VimModes.Normal => "-- NORMAL --", + VimModes.Visual => "-- VISUAL --", + VimModes.VisualLine => "-- VISUAL LINE --", + _ => null + }; + + if (_vimModeText != null) + { + if (label != null) + { + _vimModeText.Text = label; + _vimModeText.Visibility = Visibility.Visible; + } + else + { + _vimModeText.Visibility = Visibility.Collapsed; + } + } + + if (_vimBlockCaret == null) return; + + if (mode == VimModes.Insert) + { + _vimBlockCaret.Visibility = Visibility.Collapsed; + _queryTextBox.ClearValue(System.Windows.Controls.TextBox.CaretBrushProperty); + } + else + { + _vimBlockCaret.Visibility = Visibility.Visible; + _queryTextBox.CaretBrush = System.Windows.Media.Brushes.Transparent; + UpdateCaretPosition(); + } + } + + public bool HandlePreviewKeyDown(KeyEventArgs e) + { + if (!_settings.EnableVimMode) + { + if (_vimBlockCaret != null && _vimBlockCaret.Visibility == Visibility.Visible) + { + _vimBlockCaret.Visibility = Visibility.Collapsed; + } + if (_vimModeText != null && _vimModeText.Visibility == Visibility.Visible) + { + _vimModeText.Visibility = Visibility.Collapsed; + } + return false; + } + + if (_vimBlockCaret != null && _vimBlockCaret.Visibility == Visibility.Collapsed && _vimEngine.CurrentMode != VimModes.Insert) + { + _vimBlockCaret.Visibility = Visibility.Visible; + } + + var modifiers = e.KeyboardDevice.Modifiers; + + if (modifiers.HasFlag(ModifierKeys.Control) || modifiers.HasFlag(ModifierKeys.Alt)) + { + return false; + } + + if (e.Key == Key.Escape && e.KeyboardDevice.Modifiers == ModifierKeys.Shift) + { + _viewModel.Hide(); + e.Handled = true; + return true; + } + + if (_vimEngine.CurrentMode == VimModes.Insert) + { + if (e.Key == Key.Escape) + { + _vimEngine.SwitchToNormal(); + _lastEscapeTime = DateTime.Now; + e.Handled = true; + return true; + } + } + else + { + if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + { + if (e.Key == Key.Escape) + { + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + e.Handled = true; + return true; + } + } + + if (e.Key == Key.Escape) + { + if ((DateTime.Now - _lastEscapeTime).TotalMilliseconds < 400) + { + _lastEscapeTime = DateTime.MinValue; + return false; + } + else + { + _lastEscapeTime = DateTime.Now; + _pendingCommand = ""; + e.Handled = true; + return true; + } + } + + if (HandleVimKey(e)) + { + e.Handled = true; + return true; + } + } + + return false; + } + + private bool HandleVimKey(KeyEventArgs e) + { + var modifiers = e.KeyboardDevice.Modifiers; + + if (_vimEngine.CurrentMode == VimModes.Normal || _vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + { + if (!string.IsNullOrEmpty(_awaitingCharCommand)) + { + char c = GetCharFromKey(e.Key, modifiers); + if (c != '\0') + { + ExecuteCharCommand(_awaitingCharCommand, c); + } + else if (e.Key == Key.Escape) + { + _awaitingCharCommand = ""; // Cancel + } + e.Handled = true; + return true; + } + } + + switch (_vimEngine.CurrentMode) + { + case VimModes.Normal: + switch (e.Key) + { + case Key.J: + _viewModel.SelectNextItemCommand.Execute(null); + return true; + case Key.K: + _viewModel.SelectPrevItemCommand.Execute(null); + return true; + case Key.H: + ExecuteMotion(VimMotionEngine.MoveLeft(_queryTextBox.CaretIndex, _queryTextBox.Text.Length)); + return true; + case Key.L: + ExecuteMotion(VimMotionEngine.MoveRight(_queryTextBox.CaretIndex, _queryTextBox.Text.Length)); + return true; + case Key.W: + ExecuteMotion(VimMotionEngine.MoveNextWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + return true; + case Key.B: + ExecuteMotion(VimMotionEngine.MovePrevWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + return true; + case Key.E: + ExecuteMotion(VimMotionEngine.MoveEndWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + return true; + case Key.D0: + if (modifiers.HasFlag(ModifierKeys.Shift)) return false; // Handle ')' normally or ignore + ExecuteMotion(VimMotionEngine.MoveStartOfLine()); + return true; + case Key.D6: + if (modifiers.HasFlag(ModifierKeys.Shift)) // ^ + { + ExecuteMotion(VimMotionEngine.MoveFirstNonBlank(_queryTextBox.Text)); + return true; + } + return false; + case Key.D4: + if (modifiers.HasFlag(ModifierKeys.Shift)) // $ + { + ExecuteMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + return true; + } + return false; + case Key.F: + case Key.T: + case Key.R: + _awaitingCharCommand = modifiers.HasFlag(ModifierKeys.Shift) ? e.Key.ToString() : e.Key.ToString().ToLower(); + return true; + case Key.OemSemicolon: + if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') + { + ExecuteFindCommand(_lastFindCmd, _lastFindChar); + return true; + } + return false; + case Key.OemComma: + if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') + { + string reverseCmd = _lastFindCmd == "f" ? "F" : _lastFindCmd == "F" ? "f" : _lastFindCmd == "t" ? "T" : "t"; + ExecuteFindCommand(reverseCmd, _lastFindChar); + return true; + } + return false; + case Key.X: + if (modifiers.HasFlag(ModifierKeys.Shift)) // X + { + if (_queryTextBox.CaretIndex > 0) + { + int c = _queryTextBox.CaretIndex - 1; + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + _queryTextBox.CaretIndex = c; + } + } + else // x + { + if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) + { + int c = _queryTextBox.CaretIndex; + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + _queryTextBox.CaretIndex = c; + } + } + return true; + case Key.S: + if (modifiers.HasFlag(ModifierKeys.Shift)) // S -> cc (substitute entire line) + { + _queryTextBox.CaretIndex = 0; + _pendingCommand = "c"; + ExecuteMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + } + else // s -> cl + { + if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) + { + int c = _queryTextBox.CaretIndex; + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + _queryTextBox.CaretIndex = c; + } + _vimEngine.SwitchToInsert(); + } + return true; + case Key.OemTilde: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + int c = _queryTextBox.CaretIndex; + if (c < _queryTextBox.Text.Length) + { + char ch = _queryTextBox.Text[c]; + ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); + _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString()); + _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); + } + return true; + } + return false; + + case Key.P: + try + { + string text = Clipboard.GetText(); + if (!string.IsNullOrEmpty(text)) + { + int c = _queryTextBox.CaretIndex; + if (c < _queryTextBox.Text.Length) c++; + _queryTextBox.Text = _queryTextBox.Text.Insert(c, text); + _queryTextBox.CaretIndex = c; + } + } + catch { } + return true; + case Key.U: + _queryTextBox.Undo(); + return true; + case Key.D: + case Key.C: + case Key.Y: + string cmd = e.Key.ToString().ToLower(); + if (modifiers.HasFlag(ModifierKeys.Shift)) // D, C, Y + { + _pendingCommand = cmd; + ExecuteMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + return true; + } + + if (_pendingCommand == cmd) // dd, cc, yy + { + if (!string.IsNullOrEmpty(_queryTextBox.Text)) + { + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + } + if (cmd == "d" || cmd == "c") + { + _queryTextBox.Text = ""; + _queryTextBox.CaretIndex = 0; + } + if (cmd == "c") + { + _vimEngine.SwitchToInsert(); + } + _pendingCommand = ""; + } + else + { + _pendingCommand = cmd; + } + return true; + case Key.I when modifiers == ModifierKeys.None: + _vimEngine.SwitchToInsert(); + return true; + case Key.I when modifiers.HasFlag(ModifierKeys.Shift): + _vimEngine.SwitchToInsert(); + _queryTextBox.CaretIndex = 0; + return true; + case Key.A when modifiers == ModifierKeys.None: + _vimEngine.SwitchToInsert(); + if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) + { + _queryTextBox.CaretIndex++; + } + return true; + case Key.A when modifiers.HasFlag(ModifierKeys.Shift): + _vimEngine.SwitchToInsert(); + _queryTextBox.CaretIndex = _queryTextBox.Text.Length; + return true; + case Key.V when modifiers == ModifierKeys.None: + EnterVisualMode(); + return true; + case Key.V when modifiers.HasFlag(ModifierKeys.Shift): + EnterVisualLineMode(); + return true; + case Key.Escape: + return true; + default: + if (IsVimBlockedKey(e.Key)) + return true; + return false; + } + + case VimModes.Visual: + switch (e.Key) + { + case Key.V when modifiers.HasFlag(ModifierKeys.Shift): + EnterVisualLineMode(); + return true; + case Key.H: + ExecuteVisualMotion(VimMotionEngine.MoveLeft(_visualCaret, _queryTextBox.Text.Length)); + return true; + case Key.L: + ExecuteVisualMotion(VimMotionEngine.MoveRight(_visualCaret, _queryTextBox.Text.Length)); + return true; + case Key.W: + ExecuteVisualMotion(VimMotionEngine.MoveNextWord(_queryTextBox.Text, _visualCaret)); + return true; + case Key.B: + ExecuteVisualMotion(VimMotionEngine.MovePrevWord(_queryTextBox.Text, _visualCaret)); + return true; + case Key.E: + ExecuteVisualMotion(VimMotionEngine.MoveEndWord(_queryTextBox.Text, _visualCaret)); + return true; + case Key.D0: + if (modifiers.HasFlag(ModifierKeys.Shift)) return true; + ExecuteVisualMotion(VimMotionEngine.MoveStartOfLine()); + return true; + case Key.D6: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + ExecuteVisualMotion(VimMotionEngine.MoveFirstNonBlank(_queryTextBox.Text)); + return true; + } + return true; + case Key.D4: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + ExecuteVisualMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + return true; + } + return true; + case Key.F: + case Key.T: + case Key.R: + _awaitingCharCommand = modifiers.HasFlag(ModifierKeys.Shift) ? e.Key.ToString() : e.Key.ToString().ToLower(); + return true; + case Key.OemSemicolon: + if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') + { + ExecuteFindCommand(_lastFindCmd, _lastFindChar); + return true; + } + return true; + case Key.OemComma: + if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') + { + string reverseCmd = _lastFindCmd == "f" ? "F" : _lastFindCmd == "F" ? "f" : _lastFindCmd == "t" ? "T" : "t"; + ExecuteFindCommand(reverseCmd, _lastFindChar); + return true; + } + return true; + case Key.X: + case Key.D: + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); + _queryTextBox.CaretIndex = selStart; + } + _vimEngine.SwitchToNormal(); + } + return true; + case Key.Y: + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + } + _queryTextBox.CaretIndex = selStart; + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + } + return true; + case Key.C: + case Key.S: + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); + _queryTextBox.CaretIndex = selStart; + } + _vimEngine.SwitchToInsert(); + } + return true; + case Key.OemTilde: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) + chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = selStart; + _queryTextBox.SelectionLength = 0; + } + _vimEngine.SwitchToNormal(); + } + return true; + case Key.J: + _viewModel.SelectNextItemCommand.Execute(null); + return true; + case Key.K: + _viewModel.SelectPrevItemCommand.Execute(null); + return true; + default: + return true; + } + + case VimModes.VisualLine: + switch (e.Key) + { + case Key.V when modifiers == ModifierKeys.None: + EnterVisualMode(); + return true; + case Key.X: + case Key.D: + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + _queryTextBox.Text = ""; + _queryTextBox.CaretIndex = 0; + _vimEngine.SwitchToNormal(); + return true; + case Key.Y: + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + _queryTextBox.CaretIndex = 0; + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + return true; + case Key.C: + case Key.S: + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + _queryTextBox.Text = ""; + _queryTextBox.CaretIndex = 0; + _vimEngine.SwitchToInsert(); + return true; + case Key.R: + _awaitingCharCommand = "r"; + return true; + case Key.OemTilde: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + if (_queryTextBox.Text.Length > 0) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = 0; i < chars.Length; i++) + chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = 0; + _queryTextBox.SelectionLength = 0; + } + _vimEngine.SwitchToNormal(); + } + return true; + case Key.J: + _viewModel.SelectNextItemCommand.Execute(null); + return true; + case Key.K: + _viewModel.SelectPrevItemCommand.Execute(null); + return true; + default: + return true; + } + + default: + return false; + } + } + + private void ExecuteCharCommand(string cmd, char c) + { + _awaitingCharCommand = ""; + string text = _queryTextBox.Text; + int caret = _queryTextBox.CaretIndex; + + if (cmd == "r") + { + if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + char[] chars = text.ToCharArray(); + for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) + chars[i] = c; + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = selStart; + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + } + } + else if (caret < text.Length) + { + _queryTextBox.Text = text.Remove(caret, 1).Insert(caret, c.ToString()); + _queryTextBox.CaretIndex = caret; + } + } + else if (cmd == "f" || cmd == "F" || cmd == "t" || cmd == "T") + { + _lastFindCmd = cmd; + _lastFindChar = c; + ExecuteFindCommand(cmd, c); + } + } + + private void ExecuteFindCommand(string cmd, char c) + { + string text = _queryTextBox.Text; + int caret = (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + ? _visualCaret + : _queryTextBox.CaretIndex; + int target = caret; + + if (cmd == "f") target = VimMotionEngine.FindCharForward(text, caret, c, false); + else if (cmd == "F") target = VimMotionEngine.FindCharBackward(text, caret, c, false); + else if (cmd == "t") target = VimMotionEngine.FindCharForward(text, caret, c, true); + else if (cmd == "T") target = VimMotionEngine.FindCharBackward(text, caret, c, true); + + if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + ExecuteVisualMotion(target); + else + ExecuteMotion(target); + } + + private void ExecuteMotion(int targetCaret) + { + if (_pendingCommand == "d" || _pendingCommand == "c") + { + // Deletion + int start = _queryTextBox.CaretIndex; + int end = targetCaret; + if (start > end) { var temp = start; start = end; end = temp; } + + if (end > start) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(start, end - start)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(start, end - start); + _queryTextBox.CaretIndex = start; + } + + if (_pendingCommand == "c") + { + _vimEngine.SwitchToInsert(); + } + _pendingCommand = ""; + } + else + { + _queryTextBox.CaretIndex = targetCaret; + } + } + + private void UpdateVisualSelection() + { + int start = Math.Min(_visualAnchor, _visualCaret); + int end = Math.Max(_visualAnchor, _visualCaret); + int length = Math.Min(end - start + 1, _queryTextBox.Text.Length - start); + if (length < 0) length = 0; + _queryTextBox.Select(start, length); + UpdateCaretPosition(); + } + + private void ExecuteVisualMotion(int targetCaret) + { + _visualCaret = targetCaret; + UpdateVisualSelection(); + } + + private void EnterVisualMode() + { + if (_queryTextBox.Text.Length == 0) return; + if (_queryTextBox.CaretIndex >= _queryTextBox.Text.Length) + _queryTextBox.CaretIndex = _queryTextBox.Text.Length - 1; + _visualAnchor = _queryTextBox.CaretIndex; + _visualCaret = _queryTextBox.CaretIndex; + _vimEngine.SwitchToVisual(); + UpdateVisualSelection(); + } + + private void EnterVisualLineMode() + { + if (_queryTextBox.Text.Length == 0) return; + _visualAnchor = 0; + _visualCaret = _queryTextBox.Text.Length - 1; + _vimEngine.SwitchToVisualLine(); + _queryTextBox.Select(0, _queryTextBox.Text.Length); + UpdateCaretPosition(); + } + + private static bool IsVimBlockedKey(Key key) + { + if (key >= Key.A && key <= Key.Z) return true; + if (key >= Key.D0 && key <= Key.D9) return true; + if (key >= Key.NumPad0 && key <= Key.NumPad9) return true; + if (key == Key.Space) return true; + if (key == Key.OemTilde) return true; + if (key == Key.OemMinus) return true; + if (key == Key.OemPlus) return true; + if (key >= Key.OemOpenBrackets && key <= Key.OemQuotes) return true; + if (key == Key.OemPeriod || key == Key.OemComma) return true; + if (key == Key.Back) return true; + return false; + } + + private static char GetCharFromKey(Key key, ModifierKeys modifiers) + { + bool shift = modifiers.HasFlag(ModifierKeys.Shift); + if (key >= Key.A && key <= Key.Z) + return shift ? key.ToString()[0] : key.ToString().ToLower()[0]; + if (key >= Key.D0 && key <= Key.D9) + { + if (!shift) return (char)('0' + (key - Key.D0)); + switch (key) + { + case Key.D1: return '!'; case Key.D2: return '@'; case Key.D3: return '#'; + case Key.D4: return '$'; case Key.D5: return '%'; case Key.D6: return '^'; + case Key.D7: return '&'; case Key.D8: return '*'; case Key.D9: return '('; + case Key.D0: return ')'; + } + } + switch (key) + { + case Key.Space: return ' '; + case Key.OemSemicolon: return shift ? ':' : ';'; + case Key.OemComma: return shift ? '<' : ','; + case Key.OemPeriod: return shift ? '>' : '.'; + case Key.OemQuestion: return shift ? '?' : '/'; + case Key.OemQuotes: return shift ? '"' : '\''; + case Key.OemOpenBrackets: return shift ? '{' : '['; + case Key.OemCloseBrackets: return shift ? '}' : ']'; + case Key.OemPipe: return shift ? '|' : '\\'; + case Key.OemMinus: return shift ? '_' : '-'; + case Key.OemPlus: return shift ? '+' : '='; + case Key.OemTilde: return shift ? '~' : '`'; + } + return '\0'; + } + + private void QueryTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) + { + if (_vimEngine.CurrentMode != VimModes.Insert) + { + e.Handled = true; + } + } + + public bool IsInputBlocked => _vimEngine.CurrentMode != VimModes.Insert; + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _vimEngine.ModeChanged -= VimEngine_ModeChanged; + _mainWindow.Loaded -= MainWindow_Loaded; + _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + _queryTextBox.PreviewTextInput -= QueryTextBox_PreviewTextInput; + _queryTextBox.SelectionChanged -= QueryTextBox_SelectionChanged; + _queryTextBox.TextChanged -= QueryTextBox_TextChanged; + } + + _disposed = true; + } + } + } +} diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs new file mode 100644 index 00000000000..37875eefc2b --- /dev/null +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -0,0 +1,109 @@ +using System; + +namespace Flow.Launcher.VimMode +{ + public class VimMotionEngine + { + public static int MoveLeft(int caret, int length) + { + return Math.Max(0, caret - 1); + } + + public static int MoveRight(int caret, int length) + { + return Math.Min(length, caret + 1); + } + + public static int MoveStartOfLine() + { + return 0; + } + + public static int MoveEndOfLine(int length) + { + return length; + } + + public static int MoveFirstNonBlank(string text) + { + for (int i = 0; i < text.Length; i++) + { + if (!char.IsWhiteSpace(text[i])) return i; + } + return 0; + } + + public static int MoveNextWord(string text, int caret) + { + if (caret >= text.Length) return text.Length; + + bool startIsWord = IsWordChar(text[caret]); + int i = caret; + + while (i < text.Length && IsWordChar(text[i]) == startIsWord && !char.IsWhiteSpace(text[i])) + i++; + + while (i < text.Length && char.IsWhiteSpace(text[i])) + i++; + + return Math.Min(i, text.Length); + } + + public static int MovePrevWord(string text, int caret) + { + if (caret <= 0) return 0; + + int i = caret - 1; + + while (i > 0 && char.IsWhiteSpace(text[i])) + i--; + + bool targetIsWord = IsWordChar(text[i]); + + while (i > 0 && IsWordChar(text[i - 1]) == targetIsWord && !char.IsWhiteSpace(text[i - 1])) + i--; + + return Math.Max(0, i); + } + + public static int MoveEndWord(string text, int caret) + { + if (caret >= text.Length - 1) return text.Length; + + int i = caret + 1; + + while (i < text.Length - 1 && char.IsWhiteSpace(text[i])) + i++; + + bool targetIsWord = IsWordChar(text[i]); + + while (i < text.Length - 1 && IsWordChar(text[i + 1]) == targetIsWord && !char.IsWhiteSpace(text[i + 1])) + i++; + + return Math.Min(i, text.Length); + } + + private static bool IsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + public static int FindCharForward(string text, int caret, char target, bool till = false) + { + if (caret >= text.Length - 1) return caret; + int start = caret + 1; + int index = text.IndexOf(target, start); + if (index == -1) return caret; + return till ? index - 1 : index; + } + + public static int FindCharBackward(string text, int caret, char target, bool till = false) + { + if (caret <= 0) return caret; + int start = caret - 1; + int index = text.LastIndexOf(target, start); + if (index == -1) return caret; + return till ? index + 1 : index; + } + } +} diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index b4b929d1965..3208907704c 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -1619,7 +1619,7 @@ "FSharp.Core": "[9.0.303, )", "Flow.Launcher.Infrastructure": "[1.0.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.0.0, )", + "Flow.Launcher.Plugin": "[5.3.1, )", "Meziantou.Framework.Win32.Jobs": "[3.4.5, )", "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "SemanticVersioning": "[3.0.0, )", @@ -1634,7 +1634,7 @@ "BitFaster.Caching": "[2.5.4, )", "CommunityToolkit.Mvvm": "[8.4.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.0.0, )", + "Flow.Launcher.Plugin": "[5.3.1, )", "InputSimulator": "[1.0.4, )", "MemoryPack": "[1.21.4, )", "Microsoft.VisualStudio.Threading": "[17.14.15, )", diff --git a/README.md b/README.md index a1a5107412c..f7743ba23b6 100644 --- a/README.md +++ b/README.md @@ -1,437 +1,100 @@ -

- - -
- -

-

- - - -
- - - - -

- -

-A quick file search and app launcher for Windows with community-made plugins.

- -

-Dedicated to making your workflow more seamless. Search everything from applications, files, bookmarks, YouTube, Twitter and more. Flow will continue to evolve, designed to be open and built with the community at heart.

- -

Remember to star it, Flow will love you more :)

- - - -

- Official Channels • - Getting Started • - Features • - Plugins • - Hotkeys • - Sponsors • - Questions/Suggestions • - Development • - Docs -

- - - -## 📣 Official Channels - -- Website: [https://flowlauncher.com](https://flowlauncher.com) -- GitHub Organization: [https://github.com/Flow-Launcher](https://github.com/Flow-Launcher) -- Discord: [https://discord.gg/AvgAQgh](https://discord.gg/AvgAQgh) -- Reddit: [https://www.reddit.com/r/FlowLauncher/](https://www.reddit.com/r/FlowLauncher/) - -⚠️ Only trust official channels for downloads and announcements, and be careful of similar-looking domains. - -For installation, use the official methods listed below. - - - -## 🚗 Getting Started - -### Installation - -[Windows 10+ Installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) or [Portable Version](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) - -#### Winget - -``` -winget install "Flow Launcher" -``` - -#### Scoop - -``` -scoop install Flow-Launcher -``` - -#### Chocolatey - -``` -choco install Flow-Launcher -``` - -> When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo then you are good to continue the set up. - -Or download the [early access version](https://github.com/Flow-Launcher/Prereleases/releases). - - - -## 🎁 Features - -### Applications & Files - - - -- Search for apps, files or file contents. -- Supports Everything and Windows Index. - - - -- Supports search using environment variable paths. - -### Web Searches & URLs - - - - - -### Browser Bookmarks - - - -### System Commands - - - -- Provides system related commands. shutdown, lock, settings, etc. -- [System command list](#system-command-list) - -### Calculator - - - -- Do mathematical calculations and copy the result to clipboard. - -### Shell Command - - - -- Run batch and PowerShell commands as Administrator or a different user. -- Ctrl+Shift+Enter to Run as Administrator. - -### Explorer - - - -- Save file or folder locations for quick access. - -### Windows & Control Panel Settings - - - -- Search for Windows & Control Panel settings. - -### Dialog Jump - - - -- Search for a file/folder and quickly jump to its path in the Open/Save As dialog window. -- Use Alt+G to quickly jump the Open/Save As dialog window path to the path of the active file manager. - -### Drag & Drop - - - -- Drag a file/folder to File Explorer, or even Discord. -- Copy/move behavior can be changed via Ctrl or Shift, and the operation is displayed on the mouse cursor. - -### Priority - - - -- Prioritize the order of each plugin's results. - -### Preview Panel - - - -- Use F1 to toggle the preview panel. -- Media files will be displayed as large images, otherwise a large icon and full path will be displayed. -- Turn on preview permanently via Settings (Always Preview). -- Use Ctrl++/- and Ctrl+[/] to adjust search window width and height quickly if the preview area is too narrow. - -### Customizations - - - -- Window size adjustment, animation, and sound -- Color Scheme (aka Dark Mode) - - - -- There are various themes and you also can make your own. - -### Date & Time Display In Search Window - - - -- Display date and time in search window. - -### 💬 Languages - -- Supports languages from Chinese to Italian and more. -- Supports Pinyin (拼音) search. -- [Crowdin](https://crowdin.com/project/flow-launcher) support for language translations. - -
-Supported languages -
    -
  • English
  • -
  • 中文
  • -
  • 中文(繁体)
  • -
  • Українська
  • -
  • Русский
  • -
  • Français
  • -
  • 日本語
  • -
  • Dutch
  • -
  • Polski
  • -
  • Dansk
  • -
  • de, Deutsch
  • -
  • ko, 한국어
  • -
  • Srpski
  • -
  • Српски
  • -
  • Português
  • -
  • Português (Brasil)
  • -
  • Spanish
  • -
  • es-419, Spanish (Latin America)
  • -
  • Italiano
  • -
  • Norsk Bokmål
  • -
  • Slovenčina
  • -
  • Türkçe
  • -
  • čeština
  • -
  • اللغة العربية
  • -
  • Tiếng Việt
  • -
  • עברית
  • -
-
- -### Portable - -- Fully portable. -- Type `flow user data` to open your saved user settings folder. They are located at: - - If using roaming: `%APPDATA%\FlowLauncher` - - If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData` - - Type `open log location` to open your logs folder, they are saved along with your user settings folder. - -### 🎮 Game Mode - - - -- Pause hotkey activation when you are playing games. -- When in search window use Ctrl+F12 to toggle on/off. -- Type `Toggle Game Mode` - - - -## 📦 Plugins - -- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio. -- Publish your own plugin to Flow! Create plugins in: - -

- - - - - -

- -### [SpotifyPremium](https://github.com/fow5040/Flow.Launcher.Plugin.SpotifyPremium) - - - -### [Steam Search](https://github.com/Garulf/Steam-Search) - - - -### [Clipboard+](https://github.com/Jack251970/Flow.Launcher.Plugin.ClipboardPlus) - - - -### [Home Assistant Commander](https://github.com/Garulf/HA-Commander) - - - -### [Colors](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Color) - - - -### [GitHub](https://github.com/JohnTheGr8/Flow.Plugin.Github) - - - -### [Window Walker](https://github.com/taooceros/Flow.Plugin.WindowWalker) - - - -......and [more!](https://flowlauncher.com/docs/#/plugins) - - - -### 🛒 Plugin Store - - - -- You can view the full plugin list or quickly install a plugin via the Plugin Store menu inside Settings - -- or type `pm` `install`/`uninstall`/`update` + the plugin name in the search window, - - - -## ⌨️ Hotkeys - -| Hotkey | Description | -| ------------------------------------------------------------------------- | ----------------------------------------------- | -| Alt+Space | Open search window (default and configurable) | -| Enter | Execute | -| Ctrl+Enter | Open containing folder | -| Ctrl+Shift+Enter | Run as admin | -| /, Shift+Tab/Tab | Previous / Next result | -| / | Back to result / Open Context Menu | -| Ctrl+O , Shift+Enter | Open Context Menu | -| Ctrl+Tab | Autocomplete | -| F1 | Toggle Preview Panel (default and configurable) | -| Esc | Back to results / hide search window | -| Ctrl+C | Copy folder / file | -| Ctrl+Shift+C | Copy folder / file path | -| Ctrl+I | Open Flow's settings | -| Ctrl+R | Run the current query again (refresh results) | -| F5 | Reload all plugin data | -| Ctrl+F12 | Toggle Game Mode when in search window | -| Ctrl++,- | Adjust maximum results shown | -| Ctrl+[,] | Adjust search window width | -| Ctrl+H | Open search history | -| Ctrl+Backspace | Back to previous directory | -| PageUp/PageDown | Previous / Next Page | - -## System Command List - -| Command | Description | -| ---------------------------------- | --------------------------------------------------------------------------- | -| Shutdown | Shutdown computer | -| Restart | Restart computer | -| Restart With Advanced Boot Options | Restart the computer with Advanced Boot option for safe and debugging modes | -| Log Off/Sign Out | Log off | -| Lock | Lock computer | -| Sleep | Put computer to sleep | -| Hibernate | Hibernate computer | -| Empty Recycle Bin | Empty recycle bin | -| Open Recycle Bin | Open recycle bin | -| Exit | Close Flow Launcher | -| Save Settings | Save all Flow Launcher settings | -| Restart Flow Launcher | Restart Flow Launcher | -| Settings | Tweak this app | -| Reload Plugin Data | Refreshes plugin data with new content | -| Check For Update | Check for new Flow Launcher update | -| Open Log Location | Open Flow Launcher's log location | -| Index Option | Open Windows Search Index window | -| Flow Launcher Tips | Visit Flow Launcher's documentation for more help and usage tips | -| Flow Launcher UserData Folder | Open the location where Flow Launcher's settings are stored | -| Toggle Game Mode | Toggle Game Mode | -| Set Flow Launcher Theme | Set the Flow Launcher Theme | - -### 💁‍♂️ Tips - -- [More tips](https://flowlauncher.com/docs/#/usage-tips) - - - -## Sponsors -

- - Coderabbit Logo - -
-
- - - - - - -

-

- - Appwrite Logo - -
-

-

- - - - - - - - - -

- -### Mentions - -- [Why I Chose to Support Flow-Launcher](https://dev.to/appwrite/appwrite-loves-open-source-why-i-chose-to-support-flow-launcher-54pj) - Appwrite -- [Softpedia Editor's Pick](https://www.softpedia.com/get/System/Launchers-Shutdown-Tools/Flow-Launcher.shtml) - - - -## ❔ Questions/Suggestions - -Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section. **Join our community on [Discord](https://discord.gg/AvgAQgh)!** - - - -## Development - -### Localization - -Our project localization is based on [Crowdin](https://crowdin.com). If you would like to change them, please go to https://crowdin.com/project/flow-launcher. - -### WPF UI Library - -Our UI library is using [iNKORE.UI.WPF.Modern](https://github.com/iNKORE-NET/UI.WPF.Modern). - - - iNKORE.UI.WPF.Modern - - -### New Changes - -All changes to Flow are captured via pull requests. Some new changes may have been merged but are still pending release. This means that while a change may not exist in the current release, it may have been accepted and merged into the dev branch and is available as a pre-release download. It is therefore a good idea to search through the open and closed pull requests before you start to make changes to ensure the change you intend to make is not already done. - -Each of the pull requests will be marked with a milestone indicating the planned release version for the change. - -### Contributing - -Contributions are very welcome, in addition to the main project (C#) there are also [documentation](https://github.com/Flow-Launcher/docs) (md), [website](https://github.com/Flow-Launcher/flow-launcher.github.io) (html/css) and [others](https://github.com/Flow-Launcher) that can be contributed to. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to submit a pull request. - -You will find the main goals of Flow placed under the [Projects board](https://github.com/orgs/Flow-Launcher/projects/4), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well. - -Get in touch if you would like to join the Flow-Launcher Team and help build this great tool. - -### Developing/Debugging - -- Flow Launcher's target framework is .Net 9 - -- Install Visual Studio 2022 (v17.12+) - -- Install .Net 9 SDK - - via Visual Studio installer - - via winget `winget install Microsoft.DotNet.SDK.9` - - Manually from [here](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) +# Flow Launcher - Vim Edition + +This is a specialized fork of [Flow Launcher](https://github.com/Flow-Launcher/Flow.Launcher), equipped with a fully featured, terminal-grade inline **Vim Mode** directly integrated into the search bar. + +### Why does this exist? +This fork was originally created to streamline journaling workflows. By using Flow Launcher to quickly input journal entries into a personal journaling system, typing out long strings quickly became tedious to edit. Standard text box navigation isn't fast enough. This fork adds native Vim motions directly to the search bar so that when you make a mistake typing a long entry or command, you can instantly drop into Normal mode, hop across word boundaries, and fix the typo without your hands ever leaving the home row. + +--- + +## 🛠️ Advanced Vim Features + +Vim Mode transforms the Flow Launcher search bar into a modal editor, offering **Insert**, **Normal**, **Visual**, and **Visual Line** modes, complete with an overlaid block caret that physically tracks to the text layout. + +You can instantly toggle this feature on or off via the `General` tab in the Flow Launcher Settings. + +### Modes & Intercepts +- **Insert Mode**: The default state. Works exactly like standard Flow Launcher (blinking text caret). +- **Normal Mode**: Replaces the blinking caret with a solid block caret, and intercepts alphanumeric keystrokes to execute text manipulation commands. +- **Visual Mode**: Char-wise selection. Movements extend the selection from a fixed anchor point; operators apply to the selected region. +- **Visual Line Mode**: Line-wise selection. Selects the entire query; operators apply to the whole line. +- **Escape**: Pressing `Escape` while typing will unconditionally kick you into Normal mode. +- **Double-Escape**: Quickly double-tapping `Escape` (within 400ms) will immediately hide the Flow Launcher UI. + +### Normal Mode Keybindings + +#### Basic Navigation +- `h` / `l` : Move cursor left / right +- `j` / `k` : Navigate up / down through Flow Launcher search results +- `0` : Snap to the absolute beginning of the query +- `^` : Snap to the first non-blank character of the query +- `$` : Snap to the end of the query + +#### Word Boundaries +- `w` : Jump to the start of the next word boundary +- `e` : Jump to the end of the current word boundary +- `b` : Jump back to the start of the previous word boundary + +#### Character Lookup +- `f{char}` : Find the next occurrence of `{char}` forward +- `F{char}` : Find the previous occurrence of `{char}` backward +- `t{char}` : Jump till just before the next occurrence of `{char}` +- `T{char}` : Jump till just after the previous occurrence of `{char}` +- `;` : Repeat the last find lookup in the same direction +- `,` : Repeat the last find lookup in the opposite direction + +#### Text Manipulation (Integrated with System Clipboard) +- `x` : Delete the character under the cursor +- `X` (Shift+X) : Delete the character before the cursor (Backspace equivalent) +- `s` : Substitute character (Deletes under cursor and enters Insert mode) +- `S` (Shift+S) : Substitute line (Clears query and enters Insert mode) +- `r{char}` : Replace the character under the cursor with `{char}` +- `~` : Toggle the casing of the character under the cursor +- `dd` / `cc` : Delete or Change the entire query +- `D` / `C` : Delete or Change from the cursor to the end of the line +- `Y` (Shift+Y) : Yank (Copy) the entire query +- `p` : Paste from system clipboard +- `u` : Undo the last edit (hooks into WPF native undo stack) + +#### Mode Switching +- `i` : Enter Insert mode at the cursor +- `I` (Shift+I) : Enter Insert mode at the beginning of the query +- `a` : Enter Insert mode after the cursor +- `A` (Shift+A) : Enter Insert mode at the end of the query +- `v` : Enter Visual mode (char-wise selection) +- `V` (Shift+V) : Enter Visual Line mode (select entire query) + +### Visual Mode Keybindings + +#### Entering Visual Mode +- `v` : From Normal mode, enter char-wise Visual mode at the cursor +- `V` (Shift+V) : From Normal mode, enter Visual Line mode (selects entire query) + +#### Extending the Selection (Visual mode only) +- `h` / `l` : Extend selection left / right +- `w` / `b` / `e` : Extend by word boundary +- `0` / `^` / `$` : Extend to beginning / first non-blank / end of query +- `f{char}` / `F{char}` / `t{char}` / `T{char}` : Extend to character lookup +- `;` / `,` : Repeat last character lookup + +#### Operators (work on the selected region) +- `x` / `d` : Delete the selection (copies to clipboard) +- `y` : Yank (copy) the selection +- `c` / `s` : Change the selection (delete and enter Insert mode) +- `r{char}` : Replace every character in the selection with `{char}` +- `~` : Toggle casing of every character in the selection + +#### Mode Transitions +- `Esc` : Return to Normal mode (clears selection) +- `v` : In Visual Line mode, switch to char-wise Visual mode +- `V` : In Visual mode, switch to Visual Line mode (selects entire query) +- `j` / `k` : Navigate search results (same as Normal mode) + +--- + +*(The below is the standard Flow Launcher README)* + +# Flow Launcher +Flow Launcher is a quick file search and app launcher for Windows with community-made plugins. + +[Website](https://flowlauncher.com) | [Documentation](https://flowlauncher.com/docs/) | [Plugin Store](https://flowlauncher.com/docs/plugins.html) From 6077ebfc40340c07c94fc8ef4a8030a84b422b28 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 21:13:46 -0500 Subject: [PATCH 02/30] Fix vim mode indicator and visual mode search/motion --- Flow.Launcher/VimMode/VimManager.cs | 428 +++++++++++++++++++++-- Flow.Launcher/VimMode/VimMotionEngine.cs | 182 +++++++++- 2 files changed, 582 insertions(+), 28 deletions(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 77f651d65c9..1fdae0bf9fe 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -22,6 +22,11 @@ public class VimManager : IDisposable private DateTime _lastEscapeTime = DateTime.MinValue; private int _visualAnchor; private int _visualCaret; + private int _count; + private bool _gPending; + private string _awaitingTextObject = ""; + private (int anchor, int caret)? _lastVisualRange; + private string _lastChange = ""; private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryTextBox, System.Windows.Shapes.Rectangle vimBlockCaret, TextBlock vimModeText, Flow.Launcher.Infrastructure.UserSettings.Settings settings) @@ -181,6 +186,13 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) var modifiers = e.KeyboardDevice.Modifiers; + if (modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.R && _vimEngine.CurrentMode == VimModes.Normal) + { + try { _queryTextBox.Redo(); } catch { } + e.Handled = true; + return true; + } + if (modifiers.HasFlag(ModifierKeys.Control) || modifiers.HasFlag(ModifierKeys.Alt)) { return false; @@ -209,6 +221,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { if (e.Key == Key.Escape) { + SaveVisualRange(); _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); e.Handled = true; @@ -246,6 +259,34 @@ private bool HandleVimKey(KeyEventArgs e) { var modifiers = e.KeyboardDevice.Modifiers; + if (_vimEngine.CurrentMode == VimModes.Normal || _vimEngine.CurrentMode == VimModes.Visual) + { + if (_gPending) + { + _gPending = false; + return HandleGKey(e, modifiers); + } + + if (string.IsNullOrEmpty(_awaitingCharCommand) && string.IsNullOrEmpty(_awaitingTextObject) && _pendingCommand == "") + { + if (e.Key >= Key.D1 && e.Key <= Key.D9 && !modifiers.HasFlag(ModifierKeys.Shift)) + { + _count = _count * 10 + (e.Key - Key.D0); + return true; + } + if (e.Key == Key.D0 && !modifiers.HasFlag(ModifierKeys.Shift) && _count > 0) + { + _count = _count * 10; + return true; + } + } + + if (string.IsNullOrEmpty(_awaitingCharCommand) && _awaitingTextObject.Length > 0) + { + return HandleTextObjectKey(e, modifiers); + } + } + if (_vimEngine.CurrentMode == VimModes.Normal || _vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) { if (!string.IsNullOrEmpty(_awaitingCharCommand)) @@ -276,19 +317,38 @@ private bool HandleVimKey(KeyEventArgs e) _viewModel.SelectPrevItemCommand.Execute(null); return true; case Key.H: - ExecuteMotion(VimMotionEngine.MoveLeft(_queryTextBox.CaretIndex, _queryTextBox.Text.Length)); + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i, _queryTextBox.Text.Length))); return true; case Key.L: - ExecuteMotion(VimMotionEngine.MoveRight(_queryTextBox.CaretIndex, _queryTextBox.Text.Length)); + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveRight(i, _queryTextBox.Text.Length))); + return true; + case Key.W when modifiers == ModifierKeys.None: + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveNextWord(_queryTextBox.Text, i))); + return true; + case Key.B when modifiers == ModifierKeys.None: + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWord(_queryTextBox.Text, i))); return true; - case Key.W: - ExecuteMotion(VimMotionEngine.MoveNextWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + case Key.E when modifiers == ModifierKeys.None: + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWord(_queryTextBox.Text, i))); return true; - case Key.B: - ExecuteMotion(VimMotionEngine.MovePrevWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + case Key.W when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveNextWordBig(_queryTextBox.Text, i))); return true; - case Key.E: - ExecuteMotion(VimMotionEngine.MoveEndWord(_queryTextBox.Text, _queryTextBox.CaretIndex)); + case Key.B when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWordBig(_queryTextBox.Text, i))); + return true; + case Key.E when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWordBig(_queryTextBox.Text, i))); + return true; + case Key.D5 when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteMotion(VimMotionEngine.FindMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex)); + return true; + case Key.G: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + _gPending = true; + return true; + } return true; case Key.D0: if (modifiers.HasFlag(ModifierKeys.Shift)) return false; // Handle ')' normally or ignore @@ -341,13 +401,16 @@ private bool HandleVimKey(KeyEventArgs e) } else // x { + int n = GetCount(); if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) { int c = _queryTextBox.CaretIndex; - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } - _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + int len = Math.Min(n, _queryTextBox.Text.Length - c); + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); _queryTextBox.CaretIndex = c; } + _lastChange = "x"; } return true; case Key.S: @@ -380,6 +443,7 @@ private bool HandleVimKey(KeyEventArgs e) _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString()); _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); } + _lastChange = "~"; return true; } return false; @@ -409,6 +473,7 @@ private bool HandleVimKey(KeyEventArgs e) { _pendingCommand = cmd; ExecuteMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + _lastChange = cmd.ToUpper() + "_eol"; return true; } @@ -424,24 +489,34 @@ private bool HandleVimKey(KeyEventArgs e) _queryTextBox.CaretIndex = 0; } if (cmd == "c") - { _vimEngine.SwitchToInsert(); - } + _lastChange = cmd + cmd; _pendingCommand = ""; } + else if (_pendingCommand != "" && _pendingCommand != cmd) + { + _pendingCommand = ""; + return true; + } else { _pendingCommand = cmd; } return true; - case Key.I when modifiers == ModifierKeys.None: + case Key.A when _pendingCommand == "d" || _pendingCommand == "c" || _pendingCommand == "y": + _awaitingTextObject = "a"; + return true; + case Key.I when _pendingCommand == "d" || _pendingCommand == "c" || _pendingCommand == "y": + _awaitingTextObject = "i"; + return true; + case Key.I when modifiers == ModifierKeys.None && _pendingCommand == "": _vimEngine.SwitchToInsert(); return true; case Key.I when modifiers.HasFlag(ModifierKeys.Shift): _vimEngine.SwitchToInsert(); _queryTextBox.CaretIndex = 0; return true; - case Key.A when modifiers == ModifierKeys.None: + case Key.A when modifiers == ModifierKeys.None && _pendingCommand == "": _vimEngine.SwitchToInsert(); if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) { @@ -458,6 +533,10 @@ private bool HandleVimKey(KeyEventArgs e) case Key.V when modifiers.HasFlag(ModifierKeys.Shift): EnterVisualLineMode(); return true; + case Key.OemPeriod: + if (!string.IsNullOrEmpty(_lastChange)) + RepeatLastChange(); + return true; case Key.Escape: return true; default: @@ -472,25 +551,56 @@ private bool HandleVimKey(KeyEventArgs e) case Key.V when modifiers.HasFlag(ModifierKeys.Shift): EnterVisualLineMode(); return true; + case Key.O when modifiers == ModifierKeys.None: + SwapVisualEnds(); + return true; + case Key.I when modifiers == ModifierKeys.None: + { + int pos = Math.Min(_visualAnchor, _visualCaret); + _queryTextBox.SelectionLength = 0; + _queryTextBox.CaretIndex = pos; + _vimEngine.SwitchToInsert(); + } + return true; + case Key.A when modifiers == ModifierKeys.None: + { + int pos = Math.Max(_visualAnchor, _visualCaret) + 1; + _queryTextBox.SelectionLength = 0; + _queryTextBox.CaretIndex = Math.Min(pos, _queryTextBox.Text.Length); + _vimEngine.SwitchToInsert(); + } + return true; case Key.H: - ExecuteVisualMotion(VimMotionEngine.MoveLeft(_visualCaret, _queryTextBox.Text.Length)); + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i, _queryTextBox.Text.Length))); return true; case Key.L: - ExecuteVisualMotion(VimMotionEngine.MoveRight(_visualCaret, _queryTextBox.Text.Length)); + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveRight(i, _queryTextBox.Text.Length))); + return true; + case Key.W when modifiers == ModifierKeys.None: + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveNextWord(_queryTextBox.Text, i))); + return true; + case Key.B when modifiers == ModifierKeys.None: + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWord(_queryTextBox.Text, i))); return true; - case Key.W: - ExecuteVisualMotion(VimMotionEngine.MoveNextWord(_queryTextBox.Text, _visualCaret)); + case Key.E when modifiers == ModifierKeys.None: + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWord(_queryTextBox.Text, i))); return true; - case Key.B: - ExecuteVisualMotion(VimMotionEngine.MovePrevWord(_queryTextBox.Text, _visualCaret)); + case Key.W when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveNextWordBig(_queryTextBox.Text, i))); return true; - case Key.E: - ExecuteVisualMotion(VimMotionEngine.MoveEndWord(_queryTextBox.Text, _visualCaret)); + case Key.B when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWordBig(_queryTextBox.Text, i))); + return true; + case Key.E when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWordBig(_queryTextBox.Text, i))); return true; case Key.D0: if (modifiers.HasFlag(ModifierKeys.Shift)) return true; ExecuteVisualMotion(VimMotionEngine.MoveStartOfLine()); return true; + case Key.D5 when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteVisualMotion(VimMotionEngine.FindMatchingBracket(_queryTextBox.Text, _visualCaret)); + return true; case Key.D6: if (modifiers.HasFlag(ModifierKeys.Shift)) { @@ -525,6 +635,12 @@ private bool HandleVimKey(KeyEventArgs e) return true; } return true; + case Key.A when modifiers == ModifierKeys.None: + _awaitingTextObject = "a"; + return true; + case Key.I when modifiers == ModifierKeys.None: + _awaitingTextObject = "i"; + return true; case Key.X: case Key.D: { @@ -652,6 +768,207 @@ private bool HandleVimKey(KeyEventArgs e) } } + private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) + { + switch (_vimEngine.CurrentMode) + { + case VimModes.Normal: + switch (e.Key) + { + case Key.OemMinus when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteMotion(VimMotionEngine.MoveLastNonBlank(_queryTextBox.Text)); + return true; + case Key.T when modifiers.HasFlag(ModifierKeys.Shift): + _pendingCommand = "~"; + return true; + case Key.U when modifiers == ModifierKeys.None: + _pendingCommand = "gu"; + return true; + case Key.U when modifiers.HasFlag(ModifierKeys.Shift): + _pendingCommand = "gU"; + return true; + case Key.V: + if (_lastVisualRange != null) + { + _visualAnchor = _lastVisualRange.Value.anchor; + _visualCaret = _lastVisualRange.Value.caret; + _vimEngine.SwitchToVisual(); + UpdateVisualSelection(); + } + return true; + case Key.OemTilde: + _pendingCommand = "~"; + return true; + default: + return true; + } + case VimModes.Visual: + switch (e.Key) + { + case Key.V: + if (_lastVisualRange != null) + { + _visualAnchor = _lastVisualRange.Value.anchor; + _visualCaret = _lastVisualRange.Value.caret; + UpdateVisualSelection(); + } + return true; + default: + return true; + } + default: + return true; + } + } + + private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) + { + string prefix = _awaitingTextObject; + _awaitingTextObject = ""; + + char delim = GetCharFromKey(e.Key, modifiers); + if (delim == '\0' && e.Key != Key.W) return true; + if (e.Key == Key.W) delim = 'w'; + + string text = _queryTextBox.Text; + int caret = (_vimEngine.CurrentMode == VimModes.Visual) ? _visualCaret : _queryTextBox.CaretIndex; + bool around = prefix == "a"; + (int start, int end) range = (0, 0); + + switch (delim) + { + case 'w': + range = VimMotionEngine.TextObjectWord(text, caret, around); + break; + case '"': + range = VimMotionEngine.TextObjectQuote(text, caret, '"', around); + break; + case '\'': + range = VimMotionEngine.TextObjectQuote(text, caret, '\'', around); + break; + case '(': + case ')': + range = VimMotionEngine.TextObjectDelimited(text, caret, '(', ')', around); + break; + case '[': + case ']': + range = VimMotionEngine.TextObjectDelimited(text, caret, '[', ']', around); + break; + case '{': + case '}': + range = VimMotionEngine.TextObjectDelimited(text, caret, '{', '}', around); + break; + default: + return true; + } + + if (range.start < 0) return true; + + if (_vimEngine.CurrentMode == VimModes.Visual) + { + _visualAnchor = range.start; + _visualCaret = range.end; + UpdateVisualSelection(); + return true; + } + + if (_pendingCommand == "d" || _pendingCommand == "c" || _pendingCommand == "y") + { + int len = range.end - range.start + 1; + if (len > 0) + { + try { Clipboard.SetText(text.Substring(range.start, len)); } catch { } + if (_pendingCommand != "y") + { + _queryTextBox.Text = text.Remove(range.start, len); + _queryTextBox.CaretIndex = range.start; + } + if (_pendingCommand == "c") + _vimEngine.SwitchToInsert(); + } + _pendingCommand = ""; + return true; + } + + return true; + } + + private int ApplyCountMove(Func move) + { + int target = (_vimEngine.CurrentMode == VimModes.Visual) ? _visualCaret : _queryTextBox.CaretIndex; + int n = _count > 0 ? _count : 1; + for (int i = 0; i < n; i++) + target = move(target); + _count = 0; + return target; + } + + private int GetCount() + { + int c = _count > 0 ? _count : 1; + _count = 0; + return c; + } + + private void RepeatLastChange() + { + switch (_lastChange) + { + case "dd": + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + _queryTextBox.Text = ""; + _queryTextBox.CaretIndex = 0; + break; + case "cc": + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + _queryTextBox.Text = ""; + _queryTextBox.CaretIndex = 0; + _vimEngine.SwitchToInsert(); + break; + case "D_eol": + case "C_eol": + { + int c = _queryTextBox.CaretIndex; + if (c < _queryTextBox.Text.Length) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(c)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c); + _queryTextBox.CaretIndex = c; + } + if (_lastChange == "C_eol") _vimEngine.SwitchToInsert(); + } + break; + case "Y_eol": + try { Clipboard.SetText(_queryTextBox.Text); } catch { } + break; + case "x": + { + int n = GetCount(); + int c = _queryTextBox.CaretIndex; + int len = Math.Min(n, _queryTextBox.Text.Length - c); + if (len > 0) + { + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch { } + _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); + _queryTextBox.CaretIndex = c; + } + } + break; + case "~": + { + int c = _queryTextBox.CaretIndex; + if (c < _queryTextBox.Text.Length) + { + char ch = _queryTextBox.Text[c]; + ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); + _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString()); + _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); + } + } + break; + } + } + private void ExecuteCharCommand(string cmd, char c) { _awaitingCharCommand = ""; @@ -712,7 +1029,6 @@ private void ExecuteMotion(int targetCaret) { if (_pendingCommand == "d" || _pendingCommand == "c") { - // Deletion int start = _queryTextBox.CaretIndex; int end = targetCaret; if (start > end) { var temp = start; start = end; end = temp; } @@ -725,8 +1041,59 @@ private void ExecuteMotion(int targetCaret) } if (_pendingCommand == "c") - { _vimEngine.SwitchToInsert(); + _lastChange = _pendingCommand + "_motion"; + _pendingCommand = ""; + } + else if (_pendingCommand == "~") + { + int start = _queryTextBox.CaretIndex; + int end = targetCaret; + if (start > end) { var temp = start; start = end; end = temp; } + if (end < _queryTextBox.Text.Length) end++; + + if (end > start) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = start; i < end && i < chars.Length; i++) + chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = Math.Min(start, _queryTextBox.Text.Length); + } + _lastChange = "~"; + _pendingCommand = ""; + } + else if (_pendingCommand == "gu") + { + int start = _queryTextBox.CaretIndex; + int end = targetCaret; + if (start > end) { var temp = start; start = end; end = temp; } + if (end < _queryTextBox.Text.Length) end++; + + if (end > start) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = start; i < end && i < chars.Length; i++) + chars[i] = char.ToLower(chars[i]); + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = start; + } + _pendingCommand = ""; + } + else if (_pendingCommand == "gU") + { + int start = _queryTextBox.CaretIndex; + int end = targetCaret; + if (start > end) { var temp = start; start = end; end = temp; } + if (end < _queryTextBox.Text.Length) end++; + + if (end > start) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = start; i < end && i < chars.Length; i++) + chars[i] = char.ToUpper(chars[i]); + _queryTextBox.Text = new string(chars); + _queryTextBox.CaretIndex = start; } _pendingCommand = ""; } @@ -773,6 +1140,19 @@ private void EnterVisualLineMode() UpdateCaretPosition(); } + private void SwapVisualEnds() + { + int temp = _visualAnchor; + _visualAnchor = _visualCaret; + _visualCaret = temp; + UpdateVisualSelection(); + } + + private void SaveVisualRange() + { + _lastVisualRange = (_visualAnchor, _visualCaret); + } + private static bool IsVimBlockedKey(Key key) { if (key >= Key.A && key <= Key.Z) return true; diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index 37875eefc2b..f6feae2c6e8 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -33,10 +33,19 @@ public static int MoveFirstNonBlank(string text) return 0; } + public static int MoveLastNonBlank(string text) + { + for (int i = text.Length - 1; i >= 0; i--) + { + if (!char.IsWhiteSpace(text[i])) return Math.Min(i + 1, text.Length); + } + return 0; + } + public static int MoveNextWord(string text, int caret) { if (caret >= text.Length) return text.Length; - + bool startIsWord = IsWordChar(text[caret]); int i = caret; @@ -52,14 +61,14 @@ public static int MoveNextWord(string text, int caret) public static int MovePrevWord(string text, int caret) { if (caret <= 0) return 0; - + int i = caret - 1; - + while (i > 0 && char.IsWhiteSpace(text[i])) i--; bool targetIsWord = IsWordChar(text[i]); - + while (i > 0 && IsWordChar(text[i - 1]) == targetIsWord && !char.IsWhiteSpace(text[i - 1])) i--; @@ -83,6 +92,171 @@ public static int MoveEndWord(string text, int caret) return Math.Min(i, text.Length); } + public static int MoveNextWordBig(string text, int caret) + { + if (caret >= text.Length) return text.Length; + + int i = caret; + + while (i < text.Length && !char.IsWhiteSpace(text[i])) + i++; + + while (i < text.Length && char.IsWhiteSpace(text[i])) + i++; + + return Math.Min(i, text.Length); + } + + public static int MovePrevWordBig(string text, int caret) + { + if (caret <= 0) return 0; + + int i = caret - 1; + + while (i > 0 && char.IsWhiteSpace(text[i])) + i--; + + while (i > 0 && !char.IsWhiteSpace(text[i - 1])) + i--; + + return Math.Max(0, i); + } + + public static int MoveEndWordBig(string text, int caret) + { + if (caret >= text.Length - 1) return text.Length; + + int i = caret + 1; + + while (i < text.Length - 1 && char.IsWhiteSpace(text[i])) + i++; + + while (i < text.Length - 1 && !char.IsWhiteSpace(text[i + 1])) + i++; + + return Math.Min(i, text.Length); + } + + public static int FindMatchingBracket(string text, int caret) + { + if (caret >= text.Length) return caret; + char c = text[caret]; + + char target; + bool forward; + switch (c) + { + case '(': target = ')'; forward = true; break; + case ')': target = '('; forward = false; break; + case '[': target = ']'; forward = true; break; + case ']': target = '['; forward = false; break; + case '{': target = '}'; forward = true; break; + case '}': target = '{'; forward = false; break; + default: return caret; + } + + int depth = 0; + char open = forward ? c : target; + char close = forward ? target : c; + + if (forward) + { + for (int i = caret; i < text.Length; i++) + { + if (text[i] == open) depth++; + else if (text[i] == close) { depth--; if (depth == 0) return i; } + } + } + else + { + for (int i = caret; i >= 0; i--) + { + if (text[i] == close) depth++; + else if (text[i] == open) { depth--; if (depth == 0) return i; } + } + } + + return caret; + } + + public static (int start, int end) TextObjectWord(string text, int caret, bool around) + { + if (text.Length == 0) return (0, 0); + + int start, end; + bool startIsWord = IsWordChar(text[Math.Min(caret, text.Length - 1)]); + + int i = Math.Min(caret, text.Length - 1); + while (i > 0 && IsWordChar(text[i - 1]) == startIsWord && !char.IsWhiteSpace(text[i - 1])) + i--; + start = i; + + i = Math.Min(caret, text.Length - 1); + while (i < text.Length - 1 && IsWordChar(text[i + 1]) == startIsWord && !char.IsWhiteSpace(text[i + 1])) + i++; + end = i; + + if (around) + { + if (end + 1 < text.Length && char.IsWhiteSpace(text[end + 1])) + end++; + else if (start > 0 && char.IsWhiteSpace(text[start - 1])) + start--; + } + + return (start, end); + } + + public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) + { + int depth = 0; + int openPos = -1; + int closePos = -1; + + for (int i = 0; i < text.Length; i++) + { + if (text[i] == open) + { + if (i <= caret) { openPos = i; depth = 1; closePos = -1; } + else if (depth > 0) depth++; + } + else if (text[i] == close && depth > 0) + { + depth--; + if (depth == 0) { closePos = i; break; } + } + } + + if (openPos == -1 || closePos == -1) return (-1, -1); + + int start = around ? openPos : openPos + 1; + int end = around ? closePos : closePos - 1; + + return (start, end); + } + + public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) + { + int first = -1, second = -1; + + for (int i = 0; i < text.Length; i++) + { + if (text[i] == quote) + { + if (first == -1) first = i; + else if (second == -1) { second = i; break; } + } + } + + if (first == -1 || second == -1) return (-1, -1); + if (caret < first || caret > second) return (-1, -1); + + int start = around ? first : first + 1; + int end = around ? second : second - 1; + + return (start, end); + } + private static bool IsWordChar(char c) { return char.IsLetterOrDigit(c) || c == '_'; From 672b3aafc3bcf5756828cdc0cdc8e3da66ada042 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 21:25:03 -0500 Subject: [PATCH 03/30] docs: Update vim mode documentation with mode indicator, text objects, and missing operators --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7743ba23b6..22fb4fa8e16 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Vim Mode transforms the Flow Launcher search bar into a modal editor, offering * You can instantly toggle this feature on or off via the `General` tab in the Flow Launcher Settings. ### Modes & Intercepts +- **Mode Indicator**: A subtle, low-opacity indicator sits at the bottom-left of the search bar to clearly show your current mode (`-- NORMAL --`, `-- VISUAL --`, etc.) without interfering with plugins. - **Insert Mode**: The default state. Works exactly like standard Flow Launcher (blinking text caret). - **Normal Mode**: Replaces the blinking caret with a solid block caret, and intercepts alphanumeric keystrokes to execute text manipulation commands. - **Visual Mode**: Char-wise selection. Movements extend the selection from a fixed anchor point; operators apply to the selected region. @@ -31,9 +32,9 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo - `$` : Snap to the end of the query #### Word Boundaries -- `w` : Jump to the start of the next word boundary -- `e` : Jump to the end of the current word boundary -- `b` : Jump back to the start of the previous word boundary +- `w` / `W` (Shift) : Jump to the start of the next word / BIG word boundary +- `e` / `E` (Shift) : Jump to the end of the current word / BIG word boundary +- `b` / `B` (Shift) : Jump back to the start of the previous word / BIG word boundary #### Character Lookup - `f{char}` : Find the next occurrence of `{char}` forward @@ -50,12 +51,19 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo - `S` (Shift+S) : Substitute line (Clears query and enters Insert mode) - `r{char}` : Replace the character under the cursor with `{char}` - `~` : Toggle the casing of the character under the cursor +- `gu` / `gU` : Make lowercase / uppercase (operator pending, e.g., `guw` to lower a word) - `dd` / `cc` : Delete or Change the entire query - `D` / `C` : Delete or Change from the cursor to the end of the line - `Y` (Shift+Y) : Yank (Copy) the entire query - `p` : Paste from system clipboard - `u` : Undo the last edit (hooks into WPF native undo stack) +#### Text Objects (For Operators & Visual Mode) +Use these immediately after an operator (like `d`, `c`, `y`, `gu`) to target specific text structures. +- **Modifiers**: `i` (inner), `a` (around) +- **Targets**: `w` (word), `"` (double quotes), `'` (single quotes), `(` (parentheses), `[` (brackets), `{` (braces) +- *Example*: `diw` (delete inner word), `ci"` (change inside quotes), `ya(` (yank around parentheses) + #### Mode Switching - `i` : Enter Insert mode at the cursor - `I` (Shift+I) : Enter Insert mode at the beginning of the query @@ -72,7 +80,7 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo #### Extending the Selection (Visual mode only) - `h` / `l` : Extend selection left / right -- `w` / `b` / `e` : Extend by word boundary +- `w` / `b` / `e` (and `W`/`B`/`E`) : Extend by word boundary - `0` / `^` / `$` : Extend to beginning / first non-blank / end of query - `f{char}` / `F{char}` / `t{char}` / `T{char}` : Extend to character lookup - `;` / `,` : Repeat last character lookup @@ -83,6 +91,7 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo - `c` / `s` : Change the selection (delete and enter Insert mode) - `r{char}` : Replace every character in the selection with `{char}` - `~` : Toggle casing of every character in the selection +- `gu` / `gU` : Make every character in the selection lowercase / uppercase #### Mode Transitions - `Esc` : Return to Normal mode (clears selection) From 1d355d69519f6cc8fb65a6f6f6a7aad8ab24a18a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 21:44:04 -0500 Subject: [PATCH 04/30] Cleanup codebase and finalize cute vim mode indicator pill --- Flow.Launcher.Test/VimMotionEngineTest.cs | 6 +- Flow.Launcher/MainWindow.xaml | 29 +++++---- Flow.Launcher/VimMode/VimManager.cs | 79 ++++++++++++----------- Flow.Launcher/VimMode/VimMotionEngine.cs | 2 +- 4 files changed, 62 insertions(+), 54 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index 18a72c044e8..68d1d80d49b 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -11,9 +11,9 @@ public class VimMotionEngineTest [Test] public void MoveLeftTest() { - Assert.That(VimMotionEngine.MoveLeft(5, Sample.Length), Is.EqualTo(4)); - Assert.That(VimMotionEngine.MoveLeft(0, Sample.Length), Is.EqualTo(0)); - Assert.That(VimMotionEngine.MoveLeft(2, Sample.Length), Is.EqualTo(1)); + Assert.That(VimMotionEngine.MoveLeft(5), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MoveLeft(0), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveLeft(2), Is.EqualTo(1)); } [Test] diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 604caa7cbb1..633d374b3bd 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -361,19 +361,26 @@ Height="24" IsHitTestVisible="False" Panel.ZIndex="10" /> - + IsHitTestVisible="False" + Visibility="{Binding Visibility, ElementName=VimModeText}"> + +
diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 1fdae0bf9fe..3650ded2828 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -40,16 +40,15 @@ public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryT _vimEngine = new VimEngine(); _vimEngine.ModeChanged += VimEngine_ModeChanged; + _queryTextBox.PreviewTextInput += QueryTextBox_PreviewTextInput; + _queryTextBox.SelectionChanged += QueryTextBox_SelectionChanged; + _queryTextBox.TextChanged += QueryTextBox_TextChanged; _mainWindow.Loaded += MainWindow_Loaded; _viewModel.PropertyChanged += ViewModel_PropertyChanged; } private void MainWindow_Loaded(object sender, RoutedEventArgs e) { - _queryTextBox.PreviewTextInput += QueryTextBox_PreviewTextInput; - _queryTextBox.SelectionChanged += QueryTextBox_SelectionChanged; - _queryTextBox.TextChanged += QueryTextBox_TextChanged; - // Initial state UpdateIndicatorAsync(_vimEngine.CurrentMode); } @@ -82,10 +81,7 @@ private void UpdateCaretPosition() _vimBlockCaret.Height = rect.Height; } } - catch (Exception) - { - // Ignore layout exceptions - } + catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Layout exception in UpdateCaretPosition", ex); } } } @@ -130,22 +126,26 @@ private void ApplyModeUI(VimModes mode) string label = mode switch { - VimModes.Normal => "-- NORMAL --", - VimModes.Visual => "-- VISUAL --", - VimModes.VisualLine => "-- VISUAL LINE --", + VimModes.Normal => "NORMAL", + VimModes.Visual => "VISUAL", + VimModes.VisualLine => "V-LINE", _ => null }; if (_vimModeText != null) { - if (label != null) - { - _vimModeText.Text = label; - _vimModeText.Visibility = Visibility.Visible; - } - else + _vimModeText.Visibility = _settings.EnableVimMode && mode != VimModes.Insert ? Visibility.Visible : Visibility.Collapsed; + _vimModeText.Text = label ?? ""; + + if (_vimModeText.Parent is Border border) { - _vimModeText.Visibility = Visibility.Collapsed; + border.Background = mode switch + { + VimModes.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 215)), + VimModes.Visual => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(153, 50, 204)), + VimModes.VisualLine => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 140, 0)), + _ => System.Windows.Media.Brushes.Transparent + }; } } @@ -188,7 +188,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) if (modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.R && _vimEngine.CurrentMode == VimModes.Normal) { - try { _queryTextBox.Redo(); } catch { } + try { _queryTextBox.Redo(); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } e.Handled = true; return true; } @@ -317,7 +317,7 @@ private bool HandleVimKey(KeyEventArgs e) _viewModel.SelectPrevItemCommand.Execute(null); return true; case Key.H: - ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i, _queryTextBox.Text.Length))); + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i))); return true; case Key.L: ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveRight(i, _queryTextBox.Text.Length))); @@ -394,7 +394,7 @@ private bool HandleVimKey(KeyEventArgs e) if (_queryTextBox.CaretIndex > 0) { int c = _queryTextBox.CaretIndex - 1; - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); _queryTextBox.CaretIndex = c; } @@ -406,7 +406,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; int len = Math.Min(n, _queryTextBox.Text.Length - c); - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); _queryTextBox.CaretIndex = c; } @@ -425,7 +425,7 @@ private bool HandleVimKey(KeyEventArgs e) if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) { int c = _queryTextBox.CaretIndex; - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); _queryTextBox.CaretIndex = c; } @@ -460,7 +460,7 @@ private bool HandleVimKey(KeyEventArgs e) _queryTextBox.CaretIndex = c; } } - catch { } + catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } return true; case Key.U: _queryTextBox.Undo(); @@ -481,7 +481,7 @@ private bool HandleVimKey(KeyEventArgs e) { if (!string.IsNullOrEmpty(_queryTextBox.Text)) { - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } } if (cmd == "d" || cmd == "c") { @@ -571,7 +571,7 @@ private bool HandleVimKey(KeyEventArgs e) } return true; case Key.H: - ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i, _queryTextBox.Text.Length))); + ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i))); return true; case Key.L: ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveRight(i, _queryTextBox.Text.Length))); @@ -648,7 +648,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); _queryTextBox.CaretIndex = selStart; } @@ -661,7 +661,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } } _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; @@ -675,7 +675,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); _queryTextBox.CaretIndex = selStart; } @@ -717,20 +717,20 @@ private bool HandleVimKey(KeyEventArgs e) return true; case Key.X: case Key.D: - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = ""; _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToNormal(); return true; case Key.Y: - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.CaretIndex = 0; _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); return true; case Key.C: case Key.S: - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = ""; _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); @@ -877,7 +877,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) int len = range.end - range.start + 1; if (len > 0) { - try { Clipboard.SetText(text.Substring(range.start, len)); } catch { } + try { Clipboard.SetText(text.Substring(range.start, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } if (_pendingCommand != "y") { _queryTextBox.Text = text.Remove(range.start, len); @@ -915,12 +915,12 @@ private void RepeatLastChange() switch (_lastChange) { case "dd": - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = ""; _queryTextBox.CaretIndex = 0; break; case "cc": - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = ""; _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); @@ -931,7 +931,7 @@ private void RepeatLastChange() int c = _queryTextBox.CaretIndex; if (c < _queryTextBox.Text.Length) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(c)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(c)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(c); _queryTextBox.CaretIndex = c; } @@ -939,7 +939,7 @@ private void RepeatLastChange() } break; case "Y_eol": - try { Clipboard.SetText(_queryTextBox.Text); } catch { } + try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } break; case "x": { @@ -948,7 +948,7 @@ private void RepeatLastChange() int len = Math.Min(n, _queryTextBox.Text.Length - c); if (len > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); _queryTextBox.CaretIndex = c; } @@ -1035,7 +1035,7 @@ private void ExecuteMotion(int targetCaret) if (end > start) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(start, end - start)); } catch { } + try { Clipboard.SetText(_queryTextBox.Text.Substring(start, end - start)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } _queryTextBox.Text = _queryTextBox.Text.Remove(start, end - start); _queryTextBox.CaretIndex = start; } @@ -1237,3 +1237,4 @@ protected virtual void Dispose(bool disposing) } } } + diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index f6feae2c6e8..9671efad6e8 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -4,7 +4,7 @@ namespace Flow.Launcher.VimMode { public class VimMotionEngine { - public static int MoveLeft(int caret, int length) + public static int MoveLeft(int caret) { return Math.Max(0, caret - 1); } From 6afdd52f07d5f4de4fea3d96759901b0ac2f4f07 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 21:57:16 -0500 Subject: [PATCH 05/30] Fix PR review feedback: handle MoveEndWord bounds, TextObjectWord whitespace, TextObjectQuote, shift+R, and count digits. Update README docs. --- Flow.Launcher.Test/VimMotionEngineTest.cs | 2 +- Flow.Launcher/VimMode/VimManager.cs | 10 ++- Flow.Launcher/VimMode/VimMotionEngine.cs | 81 ++++++++++++++++------- README.md | 4 +- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index 68d1d80d49b..5d0c6ddabff 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -68,7 +68,7 @@ public void MoveEndWordTest() { Assert.That(VimMotionEngine.MoveEndWord(Sample, 0), Is.EqualTo(4)); Assert.That(VimMotionEngine.MoveEndWord(Sample, 5), Is.EqualTo(14)); - Assert.That(VimMotionEngine.MoveEndWord(Sample, Sample.Length - 1), Is.EqualTo(Sample.Length)); + Assert.That(VimMotionEngine.MoveEndWord(Sample, Sample.Length - 1), Is.EqualTo(Sample.Length - 1)); } [Test] diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 3650ded2828..ab598c32ae8 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -267,7 +267,7 @@ private bool HandleVimKey(KeyEventArgs e) return HandleGKey(e, modifiers); } - if (string.IsNullOrEmpty(_awaitingCharCommand) && string.IsNullOrEmpty(_awaitingTextObject) && _pendingCommand == "") + if (string.IsNullOrEmpty(_awaitingCharCommand) && string.IsNullOrEmpty(_awaitingTextObject)) { if (e.Key >= Key.D1 && e.Key <= Key.D9 && !modifiers.HasFlag(ModifierKeys.Shift)) { @@ -370,9 +370,11 @@ private bool HandleVimKey(KeyEventArgs e) return false; case Key.F: case Key.T: - case Key.R: _awaitingCharCommand = modifiers.HasFlag(ModifierKeys.Shift) ? e.Key.ToString() : e.Key.ToString().ToLower(); return true; + case Key.R: + _awaitingCharCommand = "r"; + return true; case Key.OemSemicolon: if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { @@ -617,9 +619,11 @@ private bool HandleVimKey(KeyEventArgs e) return true; case Key.F: case Key.T: - case Key.R: _awaitingCharCommand = modifiers.HasFlag(ModifierKeys.Shift) ? e.Key.ToString() : e.Key.ToString().ToLower(); return true; + case Key.R: + _awaitingCharCommand = "r"; + return true; case Key.OemSemicolon: if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index 9671efad6e8..eae0d37f0ae 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -77,19 +77,21 @@ public static int MovePrevWord(string text, int caret) public static int MoveEndWord(string text, int caret) { - if (caret >= text.Length - 1) return text.Length; + if (caret >= text.Length - 1) return text.Length - 1; int i = caret + 1; - while (i < text.Length - 1 && char.IsWhiteSpace(text[i])) + while (i < text.Length && char.IsWhiteSpace(text[i])) i++; + if (i >= text.Length) return caret; + bool targetIsWord = IsWordChar(text[i]); while (i < text.Length - 1 && IsWordChar(text[i + 1]) == targetIsWord && !char.IsWhiteSpace(text[i + 1])) i++; - return Math.Min(i, text.Length); + return Math.Min(i, text.Length - 1); } public static int MoveNextWordBig(string text, int caret) @@ -124,17 +126,19 @@ public static int MovePrevWordBig(string text, int caret) public static int MoveEndWordBig(string text, int caret) { - if (caret >= text.Length - 1) return text.Length; + if (caret >= text.Length - 1) return text.Length - 1; int i = caret + 1; - while (i < text.Length - 1 && char.IsWhiteSpace(text[i])) + while (i < text.Length && char.IsWhiteSpace(text[i])) i++; + if (i >= text.Length) return caret; + while (i < text.Length - 1 && !char.IsWhiteSpace(text[i + 1])) i++; - return Math.Min(i, text.Length); + return Math.Min(i, text.Length - 1); } public static int FindMatchingBracket(string text, int caret) @@ -184,24 +188,49 @@ public static (int start, int end) TextObjectWord(string text, int caret, bool a if (text.Length == 0) return (0, 0); int start, end; - bool startIsWord = IsWordChar(text[Math.Min(caret, text.Length - 1)]); - int i = Math.Min(caret, text.Length - 1); - while (i > 0 && IsWordChar(text[i - 1]) == startIsWord && !char.IsWhiteSpace(text[i - 1])) + bool isWs = char.IsWhiteSpace(text[i]); + bool startIsWord = IsWordChar(text[i]); + + while (i > 0) + { + if (isWs) + { + if (!char.IsWhiteSpace(text[i - 1])) break; + } + else + { + if (IsWordChar(text[i - 1]) != startIsWord || char.IsWhiteSpace(text[i - 1])) break; + } i--; + } start = i; i = Math.Min(caret, text.Length - 1); - while (i < text.Length - 1 && IsWordChar(text[i + 1]) == startIsWord && !char.IsWhiteSpace(text[i + 1])) + while (i < text.Length - 1) + { + if (isWs) + { + if (!char.IsWhiteSpace(text[i + 1])) break; + } + else + { + if (IsWordChar(text[i + 1]) != startIsWord || char.IsWhiteSpace(text[i + 1])) break; + } i++; + } end = i; - if (around) + if (around && !isWs) { if (end + 1 < text.Length && char.IsWhiteSpace(text[end + 1])) - end++; + { + while (end + 1 < text.Length && char.IsWhiteSpace(text[end + 1])) end++; + } else if (start > 0 && char.IsWhiteSpace(text[start - 1])) - start--; + { + while (start > 0 && char.IsWhiteSpace(text[start - 1])) start--; + } } return (start, end); @@ -237,24 +266,30 @@ public static (int start, int end) TextObjectDelimited(string text, int caret, c public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) { - int first = -1, second = -1; + int first = -1; for (int i = 0; i < text.Length; i++) { if (text[i] == quote) { - if (first == -1) first = i; - else if (second == -1) { second = i; break; } + if (first == -1) + { + first = i; + } + else + { + if (caret >= first && caret <= i) + { + int start = around ? first : first + 1; + int end = around ? i : i - 1; + return (start, end); + } + first = -1; + } } } - if (first == -1 || second == -1) return (-1, -1); - if (caret < first || caret > second) return (-1, -1); - - int start = around ? first : first + 1; - int end = around ? second : second - 1; - - return (start, end); + return (-1, -1); } private static bool IsWordChar(char c) diff --git a/README.md b/README.md index 22fb4fa8e16..b016e03fa5f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a specialized fork of [Flow Launcher](https://github.com/Flow-Launcher/Flow.Launcher), equipped with a fully featured, terminal-grade inline **Vim Mode** directly integrated into the search bar. -### Why does this exist? +## Why does this exist? This fork was originally created to streamline journaling workflows. By using Flow Launcher to quickly input journal entries into a personal journaling system, typing out long strings quickly became tedious to edit. Standard text box navigation isn't fast enough. This fork adds native Vim motions directly to the search bar so that when you make a mistake typing a long entry or command, you can instantly drop into Normal mode, hop across word boundaries, and fix the typo without your hands ever leaving the home row. --- @@ -14,7 +14,7 @@ Vim Mode transforms the Flow Launcher search bar into a modal editor, offering * You can instantly toggle this feature on or off via the `General` tab in the Flow Launcher Settings. ### Modes & Intercepts -- **Mode Indicator**: A subtle, low-opacity indicator sits at the bottom-left of the search bar to clearly show your current mode (`-- NORMAL --`, `-- VISUAL --`, etc.) without interfering with plugins. +- **Mode Indicator**: A slick, color-coded pill indicator sits at the far left of the search bar to clearly show your current mode (`NORMAL`, `VISUAL`, etc.). - **Insert Mode**: The default state. Works exactly like standard Flow Launcher (blinking text caret). - **Normal Mode**: Replaces the blinking caret with a solid block caret, and intercepts alphanumeric keystrokes to execute text manipulation commands. - **Visual Mode**: Char-wise selection. Movements extend the selection from a fixed anchor point; operators apply to the selected region. From 902d9d0f0b7cc149bec24ab35b5c72bd0ebb603f Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 22:08:38 -0500 Subject: [PATCH 06/30] chore: enable CI for vim-mode branch --- .github/workflows/dotnet.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 845b31305d8..d54c21990f7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -9,6 +9,7 @@ on: branches: - dev - master + - vim-mode pull_request: jobs: From 3a6a1e09fadceccd7ac98738c99e439ef5eab64a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 22:36:39 -0500 Subject: [PATCH 07/30] fix(ui): change vim mode indicator from pill text to small colored circle to prevent covering input text --- Flow.Launcher/MainWindow.xaml | 20 ++++++------------- Flow.Launcher/MainWindow.xaml.cs | 4 ++-- Flow.Launcher/VimMode/VimManager.cs | 30 +++++++++++++---------------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 633d374b3bd..dd50add8161 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -362,25 +362,17 @@ IsHitTestVisible="False" Panel.ZIndex="10" /> - - + Visibility="Collapsed" /> diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index eefd293c0bf..6baf0303381 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; @@ -97,7 +97,7 @@ public MainWindow() InitializeComponent(); UpdatePosition(); - _vimManager = new Flow.Launcher.VimMode.VimManager(this, _viewModel, QueryTextBox, VimBlockCaret, VimModeText, _settings); + _vimManager = new Flow.Launcher.VimMode.VimManager(this, _viewModel, QueryTextBox, VimBlockCaret, VimModeIndicator, _settings); SyncSoundEffectsState(); RegisterSoundEffectsEvent(); diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index ab598c32ae8..79b22b1757d 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -14,7 +14,7 @@ public class VimManager : IDisposable private readonly TextBox _queryTextBox; private readonly VimEngine _vimEngine; private readonly System.Windows.Shapes.Rectangle _vimBlockCaret; - private readonly TextBlock _vimModeText; + private readonly Border _vimModeIndicator; private string _pendingCommand = ""; private string _awaitingCharCommand = ""; private string _lastFindCmd = ""; @@ -29,13 +29,13 @@ public class VimManager : IDisposable private string _lastChange = ""; private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; - public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryTextBox, System.Windows.Shapes.Rectangle vimBlockCaret, TextBlock vimModeText, Flow.Launcher.Infrastructure.UserSettings.Settings settings) + public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryTextBox, System.Windows.Shapes.Rectangle vimBlockCaret, Border vimModeIndicator, Flow.Launcher.Infrastructure.UserSettings.Settings settings) { _mainWindow = mainWindow; _viewModel = viewModel; _queryTextBox = queryTextBox; _vimBlockCaret = vimBlockCaret; - _vimModeText = vimModeText; + _vimModeIndicator = vimModeIndicator; _settings = settings; _vimEngine = new VimEngine(); @@ -132,21 +132,17 @@ private void ApplyModeUI(VimModes mode) _ => null }; - if (_vimModeText != null) + if (_vimModeIndicator != null) { - _vimModeText.Visibility = _settings.EnableVimMode && mode != VimModes.Insert ? Visibility.Visible : Visibility.Collapsed; - _vimModeText.Text = label ?? ""; + _vimModeIndicator.Visibility = _settings.EnableVimMode && mode != VimModes.Insert ? Visibility.Visible : Visibility.Collapsed; - if (_vimModeText.Parent is Border border) + _vimModeIndicator.Background = mode switch { - border.Background = mode switch - { - VimModes.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 215)), - VimModes.Visual => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(153, 50, 204)), - VimModes.VisualLine => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 140, 0)), - _ => System.Windows.Media.Brushes.Transparent - }; - } + VimModes.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 215)), + VimModes.Visual => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(153, 50, 204)), + VimModes.VisualLine => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 140, 0)), + _ => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Transparent) + }; } if (_vimBlockCaret == null) return; @@ -172,9 +168,9 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { _vimBlockCaret.Visibility = Visibility.Collapsed; } - if (_vimModeText != null && _vimModeText.Visibility == Visibility.Visible) + if (_vimModeIndicator != null && _vimModeIndicator.Visibility == Visibility.Visible) { - _vimModeText.Visibility = Visibility.Collapsed; + _vimModeIndicator.Visibility = Visibility.Collapsed; } return false; } From 831a1984adabc35bdabecfc42e81edca2abf791f Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 22:39:36 -0500 Subject: [PATCH 08/30] fix(ui): use dynamic resources for vim mode settings text to support localization --- Flow.Launcher/Languages/en.xaml | 2 ++ Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index dae6ee81b15..4df51e85b76 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -83,6 +83,8 @@ Hide Flow Launcher when focus is lost Show taskbar when Flow Launcher is opened Temporarily show the taskbar when Flow Launcher is opened, useful for auto-hidden taskbars. + Enable Advanced Vim Mode + Enables terminal-style inline Vim keybindings (Insert, Normal, Visual, and Visual Line modes, motions, and clipboard operations) directly within the search bar. Do not show new version notifications Search Window Location Remember Last Position diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 6c26d471b67..ec8b5328383 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -84,8 +84,8 @@ + Description="{DynamicResource enableAdvancedVimModeToolTip}" + Header="{DynamicResource enableAdvancedVimMode}"> Date: Thu, 18 Jun 2026 22:44:35 -0500 Subject: [PATCH 09/30] docs: add XML docstrings to Vim mode public API --- Flow.Launcher/VimMode/VimEngine.cs | 24 +++++ Flow.Launcher/VimMode/VimManager.cs | 17 ++++ Flow.Launcher/VimMode/VimMotionEngine.cs | 115 +++++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/Flow.Launcher/VimMode/VimEngine.cs b/Flow.Launcher/VimMode/VimEngine.cs index 354b7b0727e..a582b537619 100644 --- a/Flow.Launcher/VimMode/VimEngine.cs +++ b/Flow.Launcher/VimMode/VimEngine.cs @@ -2,6 +2,9 @@ namespace Flow.Launcher.VimMode { + /// + /// Represents the different states of the Vim engine. + /// public enum VimModes { Insert, @@ -10,12 +13,24 @@ public enum VimModes VisualLine } + /// + /// Core state machine for managing Vim modes and transitions. + /// public class VimEngine { + /// + /// Gets the current Vim mode. + /// public VimModes CurrentMode { get; private set; } = VimModes.Insert; + /// + /// Event fired whenever the Vim mode changes. + /// public event Action ModeChanged; + /// + /// Switches the engine to Insert mode. + /// public void SwitchToInsert() { var oldMode = CurrentMode; @@ -24,6 +39,9 @@ public void SwitchToInsert() ModeChanged?.Invoke(CurrentMode); } + /// + /// Switches the engine to Normal mode. + /// public void SwitchToNormal() { var oldMode = CurrentMode; @@ -32,6 +50,9 @@ public void SwitchToNormal() ModeChanged?.Invoke(CurrentMode); } + /// + /// Switches the engine to Visual mode. + /// public void SwitchToVisual() { var oldMode = CurrentMode; @@ -40,6 +61,9 @@ public void SwitchToVisual() ModeChanged?.Invoke(CurrentMode); } + /// + /// Switches the engine to Visual Line mode. + /// public void SwitchToVisualLine() { var oldMode = CurrentMode; diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 79b22b1757d..f196ccdaf90 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -6,6 +6,9 @@ namespace Flow.Launcher.VimMode { + /// + /// Manages the integration of Vim keybindings and UI overlays into the application window. + /// public class VimManager : IDisposable { private bool _disposed; @@ -29,6 +32,9 @@ public class VimManager : IDisposable private string _lastChange = ""; private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; + /// + /// Initializes a new instance of the VimManager class. + /// public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryTextBox, System.Windows.Shapes.Rectangle vimBlockCaret, Border vimModeIndicator, Flow.Launcher.Infrastructure.UserSettings.Settings settings) { _mainWindow = mainWindow; @@ -160,6 +166,11 @@ private void ApplyModeUI(VimModes mode) } } + /// + /// Intercepts and processes key presses before they reach the main window, applying Vim bindings if enabled. + /// + /// The key event arguments. + /// True if the key was handled by Vim mode, otherwise false. public bool HandlePreviewKeyDown(KeyEventArgs e) { if (!_settings.EnableVimMode) @@ -1210,8 +1221,14 @@ private void QueryTextBox_PreviewTextInput(object sender, TextCompositionEventAr } } + /// + /// Gets a value indicating whether native text input is currently blocked by Vim mode. + /// public bool IsInputBlocked => _vimEngine.CurrentMode != VimModes.Insert; + /// + /// Disposes the Vim manager and detaches from window events. + /// public void Dispose() { Dispose(disposing: true); diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index eae0d37f0ae..9278df7a288 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -2,28 +2,56 @@ namespace Flow.Launcher.VimMode { + /// + /// Provides pure static methods for calculating caret movements and text object selections. + /// public class VimMotionEngine { + /// + /// Calculates the new caret position after moving left one character. + /// + /// The current caret index. + /// The new caret index. public static int MoveLeft(int caret) { return Math.Max(0, caret - 1); } + /// + /// Calculates the new caret position after moving right one character. + /// + /// The current caret index. + /// The total length of the text. + /// The new caret index. public static int MoveRight(int caret, int length) { return Math.Min(length, caret + 1); } + /// + /// Calculates the new caret position at the start of the line. + /// + /// The start index (always 0). public static int MoveStartOfLine() { return 0; } + /// + /// Calculates the new caret position at the end of the line. + /// + /// The total length of the text. + /// The end index. public static int MoveEndOfLine(int length) { return length; } + /// + /// Calculates the index of the first non-blank character in the text. + /// + /// The text to search. + /// The index of the first non-blank character, or 0 if none. public static int MoveFirstNonBlank(string text) { for (int i = 0; i < text.Length; i++) @@ -33,6 +61,11 @@ public static int MoveFirstNonBlank(string text) return 0; } + /// + /// Calculates the index after the last non-blank character in the text. + /// + /// The text to search. + /// The target index. public static int MoveLastNonBlank(string text) { for (int i = text.Length - 1; i >= 0; i--) @@ -42,6 +75,12 @@ public static int MoveLastNonBlank(string text) return 0; } + /// + /// Calculates the index of the start of the next word (w motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MoveNextWord(string text, int caret) { if (caret >= text.Length) return text.Length; @@ -58,6 +97,12 @@ public static int MoveNextWord(string text, int caret) return Math.Min(i, text.Length); } + /// + /// Calculates the index of the start of the previous word (b motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MovePrevWord(string text, int caret) { if (caret <= 0) return 0; @@ -75,6 +120,12 @@ public static int MovePrevWord(string text, int caret) return Math.Max(0, i); } + /// + /// Calculates the index of the end of the current or next word (e motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MoveEndWord(string text, int caret) { if (caret >= text.Length - 1) return text.Length - 1; @@ -94,6 +145,12 @@ public static int MoveEndWord(string text, int caret) return Math.Min(i, text.Length - 1); } + /// + /// Calculates the index of the start of the next big word (W motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MoveNextWordBig(string text, int caret) { if (caret >= text.Length) return text.Length; @@ -109,6 +166,12 @@ public static int MoveNextWordBig(string text, int caret) return Math.Min(i, text.Length); } + /// + /// Calculates the index of the start of the previous big word (B motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MovePrevWordBig(string text, int caret) { if (caret <= 0) return 0; @@ -124,6 +187,12 @@ public static int MovePrevWordBig(string text, int caret) return Math.Max(0, i); } + /// + /// Calculates the index of the end of the current or next big word (E motion). + /// + /// The text to traverse. + /// The current caret index. + /// The new caret index. public static int MoveEndWordBig(string text, int caret) { if (caret >= text.Length - 1) return text.Length - 1; @@ -141,6 +210,12 @@ public static int MoveEndWordBig(string text, int caret) return Math.Min(i, text.Length - 1); } + /// + /// Finds the index of the matching bracket (% motion). + /// + /// The text containing brackets. + /// The current caret index. + /// The index of the matching bracket, or the original caret if none. public static int FindMatchingBracket(string text, int caret) { if (caret >= text.Length) return caret; @@ -183,6 +258,13 @@ public static int FindMatchingBracket(string text, int caret) return caret; } + /// + /// Calculates the selection bounds for a word text object (iw / aw). + /// + /// The text. + /// The current caret index. + /// True for aw, false for iw. + /// A tuple containing the start and end indices of the selection. public static (int start, int end) TextObjectWord(string text, int caret, bool around) { if (text.Length == 0) return (0, 0); @@ -236,6 +318,15 @@ public static (int start, int end) TextObjectWord(string text, int caret, bool a return (start, end); } + /// + /// Calculates the selection bounds for a delimited text object, such as parentheses or brackets. + /// + /// The text. + /// The current caret index. + /// The opening delimiter. + /// The closing delimiter. + /// True to include the delimiters (a object), false to exclude (i object). + /// A tuple containing the start and end indices, or (-1, -1) if invalid. public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) { int depth = 0; @@ -264,6 +355,14 @@ public static (int start, int end) TextObjectDelimited(string text, int caret, c return (start, end); } + /// + /// Calculates the selection bounds for a quote text object. + /// + /// The text. + /// The current caret index. + /// The quote character. + /// True to include the quotes, false to exclude. + /// A tuple containing the start and end indices, or (-1, -1) if invalid. public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) { int first = -1; @@ -297,6 +396,14 @@ private static bool IsWordChar(char c) return char.IsLetterOrDigit(c) || c == '_'; } + /// + /// Finds the next occurrence of a character (f / t motion). + /// + /// The text. + /// The current caret index. + /// The character to find. + /// True for t (stops before), false for f (lands on). + /// The index of the target character, or the original caret if not found. public static int FindCharForward(string text, int caret, char target, bool till = false) { if (caret >= text.Length - 1) return caret; @@ -306,6 +413,14 @@ public static int FindCharForward(string text, int caret, char target, bool till return till ? index - 1 : index; } + /// + /// Finds the previous occurrence of a character (F / T motion). + /// + /// The text. + /// The current caret index. + /// The character to find. + /// True for T (stops after), false for F (lands on). + /// The index of the target character, or the original caret if not found. public static int FindCharBackward(string text, int caret, char target, bool till = false) { if (caret <= 0) return caret; From 2dc28c1359a6ed1275160ea451db4fd30272ddfb Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 23:21:35 -0500 Subject: [PATCH 10/30] refactor: remove unused label variable --- Flow.Launcher/VimMode/VimManager.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index f196ccdaf90..8d7416e8424 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -130,14 +130,6 @@ private void ApplyModeUI(VimModes mode) { InputMethod.SetIsInputMethodSuspended(_queryTextBox, mode != VimModes.Insert); - string label = mode switch - { - VimModes.Normal => "NORMAL", - VimModes.Visual => "VISUAL", - VimModes.VisualLine => "V-LINE", - _ => null - }; - if (_vimModeIndicator != null) { _vimModeIndicator.Visibility = _settings.EnableVimMode && mode != VimModes.Insert ? Visibility.Visible : Visibility.Collapsed; From 1afa4467083c64b532c1189c724c04a1b252d115 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Thu, 18 Jun 2026 23:43:01 -0500 Subject: [PATCH 11/30] fix(ui): preserve WPF data bindings to fix auto-complete crash --- Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/VimMode/VimManager.cs | 48 ++++++++++++++--------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 6baf0303381..c1a9f619d2b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1430,7 +1430,7 @@ private void QueryTextBox_KeyUp(object sender, KeyEventArgs e) if (_viewModel.QueryText != QueryTextBox.Text) { BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); - be.UpdateSource(); + be?.UpdateSource(); } } diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 8d7416e8424..0aae50ecf63 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -396,7 +396,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex - 1; try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } } @@ -408,7 +408,7 @@ private bool HandleVimKey(KeyEventArgs e) int c = _queryTextBox.CaretIndex; int len = Math.Min(n, _queryTextBox.Text.Length - c); try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } _lastChange = "x"; @@ -427,7 +427,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } _vimEngine.SwitchToInsert(); @@ -441,7 +441,7 @@ private bool HandleVimKey(KeyEventArgs e) { char ch = _queryTextBox.Text[c]; ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); - _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString()); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); } _lastChange = "~"; @@ -457,7 +457,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; if (c < _queryTextBox.Text.Length) c++; - _queryTextBox.Text = _queryTextBox.Text.Insert(c, text); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Insert(c, text)); _queryTextBox.CaretIndex = c; } } @@ -486,7 +486,7 @@ private bool HandleVimKey(KeyEventArgs e) } if (cmd == "d" || cmd == "c") { - _queryTextBox.Text = ""; + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; } if (cmd == "c") @@ -652,7 +652,7 @@ private bool HandleVimKey(KeyEventArgs e) if (selLength > 0) { try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } _vimEngine.SwitchToNormal(); @@ -679,7 +679,7 @@ private bool HandleVimKey(KeyEventArgs e) if (selLength > 0) { try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(selStart, selLength); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } _vimEngine.SwitchToInsert(); @@ -695,7 +695,7 @@ private bool HandleVimKey(KeyEventArgs e) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; } @@ -721,7 +721,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.X: case Key.D: try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = ""; + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToNormal(); return true; @@ -734,7 +734,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.C: case Key.S: try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = ""; + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); return true; @@ -749,7 +749,7 @@ private bool HandleVimKey(KeyEventArgs e) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = 0; i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = 0; _queryTextBox.SelectionLength = 0; } @@ -883,7 +883,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) try { Clipboard.SetText(text.Substring(range.start, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } if (_pendingCommand != "y") { - _queryTextBox.Text = text.Remove(range.start, len); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text.Remove(range.start, len)); _queryTextBox.CaretIndex = range.start; } if (_pendingCommand == "c") @@ -919,12 +919,12 @@ private void RepeatLastChange() { case "dd": try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = ""; + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; break; case "cc": try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = ""; + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); break; @@ -935,7 +935,7 @@ private void RepeatLastChange() if (c < _queryTextBox.Text.Length) { try { Clipboard.SetText(_queryTextBox.Text.Substring(c)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(c); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c)); _queryTextBox.CaretIndex = c; } if (_lastChange == "C_eol") _vimEngine.SwitchToInsert(); @@ -952,7 +952,7 @@ private void RepeatLastChange() if (len > 0) { try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(c, len); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } } @@ -964,7 +964,7 @@ private void RepeatLastChange() { char ch = _queryTextBox.Text[c]; ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); - _queryTextBox.Text = _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString()); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); } } @@ -989,7 +989,7 @@ private void ExecuteCharCommand(string cmd, char c) char[] chars = text.ToCharArray(); for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) chars[i] = c; - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); @@ -997,7 +997,7 @@ private void ExecuteCharCommand(string cmd, char c) } else if (caret < text.Length) { - _queryTextBox.Text = text.Remove(caret, 1).Insert(caret, c.ToString()); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text.Remove(caret, 1).Insert(caret, c.ToString())); _queryTextBox.CaretIndex = caret; } } @@ -1039,7 +1039,7 @@ private void ExecuteMotion(int targetCaret) if (end > start) { try { Clipboard.SetText(_queryTextBox.Text.Substring(start, end - start)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } - _queryTextBox.Text = _queryTextBox.Text.Remove(start, end - start); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(start, end - start)); _queryTextBox.CaretIndex = start; } @@ -1060,7 +1060,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = Math.Min(start, _queryTextBox.Text.Length); } _lastChange = "~"; @@ -1078,7 +1078,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.ToLower(chars[i]); - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = start; } _pendingCommand = ""; @@ -1095,7 +1095,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.ToUpper(chars[i]); - _queryTextBox.Text = new string(chars); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); _queryTextBox.CaretIndex = start; } _pendingCommand = ""; From 18a3c815e7dcfac3a73b70806e6bcd4a7d21df3a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:05:35 -0500 Subject: [PATCH 12/30] Refactor VimMode logic, fix crashes, and optimize performance --- Flow.Launcher.Test/VimEngineTest.cs | 40 +++---- Flow.Launcher/VimMode/VimEngine.cs | 22 ++-- Flow.Launcher/VimMode/VimManager.cs | 136 +++++++++++++---------- Flow.Launcher/VimMode/VimMotionEngine.cs | 39 ++++--- 4 files changed, 133 insertions(+), 104 deletions(-) diff --git a/Flow.Launcher.Test/VimEngineTest.cs b/Flow.Launcher.Test/VimEngineTest.cs index de2712f5817..062eed22887 100644 --- a/Flow.Launcher.Test/VimEngineTest.cs +++ b/Flow.Launcher.Test/VimEngineTest.cs @@ -11,7 +11,7 @@ public class VimEngineTest public void DefaultModeIsInsert() { var engine = new VimEngine(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Insert)); } [Test] @@ -19,13 +19,13 @@ public void SwitchToInsertFromNormalFiresEvent() { var engine = new VimEngine(); engine.SwitchToNormal(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToInsert(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); - Assert.That(modes, Is.EqualTo(new[] { VimModes.Insert })); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Insert)); + Assert.That(modes, Is.EqualTo(new[] { VimModeType.Insert })); } [Test] @@ -44,13 +44,13 @@ public void SwitchToInsertWhenAlreadyInsertDoesNotFire() public void SwitchToNormalFiresEvent() { var engine = new VimEngine(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToNormal(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Normal)); - Assert.That(modes, Is.EqualTo(new[] { VimModes.Normal })); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Normal)); + Assert.That(modes, Is.EqualTo(new[] { VimModeType.Normal })); } [Test] @@ -71,13 +71,13 @@ public void SwitchToVisualFiresEvent() { var engine = new VimEngine(); engine.SwitchToNormal(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToVisual(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Visual)); - Assert.That(modes, Is.EqualTo(new[] { VimModes.Visual })); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Visual)); + Assert.That(modes, Is.EqualTo(new[] { VimModeType.Visual })); } [Test] @@ -98,13 +98,13 @@ public void SwitchToVisualLineFiresEvent() { var engine = new VimEngine(); engine.SwitchToVisual(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToVisualLine(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.VisualLine)); - Assert.That(modes, Is.EqualTo(new[] { VimModes.VisualLine })); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.VisualLine)); + Assert.That(modes, Is.EqualTo(new[] { VimModeType.VisualLine })); } [Test] @@ -124,7 +124,7 @@ public void SwitchToVisualLineWhenAlreadyDoesNotFire() public void FullModeCycleTest() { var engine = new VimEngine(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToNormal(); @@ -132,9 +132,9 @@ public void FullModeCycleTest() engine.SwitchToNormal(); engine.SwitchToInsert(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Insert)); Assert.That(modes, Is.EqualTo(new[] { - VimModes.Normal, VimModes.Visual, VimModes.Normal, VimModes.Insert + VimModeType.Normal, VimModeType.Visual, VimModeType.Normal, VimModeType.Insert })); } @@ -142,7 +142,7 @@ public void FullModeCycleTest() public void VisualLineCycleTest() { var engine = new VimEngine(); - var modes = new List(); + var modes = new List(); engine.ModeChanged += m => modes.Add(m); engine.SwitchToNormal(); @@ -152,10 +152,10 @@ public void VisualLineCycleTest() engine.SwitchToNormal(); engine.SwitchToInsert(); - Assert.That(engine.CurrentMode, Is.EqualTo(VimModes.Insert)); + Assert.That(engine.CurrentMode, Is.EqualTo(VimModeType.Insert)); Assert.That(modes, Is.EqualTo(new[] { - VimModes.Normal, VimModes.VisualLine, VimModes.Visual, - VimModes.VisualLine, VimModes.Normal, VimModes.Insert + VimModeType.Normal, VimModeType.VisualLine, VimModeType.Visual, + VimModeType.VisualLine, VimModeType.Normal, VimModeType.Insert })); } diff --git a/Flow.Launcher/VimMode/VimEngine.cs b/Flow.Launcher/VimMode/VimEngine.cs index a582b537619..d30df6f2aed 100644 --- a/Flow.Launcher/VimMode/VimEngine.cs +++ b/Flow.Launcher/VimMode/VimEngine.cs @@ -5,7 +5,7 @@ namespace Flow.Launcher.VimMode /// /// Represents the different states of the Vim engine. /// - public enum VimModes + public enum VimModeType { Insert, Normal, @@ -21,12 +21,12 @@ public class VimEngine /// /// Gets the current Vim mode. /// - public VimModes CurrentMode { get; private set; } = VimModes.Insert; + public VimModeType CurrentMode { get; private set; } = VimModeType.Insert; /// /// Event fired whenever the Vim mode changes. /// - public event Action ModeChanged; + public event Action ModeChanged; /// /// Switches the engine to Insert mode. @@ -34,8 +34,8 @@ public class VimEngine public void SwitchToInsert() { var oldMode = CurrentMode; - CurrentMode = VimModes.Insert; - if (oldMode != VimModes.Insert) + CurrentMode = VimModeType.Insert; + if (oldMode != VimModeType.Insert) ModeChanged?.Invoke(CurrentMode); } @@ -45,8 +45,8 @@ public void SwitchToInsert() public void SwitchToNormal() { var oldMode = CurrentMode; - CurrentMode = VimModes.Normal; - if (oldMode != VimModes.Normal) + CurrentMode = VimModeType.Normal; + if (oldMode != VimModeType.Normal) ModeChanged?.Invoke(CurrentMode); } @@ -56,8 +56,8 @@ public void SwitchToNormal() public void SwitchToVisual() { var oldMode = CurrentMode; - CurrentMode = VimModes.Visual; - if (oldMode != VimModes.Visual) + CurrentMode = VimModeType.Visual; + if (oldMode != VimModeType.Visual) ModeChanged?.Invoke(CurrentMode); } @@ -67,8 +67,8 @@ public void SwitchToVisual() public void SwitchToVisualLine() { var oldMode = CurrentMode; - CurrentMode = VimModes.VisualLine; - if (oldMode != VimModes.VisualLine) + CurrentMode = VimModeType.VisualLine; + if (oldMode != VimModeType.VisualLine) ModeChanged?.Invoke(CurrentMode); } } diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 0aae50ecf63..36ab8917d06 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -15,6 +15,21 @@ public class VimManager : IDisposable private readonly MainWindow _mainWindow; private readonly MainViewModel _viewModel; private readonly TextBox _queryTextBox; + + private static void SetClipboardText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try { Clipboard.SetText(text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard operation failed", ex); } + } + } + + private static System.Windows.Media.SolidColorBrush CreateBrush(byte r, byte g, byte b) + { + var brush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(r, g, b)); + brush.Freeze(); + return brush; + } private readonly VimEngine _vimEngine; private readonly System.Windows.Shapes.Rectangle _vimBlockCaret; private readonly Border _vimModeIndicator; @@ -71,11 +86,11 @@ private void QueryTextBox_TextChanged(object sender, TextChangedEventArgs e) private void UpdateCaretPosition() { - if (_vimEngine.CurrentMode != VimModes.Insert && _vimBlockCaret != null) + if (_vimEngine.CurrentMode != VimModeType.Insert && _vimBlockCaret != null) { try { - int index = (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + int index = (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) ? _visualCaret : _queryTextBox.CaretIndex; var rect = _queryTextBox.GetRectFromCharacterIndex(index); @@ -99,7 +114,7 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop { _mainWindow.Dispatcher.BeginInvoke(new Action(() => { - if (_vimEngine.CurrentMode != VimModes.Insert) + if (_vimEngine.CurrentMode != VimModeType.Insert) { _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToInsert(); @@ -109,12 +124,12 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop } } - private void VimEngine_ModeChanged(VimModes mode) + private void VimEngine_ModeChanged(VimModeType mode) { UpdateIndicatorAsync(mode); } - private void UpdateIndicatorAsync(VimModes mode) + private void UpdateIndicatorAsync(VimModeType mode) { if (_mainWindow.Dispatcher.CheckAccess()) { @@ -126,26 +141,26 @@ private void UpdateIndicatorAsync(VimModes mode) } } - private void ApplyModeUI(VimModes mode) + private void ApplyModeUI(VimModeType mode) { - InputMethod.SetIsInputMethodSuspended(_queryTextBox, mode != VimModes.Insert); + InputMethod.SetIsInputMethodSuspended(_queryTextBox, mode != VimModeType.Insert); if (_vimModeIndicator != null) { - _vimModeIndicator.Visibility = _settings.EnableVimMode && mode != VimModes.Insert ? Visibility.Visible : Visibility.Collapsed; + _vimModeIndicator.Visibility = _settings.EnableVimMode && mode != VimModeType.Insert ? Visibility.Visible : Visibility.Collapsed; _vimModeIndicator.Background = mode switch { - VimModes.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 215)), - VimModes.Visual => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(153, 50, 204)), - VimModes.VisualLine => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 140, 0)), + VimModeType.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? CreateBrush(0, 120, 215), + VimModeType.Visual => CreateBrush(153, 50, 204), + VimModeType.VisualLine => CreateBrush(255, 140, 0), _ => new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Transparent) }; } if (_vimBlockCaret == null) return; - if (mode == VimModes.Insert) + if (mode == VimModeType.Insert) { _vimBlockCaret.Visibility = Visibility.Collapsed; _queryTextBox.ClearValue(System.Windows.Controls.TextBox.CaretBrushProperty); @@ -178,16 +193,16 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) return false; } - if (_vimBlockCaret != null && _vimBlockCaret.Visibility == Visibility.Collapsed && _vimEngine.CurrentMode != VimModes.Insert) + if (_vimBlockCaret != null && _vimBlockCaret.Visibility == Visibility.Collapsed && _vimEngine.CurrentMode != VimModeType.Insert) { _vimBlockCaret.Visibility = Visibility.Visible; } var modifiers = e.KeyboardDevice.Modifiers; - if (modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.R && _vimEngine.CurrentMode == VimModes.Normal) + if (modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.R && _vimEngine.CurrentMode == VimModeType.Normal) { - try { _queryTextBox.Redo(); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + try { _queryTextBox.Redo(); } catch { } e.Handled = true; return true; } @@ -204,7 +219,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) return true; } - if (_vimEngine.CurrentMode == VimModes.Insert) + if (_vimEngine.CurrentMode == VimModeType.Insert) { if (e.Key == Key.Escape) { @@ -216,7 +231,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) } else { - if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + if (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) { if (e.Key == Key.Escape) { @@ -258,7 +273,7 @@ private bool HandleVimKey(KeyEventArgs e) { var modifiers = e.KeyboardDevice.Modifiers; - if (_vimEngine.CurrentMode == VimModes.Normal || _vimEngine.CurrentMode == VimModes.Visual) + if (_vimEngine.CurrentMode == VimModeType.Normal || _vimEngine.CurrentMode == VimModeType.Visual) { if (_gPending) { @@ -286,7 +301,7 @@ private bool HandleVimKey(KeyEventArgs e) } } - if (_vimEngine.CurrentMode == VimModes.Normal || _vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + if (_vimEngine.CurrentMode == VimModeType.Normal || _vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) { if (!string.IsNullOrEmpty(_awaitingCharCommand)) { @@ -306,7 +321,7 @@ private bool HandleVimKey(KeyEventArgs e) switch (_vimEngine.CurrentMode) { - case VimModes.Normal: + case VimModeType.Normal: switch (e.Key) { case Key.J: @@ -340,7 +355,7 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWordBig(_queryTextBox.Text, i))); return true; case Key.D5 when modifiers.HasFlag(ModifierKeys.Shift): - ExecuteMotion(VimMotionEngine.FindMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex)); + ExecuteMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex)); return true; case Key.G: if (modifiers.HasFlag(ModifierKeys.Shift)) @@ -395,7 +410,7 @@ private bool HandleVimKey(KeyEventArgs e) if (_queryTextBox.CaretIndex > 0) { int c = _queryTextBox.CaretIndex - 1; - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(c, 1)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } @@ -407,7 +422,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; int len = Math.Min(n, _queryTextBox.Text.Length - c); - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(c, len)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } @@ -426,7 +441,7 @@ private bool HandleVimKey(KeyEventArgs e) if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) { int c = _queryTextBox.CaretIndex; - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, 1)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(c, 1)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } @@ -482,7 +497,7 @@ private bool HandleVimKey(KeyEventArgs e) { if (!string.IsNullOrEmpty(_queryTextBox.Text)) { - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); } if (cmd == "d" || cmd == "c") { @@ -546,7 +561,7 @@ private bool HandleVimKey(KeyEventArgs e) return false; } - case VimModes.Visual: + case VimModeType.Visual: switch (e.Key) { case Key.V when modifiers.HasFlag(ModifierKeys.Shift): @@ -600,7 +615,7 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteVisualMotion(VimMotionEngine.MoveStartOfLine()); return true; case Key.D5 when modifiers.HasFlag(ModifierKeys.Shift): - ExecuteVisualMotion(VimMotionEngine.FindMatchingBracket(_queryTextBox.Text, _visualCaret)); + ExecuteVisualMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _visualCaret)); return true; case Key.D6: if (modifiers.HasFlag(ModifierKeys.Shift)) @@ -651,7 +666,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } @@ -664,7 +679,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); } _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; @@ -678,7 +693,7 @@ private bool HandleVimKey(KeyEventArgs e) int selLength = _queryTextBox.SelectionLength; if (selLength > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(selStart, selLength)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } @@ -712,7 +727,7 @@ private bool HandleVimKey(KeyEventArgs e) return true; } - case VimModes.VisualLine: + case VimModeType.VisualLine: switch (e.Key) { case Key.V when modifiers == ModifierKeys.None: @@ -720,20 +735,20 @@ private bool HandleVimKey(KeyEventArgs e) return true; case Key.X: case Key.D: - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToNormal(); return true; case Key.Y: - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); _queryTextBox.CaretIndex = 0; _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); return true; case Key.C: case Key.S: - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); @@ -775,7 +790,7 @@ private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) { switch (_vimEngine.CurrentMode) { - case VimModes.Normal: + case VimModeType.Normal: switch (e.Key) { case Key.OemMinus when modifiers.HasFlag(ModifierKeys.Shift): @@ -805,7 +820,7 @@ private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) default: return true; } - case VimModes.Visual: + case VimModeType.Visual: switch (e.Key) { case Key.V: @@ -834,7 +849,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) if (e.Key == Key.W) delim = 'w'; string text = _queryTextBox.Text; - int caret = (_vimEngine.CurrentMode == VimModes.Visual) ? _visualCaret : _queryTextBox.CaretIndex; + int caret = (_vimEngine.CurrentMode == VimModeType.Visual) ? _visualCaret : _queryTextBox.CaretIndex; bool around = prefix == "a"; (int start, int end) range = (0, 0); @@ -867,7 +882,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) if (range.start < 0) return true; - if (_vimEngine.CurrentMode == VimModes.Visual) + if (_vimEngine.CurrentMode == VimModeType.Visual) { _visualAnchor = range.start; _visualCaret = range.end; @@ -880,7 +895,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) int len = range.end - range.start + 1; if (len > 0) { - try { Clipboard.SetText(text.Substring(range.start, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(text.Substring(range.start, len)); if (_pendingCommand != "y") { _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text.Remove(range.start, len)); @@ -898,7 +913,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) private int ApplyCountMove(Func move) { - int target = (_vimEngine.CurrentMode == VimModes.Visual) ? _visualCaret : _queryTextBox.CaretIndex; + int target = (_vimEngine.CurrentMode == VimModeType.Visual) ? _visualCaret : _queryTextBox.CaretIndex; int n = _count > 0 ? _count : 1; for (int i = 0; i < n; i++) target = move(target); @@ -918,12 +933,12 @@ private void RepeatLastChange() switch (_lastChange) { case "dd": - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; break; case "cc": - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); @@ -934,7 +949,7 @@ private void RepeatLastChange() int c = _queryTextBox.CaretIndex; if (c < _queryTextBox.Text.Length) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(c)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(c)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c)); _queryTextBox.CaretIndex = c; } @@ -942,7 +957,7 @@ private void RepeatLastChange() } break; case "Y_eol": - try { Clipboard.SetText(_queryTextBox.Text); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text); break; case "x": { @@ -951,7 +966,7 @@ private void RepeatLastChange() int len = Math.Min(n, _queryTextBox.Text.Length - c); if (len > 0) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(c, len)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(c, len)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } @@ -980,7 +995,7 @@ private void ExecuteCharCommand(string cmd, char c) if (cmd == "r") { - if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + if (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) { int selStart = _queryTextBox.SelectionStart; int selLength = _queryTextBox.SelectionLength; @@ -1012,7 +1027,7 @@ private void ExecuteCharCommand(string cmd, char c) private void ExecuteFindCommand(string cmd, char c) { string text = _queryTextBox.Text; - int caret = (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + int caret = (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) ? _visualCaret : _queryTextBox.CaretIndex; int target = caret; @@ -1022,7 +1037,7 @@ private void ExecuteFindCommand(string cmd, char c) else if (cmd == "t") target = VimMotionEngine.FindCharForward(text, caret, c, true); else if (cmd == "T") target = VimMotionEngine.FindCharBackward(text, caret, c, true); - if (_vimEngine.CurrentMode == VimModes.Visual || _vimEngine.CurrentMode == VimModes.VisualLine) + if (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) ExecuteVisualMotion(target); else ExecuteMotion(target); @@ -1030,15 +1045,15 @@ private void ExecuteFindCommand(string cmd, char c) private void ExecuteMotion(int targetCaret) { - if (_pendingCommand == "d" || _pendingCommand == "c") + if (_pendingCommand == "d" || _pendingCommand == "c" || _pendingCommand == "y") { - int start = _queryTextBox.CaretIndex; - int end = targetCaret; + int start = Math.Max(0, _queryTextBox.CaretIndex); + int end = Math.Max(0, targetCaret); if (start > end) { var temp = start; start = end; end = temp; } if (end > start) { - try { Clipboard.SetText(_queryTextBox.Text.Substring(start, end - start)); } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + SetClipboardText(_queryTextBox.Text.Substring(start, end - start)); _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(start, end - start)); _queryTextBox.CaretIndex = start; } @@ -1050,8 +1065,8 @@ private void ExecuteMotion(int targetCaret) } else if (_pendingCommand == "~") { - int start = _queryTextBox.CaretIndex; - int end = targetCaret; + int start = Math.Max(0, _queryTextBox.CaretIndex); + int end = Math.Max(0, targetCaret); if (start > end) { var temp = start; start = end; end = temp; } if (end < _queryTextBox.Text.Length) end++; @@ -1068,8 +1083,8 @@ private void ExecuteMotion(int targetCaret) } else if (_pendingCommand == "gu") { - int start = _queryTextBox.CaretIndex; - int end = targetCaret; + int start = Math.Max(0, _queryTextBox.CaretIndex); + int end = Math.Max(0, targetCaret); if (start > end) { var temp = start; start = end; end = temp; } if (end < _queryTextBox.Text.Length) end++; @@ -1085,8 +1100,8 @@ private void ExecuteMotion(int targetCaret) } else if (_pendingCommand == "gU") { - int start = _queryTextBox.CaretIndex; - int end = targetCaret; + int start = Math.Max(0, _queryTextBox.CaretIndex); + int end = Math.Max(0, targetCaret); if (start > end) { var temp = start; start = end; end = temp; } if (end < _queryTextBox.Text.Length) end++; @@ -1110,6 +1125,7 @@ private void UpdateVisualSelection() { int start = Math.Min(_visualAnchor, _visualCaret); int end = Math.Max(_visualAnchor, _visualCaret); + if (start < 0) start = 0; int length = Math.Min(end - start + 1, _queryTextBox.Text.Length - start); if (length < 0) length = 0; _queryTextBox.Select(start, length); @@ -1207,7 +1223,7 @@ private static char GetCharFromKey(Key key, ModifierKeys modifiers) private void QueryTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { - if (_vimEngine.CurrentMode != VimModes.Insert) + if (_vimEngine.CurrentMode != VimModeType.Insert) { e.Handled = true; } @@ -1216,7 +1232,7 @@ private void QueryTextBox_PreviewTextInput(object sender, TextCompositionEventAr /// /// Gets a value indicating whether native text input is currently blocked by Vim mode. /// - public bool IsInputBlocked => _vimEngine.CurrentMode != VimModes.Insert; + public bool IsInputBlocked => _vimEngine.CurrentMode != VimModeType.Insert; /// /// Disposes the Vim manager and detaches from window events. diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index 9278df7a288..9cbdf51520e 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -216,7 +216,7 @@ public static int MoveEndWordBig(string text, int caret) /// The text containing brackets. /// The current caret index. /// The index of the matching bracket, or the original caret if none. - public static int FindMatchingBracket(string text, int caret) + public static int MoveToMatchingBracket(string text, int caret) { if (caret >= text.Length) return caret; char c = text[caret]; @@ -327,31 +327,41 @@ public static (int start, int end) TextObjectWord(string text, int caret, bool a /// The closing delimiter. /// True to include the delimiters (a object), false to exclude (i object). /// A tuple containing the start and end indices, or (-1, -1) if invalid. - public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) + public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) { - int depth = 0; int openPos = -1; - int closePos = -1; - - for (int i = 0; i < text.Length; i++) + int depth = 0; + for (int i = Math.Min(caret, text.Length - 1); i >= 0; i--) { - if (text[i] == open) + if (text[i] == close) depth++; + else if (text[i] == open) { - if (i <= caret) { openPos = i; depth = 1; closePos = -1; } - else if (depth > 0) depth++; + if (depth == 0) { openPos = i; break; } + depth--; } - else if (text[i] == close && depth > 0) + } + + if (openPos == -1) return (-1, -1); + + int closePos = -1; + depth = 0; + for (int i = openPos + 1; i < text.Length; i++) + { + if (text[i] == open) depth++; + else if (text[i] == close) { - depth--; if (depth == 0) { closePos = i; break; } + depth--; } } - if (openPos == -1 || closePos == -1) return (-1, -1); + if (closePos == -1) return (-1, -1); int start = around ? openPos : openPos + 1; int end = around ? closePos : closePos - 1; + if (start > end) return (start, start - 1); + return (start, end); } @@ -363,7 +373,7 @@ public static (int start, int end) TextObjectDelimited(string text, int caret, c /// The quote character. /// True to include the quotes, false to exclude. /// A tuple containing the start and end indices, or (-1, -1) if invalid. - public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) + public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) { int first = -1; @@ -381,6 +391,7 @@ public static (int start, int end) TextObjectQuote(string text, int caret, char { int start = around ? first : first + 1; int end = around ? i : i - 1; + if (start > end) return (start, start - 1); return (start, end); } first = -1; @@ -391,6 +402,8 @@ public static (int start, int end) TextObjectQuote(string text, int caret, char return (-1, -1); } + + private static bool IsWordChar(char c) { return char.IsLetterOrDigit(c) || c == '_'; From 4a2bcfb58a27a250c2c89748e8339b668871248f Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:19:18 -0500 Subject: [PATCH 13/30] Update packages.lock.json after build --- Flow.Launcher/packages.lock.json | 843 +++++++++++++++++++++++++++++++ 1 file changed, 843 insertions(+) diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index 3208907704c..15a9598f18c 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -1653,6 +1653,849 @@ "JetBrains.Annotations": "[2025.2.2, )" } } + }, + "net9.0-windows10.0.19041/win-x64": { + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.Microsoft.Win32.Primitives": "4.3.0" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "23g6rqftKmovn2cLeGsuHUYm0FD7pdutb0uQMJpZ3qTvq+zHkgmt6J65VtRry4WDGYlmkMa4xDACtaQ94alNag==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "runtime.any.System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "S/GPBmfPBB48ZghLxdDR7kDAJVAqgAuThyDJho3OLP5OS4tWD2ydyL8LKm8lhiBxce10OKe9X2zZ6DUjAqEbPg==" + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1lpifymjGDzoYIaam6/Hyqf8GhBI3xXYLK2TgEvTtuZMorG3Kb9QnMTIKhLjJYXIiu1JvxjngHvtVFQQlpQ3HQ==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sMDBnad4rp4t7GY442Jux0MCUuKL4otn5BK6Ni0ARTXTSpRNBzZ7hpMfKSvnVSED5kYJm96YOWsqV0JH0d2uuw==" + }, + "runtime.any.System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "M1r+760j1CNA6M/ZaW6KX8gOS8nxPRqloqDcJYVidRG566Ykwcs29AweZs2JF+nMOCgWDiMfPSTMfvwOI9F77w==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ==" + }, + "runtime.any.System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cPhT+Vqu52+cQQrDai/V91gubXUnDKNRvlBnH+hOgtGyHdC17aQIU64EaehwAQymd7kJA5rSrVRNfDYrbhnzyA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Lxb89SMvf8w9p9+keBLyL6H6x/TEmc6QVsIIA0T36IuyOY3kNvIdyGddA2qt35cRamzxF8K5p0Opq4G4HjNbhQ==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==", + "dependencies": { + "System.Private.Uri": "4.3.0" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GG84X6vufoEzqx8PbeBKheE4srOhimv+yLtGb/JkR3Y2FmoqmueLNFU4Xx8Y67plFpltQSdK74x0qlEhIpv/CQ==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lBoFeQfxe/4eqjPi46E0LU/YaCMdNkQ8B4MZu/mkzdIAZh8RQ1NYZSj0egrQKdgdvlPFtP4STtob40r4o2DBAw==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ==" + }, + "runtime.any.System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NLrxmLsfRrOuVqPWG+2lrQZnE53MLVeo+w9c54EV+TUo4c8rILpsDXfY8pPiOy9kHpUHHP07ugKmtsU3vVW5Jg==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w==" + }, + "runtime.any.System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "w4ehZJ+AwXYmGwYu+rMvym6RvMaRiUEQR1u6dwcyuKHxz8Heu/mO9AG1MquEgTyucnhv3M43X0iKpDOoN17C0w==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.CoreCompat.System.Drawing": { + "type": "Transitive", + "resolved": "5.8.64", + "contentHash": "Ey7xQgWwixxdrmhzEUvaR4kxZDSQMWQScp8ViLvmL5xCBKG6U3TaMv/jzHilpfQXpHmJ4IylKGzzMvnYX2FwHQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "runtime.win.Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NU51SEt/ZaD2MF48sJ17BIqx7rjeNNLXUevfMOjqQIetdndXwYjZfZsT6jD+rSWp/FYxjesdK4xUSl4OTEI0jw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "runtime.win.System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RRACWygml5dnmfgC1SW6tLGsFgwsUAKFtvhdyHnIEz4EhWyrd7pacDdY95CacQJy7BMXRDRCejC9aCRC0Y1sQA==", + "dependencies": { + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "runtime.win.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "hHHP0WCStene2jjeYcuDkETozUYF/3sHVRHAEOgS3L15hlip24ssqCTnJC28Z03Wpo078oMcJd0H4egD2aJI8g==" + }, + "runtime.win.System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z37zcSCpXuGCYtFbqYO0TwOVXxS2d+BXgSoDFZmRg8BC4Cuy54edjyIvhhcfCrDQA9nl+EPFTgHN54dRAK7mNA==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Overlapped": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "runtime.win.System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lkXXykakvXUU+Zq2j0pC6EO20lEhijjqMc01XXpp1CJN+DeCwl3nsj4t5Xbpz3kA7yQyTqw6d9SyIzsyLsV3zA==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "runtime.win.System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FK/2gX6MmuLIKNCGsV59Fe4IYrLrI5n9pQ1jh477wiivEM/NCXDT2dRetH5FSfY0bQ+VgTLcS3zcmjQ8my3nxQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Net.NameResolution": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Principal.Windows": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Overlapped": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "runtime.win.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RkgHVhUPvzZxuUubiZe8yr/6CypRVXj0VBzaR8hsqQ8f+rUo7e4PWrHTLOCjd8fBMGWCrY//fi7Ku3qXD7oHRw==", + "dependencies": { + "System.Private.Uri": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Collections": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.win.System.Console": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.System.Diagnostics.Debug": "4.3.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.9", + "contentHash": "wpsUfnyv8E5K4WQaok6weewvAbQhcLwXFcHBm5U0gdEaBs85N//ssuYvRPFWwz2rO/9/DFP3A1sGMzUFBj8y3w==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Diagnostics.Tools": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Diagnostics.Tracing": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "7.0.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Globalization": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Globalization.Calendars": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.any.System.IO": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.win.System.IO.FileSystem": "4.3.0" + } + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.NameResolution": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Principal.Windows": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "runtime.win.System.Net.Primitives": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.win.System.Net.Sockets": "4.3.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection.Extensions": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection.Primitives": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Resources.ResourceManager": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "runtime.any.System.Runtime": "4.3.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "runtime.any.System.Runtime.InteropServices": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.any.System.Text.Encoding.Extensions": "4.3.0" + } + }, + "System.Threading.Overlapped": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m3HQ2dPiX/DSTpf+yJt8B0c+SRvzfqAJKx+QDWi+VLhz8svLT23MVjEOHPF/KiSLeArKU/iHescrbLd3yVgyNg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Threading.Timer": "4.3.0" + } + } } } } \ No newline at end of file From bb3baab81c9887d06644045fbc74afdc9a45c017 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:39:57 -0500 Subject: [PATCH 14/30] Fix yank motion deletion bug --- Flow.Launcher/VimMode/VimManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 36ab8917d06..dab7e154365 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -1054,8 +1054,11 @@ private void ExecuteMotion(int targetCaret) if (end > start) { SetClipboardText(_queryTextBox.Text.Substring(start, end - start)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(start, end - start)); - _queryTextBox.CaretIndex = start; + if (_pendingCommand != "y") + { + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(start, end - start)); + _queryTextBox.CaretIndex = start; + } } if (_pendingCommand == "c") From 38e62de18e7b15e97bc6e908c51dda30acb13f86 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:43:47 -0500 Subject: [PATCH 15/30] Decrease VimMode indicator size --- Flow.Launcher/MainWindow.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index dd50add8161..3e53a1ac0a8 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -366,9 +366,9 @@ HorizontalAlignment="Left" VerticalAlignment="Center" Margin="6,0,0,0" - Width="8" - Height="8" - CornerRadius="4" + Width="6" + Height="6" + CornerRadius="3" Background="{DynamicResource BasicSystemAccentColor}" Panel.ZIndex="10" IsHitTestVisible="False" From 3c4ef29f1f690ef0072fa935af2b9760abe88b4a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:51:01 -0500 Subject: [PATCH 16/30] Adjust VimModeIndicator vertical margin --- Flow.Launcher/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 3e53a1ac0a8..dcfde657a69 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -365,7 +365,7 @@ x:Name="VimModeIndicator" HorizontalAlignment="Left" VerticalAlignment="Center" - Margin="6,0,0,0" + Margin="6,2,0,0" Width="6" Height="6" CornerRadius="3" From dacfdab97ea71b3b67c6297979506e5f926c3f70 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 00:52:04 -0500 Subject: [PATCH 17/30] Update lockfile --- Flow.Launcher/packages.lock.json | 843 ------------------------------- 1 file changed, 843 deletions(-) diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index 15a9598f18c..3208907704c 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -1653,849 +1653,6 @@ "JetBrains.Annotations": "[2025.2.2, )" } } - }, - "net9.0-windows10.0.19041/win-x64": { - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.win.Microsoft.Win32.Primitives": "4.3.0" - } - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" - }, - "runtime.any.System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "23g6rqftKmovn2cLeGsuHUYm0FD7pdutb0uQMJpZ3qTvq+zHkgmt6J65VtRry4WDGYlmkMa4xDACtaQ94alNag==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "runtime.any.System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "S/GPBmfPBB48ZghLxdDR7kDAJVAqgAuThyDJho3OLP5OS4tWD2ydyL8LKm8lhiBxce10OKe9X2zZ6DUjAqEbPg==" - }, - "runtime.any.System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1lpifymjGDzoYIaam6/Hyqf8GhBI3xXYLK2TgEvTtuZMorG3Kb9QnMTIKhLjJYXIiu1JvxjngHvtVFQQlpQ3HQ==" - }, - "runtime.any.System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sMDBnad4rp4t7GY442Jux0MCUuKL4otn5BK6Ni0ARTXTSpRNBzZ7hpMfKSvnVSED5kYJm96YOWsqV0JH0d2uuw==" - }, - "runtime.any.System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "M1r+760j1CNA6M/ZaW6KX8gOS8nxPRqloqDcJYVidRG566Ykwcs29AweZs2JF+nMOCgWDiMfPSTMfvwOI9F77w==" - }, - "runtime.any.System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ==" - }, - "runtime.any.System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ==" - }, - "runtime.any.System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cPhT+Vqu52+cQQrDai/V91gubXUnDKNRvlBnH+hOgtGyHdC17aQIU64EaehwAQymd7kJA5rSrVRNfDYrbhnzyA==" - }, - "runtime.any.System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg==" - }, - "runtime.any.System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Lxb89SMvf8w9p9+keBLyL6H6x/TEmc6QVsIIA0T36IuyOY3kNvIdyGddA2qt35cRamzxF8K5p0Opq4G4HjNbhQ==" - }, - "runtime.any.System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==", - "dependencies": { - "System.Private.Uri": "4.3.0" - } - }, - "runtime.any.System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GG84X6vufoEzqx8PbeBKheE4srOhimv+yLtGb/JkR3Y2FmoqmueLNFU4Xx8Y67plFpltQSdK74x0qlEhIpv/CQ==" - }, - "runtime.any.System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lBoFeQfxe/4eqjPi46E0LU/YaCMdNkQ8B4MZu/mkzdIAZh8RQ1NYZSj0egrQKdgdvlPFtP4STtob40r4o2DBAw==" - }, - "runtime.any.System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ==" - }, - "runtime.any.System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NLrxmLsfRrOuVqPWG+2lrQZnE53MLVeo+w9c54EV+TUo4c8rILpsDXfY8pPiOy9kHpUHHP07ugKmtsU3vVW5Jg==" - }, - "runtime.any.System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w==" - }, - "runtime.any.System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "w4ehZJ+AwXYmGwYu+rMvym6RvMaRiUEQR1u6dwcyuKHxz8Heu/mO9AG1MquEgTyucnhv3M43X0iKpDOoN17C0w==" - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" - }, - "runtime.osx.10.10-x64.CoreCompat.System.Drawing": { - "type": "Transitive", - "resolved": "5.8.64", - "contentHash": "Ey7xQgWwixxdrmhzEUvaR4kxZDSQMWQScp8ViLvmL5xCBKG6U3TaMv/jzHilpfQXpHmJ4IylKGzzMvnYX2FwHQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" - }, - "runtime.win.Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NU51SEt/ZaD2MF48sJ17BIqx7rjeNNLXUevfMOjqQIetdndXwYjZfZsT6jD+rSWp/FYxjesdK4xUSl4OTEI0jw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "runtime.win.System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RRACWygml5dnmfgC1SW6tLGsFgwsUAKFtvhdyHnIEz4EhWyrd7pacDdY95CacQJy7BMXRDRCejC9aCRC0Y1sQA==", - "dependencies": { - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "runtime.win.System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "hHHP0WCStene2jjeYcuDkETozUYF/3sHVRHAEOgS3L15hlip24ssqCTnJC28Z03Wpo078oMcJd0H4egD2aJI8g==" - }, - "runtime.win.System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z37zcSCpXuGCYtFbqYO0TwOVXxS2d+BXgSoDFZmRg8BC4Cuy54edjyIvhhcfCrDQA9nl+EPFTgHN54dRAK7mNA==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Overlapped": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "runtime.win.System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lkXXykakvXUU+Zq2j0pC6EO20lEhijjqMc01XXpp1CJN+DeCwl3nsj4t5Xbpz3kA7yQyTqw6d9SyIzsyLsV3zA==", - "dependencies": { - "Microsoft.Win32.Primitives": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "runtime.win.System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FK/2gX6MmuLIKNCGsV59Fe4IYrLrI5n9pQ1jh477wiivEM/NCXDT2dRetH5FSfY0bQ+VgTLcS3zcmjQ8my3nxQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Net.NameResolution": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Principal.Windows": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Overlapped": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "runtime.win.System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RkgHVhUPvzZxuUubiZe8yr/6CypRVXj0VBzaR8hsqQ8f+rUo7e4PWrHTLOCjd8fBMGWCrY//fi7Ku3qXD7oHRw==", - "dependencies": { - "System.Private.Uri": "4.3.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Collections": "4.3.0" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.win.System.Console": "4.3.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.win.System.Diagnostics.Debug": "4.3.0" - } - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "9.0.9", - "contentHash": "wpsUfnyv8E5K4WQaok6weewvAbQhcLwXFcHBm5U0gdEaBs85N//ssuYvRPFWwz2rO/9/DFP3A1sGMzUFBj8y3w==" - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Diagnostics.Tools": "4.3.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Diagnostics.Tracing": "4.3.0" - } - }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Globalization": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Globalization.Calendars": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.any.System.IO": "4.3.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.win.System.IO.FileSystem": "4.3.0" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Net.NameResolution": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Principal.Windows": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "runtime.win.System.Net.Primitives": "4.3.0" - } - }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.win.System.Net.Sockets": "4.3.0" - } - }, - "System.Private.Uri": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Reflection": "4.3.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Reflection.Extensions": "4.3.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Reflection.Primitives": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Resources.ResourceManager": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "runtime.any.System.Runtime": "4.3.0" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.win.System.Runtime.Extensions": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "runtime.any.System.Runtime.InteropServices": "4.3.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Text.Encoding": "4.3.0" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.any.System.Text.Encoding.Extensions": "4.3.0" - } - }, - "System.Threading.Overlapped": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m3HQ2dPiX/DSTpf+yJt8B0c+SRvzfqAJKx+QDWi+VLhz8svLT23MVjEOHPF/KiSLeArKU/iHescrbLd3yVgyNg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "runtime.any.System.Threading.Timer": "4.3.0" - } - } } } } \ No newline at end of file From dddeb6429ada7fc36db825241eec8e631ffd8d58 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 01:29:35 -0500 Subject: [PATCH 18/30] Fix V key behavior: V from Normal enters Visual Line mode, toggle logic for all modes --- Flow.Launcher/VimMode/VimManager.cs | 33 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index dab7e154365..5114a8baad1 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -543,11 +543,11 @@ private bool HandleVimKey(KeyEventArgs e) _vimEngine.SwitchToInsert(); _queryTextBox.CaretIndex = _queryTextBox.Text.Length; return true; - case Key.V when modifiers == ModifierKeys.None: - EnterVisualMode(); - return true; - case Key.V when modifiers.HasFlag(ModifierKeys.Shift): - EnterVisualLineMode(); + case Key.V: + if (modifiers.HasFlag(ModifierKeys.Shift)) + EnterVisualLineMode(); + else + EnterVisualMode(); return true; case Key.OemPeriod: if (!string.IsNullOrEmpty(_lastChange)) @@ -564,8 +564,14 @@ private bool HandleVimKey(KeyEventArgs e) case VimModeType.Visual: switch (e.Key) { - case Key.V when modifiers.HasFlag(ModifierKeys.Shift): - EnterVisualLineMode(); + case Key.V: + if (modifiers.HasFlag(ModifierKeys.Shift)) + EnterVisualLineMode(); + else + { + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + } return true; case Key.O when modifiers == ModifierKeys.None: SwapVisualEnds(); @@ -730,8 +736,17 @@ private bool HandleVimKey(KeyEventArgs e) case VimModeType.VisualLine: switch (e.Key) { - case Key.V when modifiers == ModifierKeys.None: - EnterVisualMode(); + case Key.V: + if (modifiers.HasFlag(ModifierKeys.Shift)) + { + _queryTextBox.SelectionLength = 0; + _vimEngine.SwitchToNormal(); + } + else + { + _vimEngine.SwitchToVisual(); + UpdateVisualSelection(); + } return true; case Key.X: case Key.D: From b35626f51b9db15e064e0c626cdd5f9733a3d8bf Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 01:49:39 -0500 Subject: [PATCH 19/30] Implement operation-level undo/redo stack, document u and Ctrl+R --- Flow.Launcher/VimMode/VimManager.cs | 93 +++++++++++++++++++++-------- README.md | 3 +- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 5114a8baad1..b412658bab4 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -47,6 +47,47 @@ private static System.Windows.Media.SolidColorBrush CreateBrush(byte r, byte g, private string _lastChange = ""; private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; + // Vim-style operation-level undo/redo stacks + private readonly System.Collections.Generic.Stack<(string text, int caretIndex)> _undoStack = new(); + private readonly System.Collections.Generic.Stack<(string text, int caretIndex)> _redoStack = new(); + + /// Snapshots the current text+caret state onto the undo stack and clears the redo stack. + private void PushUndo() + { + _undoStack.Push((_queryTextBox.Text, _queryTextBox.CaretIndex)); + _redoStack.Clear(); + } + + private void VimUndo() + { + if (_undoStack.Count == 0) return; + _redoStack.Push((_queryTextBox.Text, _queryTextBox.CaretIndex)); + var (text, caret) = _undoStack.Pop(); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text); + _queryTextBox.CaretIndex = Math.Min(caret, text.Length); + } + + private void VimRedo() + { + if (_redoStack.Count == 0) return; + _undoStack.Push((_queryTextBox.Text, _queryTextBox.CaretIndex)); + var (text, caret) = _redoStack.Pop(); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text); + _queryTextBox.CaretIndex = Math.Min(caret, text.Length); + } + + /// + /// Applies a text mutation: snapshots the current state onto the undo stack, + /// then sets the TextBox text. All text-mutating Vim commands must use this + /// instead of calling SetCurrentValue directly. + /// + private void SetText(string newText) + { + PushUndo(); + _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, newText); + } + + /// /// Initializes a new instance of the VimManager class. /// @@ -202,7 +243,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) if (modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.R && _vimEngine.CurrentMode == VimModeType.Normal) { - try { _queryTextBox.Redo(); } catch { } + VimRedo(); e.Handled = true; return true; } @@ -411,7 +452,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex - 1; SetClipboardText(_queryTextBox.Text.Substring(c, 1)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); + SetText(_queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } } @@ -423,7 +464,7 @@ private bool HandleVimKey(KeyEventArgs e) int c = _queryTextBox.CaretIndex; int len = Math.Min(n, _queryTextBox.Text.Length - c); SetClipboardText(_queryTextBox.Text.Substring(c, len)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); + SetText(_queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } _lastChange = "x"; @@ -442,7 +483,7 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; SetClipboardText(_queryTextBox.Text.Substring(c, 1)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1)); + SetText(_queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } _vimEngine.SwitchToInsert(); @@ -456,7 +497,7 @@ private bool HandleVimKey(KeyEventArgs e) { char ch = _queryTextBox.Text[c]; ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); + SetText(_queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); } _lastChange = "~"; @@ -472,14 +513,14 @@ private bool HandleVimKey(KeyEventArgs e) { int c = _queryTextBox.CaretIndex; if (c < _queryTextBox.Text.Length) c++; - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Insert(c, text)); + SetText(_queryTextBox.Text.Insert(c, text)); _queryTextBox.CaretIndex = c; } } catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } return true; case Key.U: - _queryTextBox.Undo(); + VimUndo(); return true; case Key.D: case Key.C: @@ -501,7 +542,7 @@ private bool HandleVimKey(KeyEventArgs e) } if (cmd == "d" || cmd == "c") { - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); + SetText(""); _queryTextBox.CaretIndex = 0; } if (cmd == "c") @@ -673,7 +714,7 @@ private bool HandleVimKey(KeyEventArgs e) if (selLength > 0) { SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); + SetText(_queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } _vimEngine.SwitchToNormal(); @@ -700,7 +741,7 @@ private bool HandleVimKey(KeyEventArgs e) if (selLength > 0) { SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(selStart, selLength)); + SetText(_queryTextBox.Text.Remove(selStart, selLength)); _queryTextBox.CaretIndex = selStart; } _vimEngine.SwitchToInsert(); @@ -716,7 +757,7 @@ private bool HandleVimKey(KeyEventArgs e) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; } @@ -751,7 +792,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.X: case Key.D: SetClipboardText(_queryTextBox.Text); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); + SetText(""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToNormal(); return true; @@ -764,7 +805,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.C: case Key.S: SetClipboardText(_queryTextBox.Text); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); + SetText(""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); return true; @@ -779,7 +820,7 @@ private bool HandleVimKey(KeyEventArgs e) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = 0; i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = 0; _queryTextBox.SelectionLength = 0; } @@ -913,7 +954,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) SetClipboardText(text.Substring(range.start, len)); if (_pendingCommand != "y") { - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text.Remove(range.start, len)); + SetText(text.Remove(range.start, len)); _queryTextBox.CaretIndex = range.start; } if (_pendingCommand == "c") @@ -949,12 +990,12 @@ private void RepeatLastChange() { case "dd": SetClipboardText(_queryTextBox.Text); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); + SetText(""); _queryTextBox.CaretIndex = 0; break; case "cc": SetClipboardText(_queryTextBox.Text); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, ""); + SetText(""); _queryTextBox.CaretIndex = 0; _vimEngine.SwitchToInsert(); break; @@ -965,7 +1006,7 @@ private void RepeatLastChange() if (c < _queryTextBox.Text.Length) { SetClipboardText(_queryTextBox.Text.Substring(c)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c)); + SetText(_queryTextBox.Text.Remove(c)); _queryTextBox.CaretIndex = c; } if (_lastChange == "C_eol") _vimEngine.SwitchToInsert(); @@ -982,7 +1023,7 @@ private void RepeatLastChange() if (len > 0) { SetClipboardText(_queryTextBox.Text.Substring(c, len)); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, len)); + SetText(_queryTextBox.Text.Remove(c, len)); _queryTextBox.CaretIndex = c; } } @@ -994,7 +1035,7 @@ private void RepeatLastChange() { char ch = _queryTextBox.Text[c]; ch = char.IsUpper(ch) ? char.ToLower(ch) : char.ToUpper(ch); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); + SetText(_queryTextBox.Text.Remove(c, 1).Insert(c, ch.ToString())); _queryTextBox.CaretIndex = Math.Min(_queryTextBox.Text.Length, c + 1); } } @@ -1019,7 +1060,7 @@ private void ExecuteCharCommand(string cmd, char c) char[] chars = text.ToCharArray(); for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) chars[i] = c; - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); @@ -1027,7 +1068,7 @@ private void ExecuteCharCommand(string cmd, char c) } else if (caret < text.Length) { - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, text.Remove(caret, 1).Insert(caret, c.ToString())); + SetText(text.Remove(caret, 1).Insert(caret, c.ToString())); _queryTextBox.CaretIndex = caret; } } @@ -1071,7 +1112,7 @@ private void ExecuteMotion(int targetCaret) SetClipboardText(_queryTextBox.Text.Substring(start, end - start)); if (_pendingCommand != "y") { - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, _queryTextBox.Text.Remove(start, end - start)); + SetText(_queryTextBox.Text.Remove(start, end - start)); _queryTextBox.CaretIndex = start; } } @@ -1093,7 +1134,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = Math.Min(start, _queryTextBox.Text.Length); } _lastChange = "~"; @@ -1111,7 +1152,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.ToLower(chars[i]); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = start; } _pendingCommand = ""; @@ -1128,7 +1169,7 @@ private void ExecuteMotion(int targetCaret) char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) chars[i] = char.ToUpper(chars[i]); - _queryTextBox.SetCurrentValue(System.Windows.Controls.TextBox.TextProperty, new string(chars)); + SetText(new string(chars)); _queryTextBox.CaretIndex = start; } _pendingCommand = ""; diff --git a/README.md b/README.md index b016e03fa5f..0d12f24cdf1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo - `D` / `C` : Delete or Change from the cursor to the end of the line - `Y` (Shift+Y) : Yank (Copy) the entire query - `p` : Paste from system clipboard -- `u` : Undo the last edit (hooks into WPF native undo stack) +- `u` : Undo the last Vim operation (operation-level, not character-level) +- `Ctrl+R` : Redo the last undone operation #### Text Objects (For Operators & Visual Mode) Use these immediately after an operator (like `d`, `c`, `y`, `gu`) to target specific text structures. From df55bea82e70f85c709f55762585d091a9e855a3 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 02:04:55 -0500 Subject: [PATCH 20/30] Fix repeat (.) for dw/cw/s/X/r and all motion-based operations --- Flow.Launcher/VimMode/VimManager.cs | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index b412658bab4..e855f98bb6a 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -45,6 +45,8 @@ private static System.Windows.Media.SolidColorBrush CreateBrush(byte r, byte g, private string _awaitingTextObject = ""; private (int anchor, int caret)? _lastVisualRange; private string _lastChange = ""; + private int _lastChangeLen = 0; // chars affected by last change (for . repeat) + private char _lastReplaceChar = '\0'; // char used in last r{char} (for . repeat) private readonly Flow.Launcher.Infrastructure.UserSettings.Settings _settings; // Vim-style operation-level undo/redo stacks @@ -455,6 +457,8 @@ private bool HandleVimKey(KeyEventArgs e) SetText(_queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } + _lastChange = "X"; + _lastChangeLen = 1; } else // x { @@ -468,6 +472,7 @@ private bool HandleVimKey(KeyEventArgs e) _queryTextBox.CaretIndex = c; } _lastChange = "x"; + _lastChangeLen = n; } return true; case Key.S: @@ -486,6 +491,8 @@ private bool HandleVimKey(KeyEventArgs e) SetText(_queryTextBox.Text.Remove(c, 1)); _queryTextBox.CaretIndex = c; } + _lastChange = "s"; + _lastChangeLen = 1; _vimEngine.SwitchToInsert(); } return true; @@ -1040,6 +1047,56 @@ private void RepeatLastChange() } } break; + case "d_motion": + { + int c = _queryTextBox.CaretIndex; + int len = Math.Min(_lastChangeLen, _queryTextBox.Text.Length - c); + if (len > 0) + { + SetClipboardText(_queryTextBox.Text.Substring(c, len)); + SetText(_queryTextBox.Text.Remove(c, len)); + _queryTextBox.CaretIndex = c; + } + } + break; + case "c_motion": + { + int c = _queryTextBox.CaretIndex; + int len = Math.Min(_lastChangeLen, _queryTextBox.Text.Length - c); + if (len > 0) + { + SetClipboardText(_queryTextBox.Text.Substring(c, len)); + SetText(_queryTextBox.Text.Remove(c, len)); + _queryTextBox.CaretIndex = c; + } + _vimEngine.SwitchToInsert(); + } + break; + case "s": + case "X": + { + int c = _queryTextBox.CaretIndex; + if (_lastChange == "X") c = Math.Max(0, c - 1); + int len = Math.Min(_lastChangeLen, _queryTextBox.Text.Length - c); + if (len > 0) + { + SetClipboardText(_queryTextBox.Text.Substring(c, len)); + SetText(_queryTextBox.Text.Remove(c, len)); + _queryTextBox.CaretIndex = c; + } + if (_lastChange == "s") _vimEngine.SwitchToInsert(); + } + break; + case "r": + { + int c = _queryTextBox.CaretIndex; + if (_lastReplaceChar != '\0' && c < _queryTextBox.Text.Length) + { + SetText(_queryTextBox.Text.Remove(c, 1).Insert(c, _lastReplaceChar.ToString())); + _queryTextBox.CaretIndex = c; + } + } + break; } } @@ -1070,6 +1127,8 @@ private void ExecuteCharCommand(string cmd, char c) { SetText(text.Remove(caret, 1).Insert(caret, c.ToString())); _queryTextBox.CaretIndex = caret; + _lastChange = "r"; + _lastReplaceChar = c; } } else if (cmd == "f" || cmd == "F" || cmd == "t" || cmd == "T") @@ -1120,6 +1179,7 @@ private void ExecuteMotion(int targetCaret) if (_pendingCommand == "c") _vimEngine.SwitchToInsert(); _lastChange = _pendingCommand + "_motion"; + _lastChangeLen = end - start; _pendingCommand = ""; } else if (_pendingCommand == "~") From f1498cae5690cdb862e84279edaff00a97864f6b Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Fri, 19 Jun 2026 12:40:13 -0500 Subject: [PATCH 21/30] Address cubic review: fix state leaks, restore caret, refactor engine --- Flow.Launcher/VimMode/VimEngine.cs | 38 +++++++++-------------------- Flow.Launcher/VimMode/VimManager.cs | 7 ++++++ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/VimMode/VimEngine.cs b/Flow.Launcher/VimMode/VimEngine.cs index d30df6f2aed..80fde6274ad 100644 --- a/Flow.Launcher/VimMode/VimEngine.cs +++ b/Flow.Launcher/VimMode/VimEngine.cs @@ -28,48 +28,32 @@ public class VimEngine /// public event Action ModeChanged; - /// - /// Switches the engine to Insert mode. - /// - public void SwitchToInsert() + private void SetMode(VimModeType newMode) { var oldMode = CurrentMode; - CurrentMode = VimModeType.Insert; - if (oldMode != VimModeType.Insert) + CurrentMode = newMode; + if (oldMode != newMode) ModeChanged?.Invoke(CurrentMode); } + /// + /// Switches the engine to Insert mode. + /// + public void SwitchToInsert() => SetMode(VimModeType.Insert); + /// /// Switches the engine to Normal mode. /// - public void SwitchToNormal() - { - var oldMode = CurrentMode; - CurrentMode = VimModeType.Normal; - if (oldMode != VimModeType.Normal) - ModeChanged?.Invoke(CurrentMode); - } + public void SwitchToNormal() => SetMode(VimModeType.Normal); /// /// Switches the engine to Visual mode. /// - public void SwitchToVisual() - { - var oldMode = CurrentMode; - CurrentMode = VimModeType.Visual; - if (oldMode != VimModeType.Visual) - ModeChanged?.Invoke(CurrentMode); - } + public void SwitchToVisual() => SetMode(VimModeType.Visual); /// /// Switches the engine to Visual Line mode. /// - public void SwitchToVisualLine() - { - var oldMode = CurrentMode; - CurrentMode = VimModeType.VisualLine; - if (oldMode != VimModeType.VisualLine) - ModeChanged?.Invoke(CurrentMode); - } + public void SwitchToVisualLine() => SetMode(VimModeType.VisualLine); } } diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index e855f98bb6a..19361f61b95 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -233,6 +233,8 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { _vimModeIndicator.Visibility = Visibility.Collapsed; } + _queryTextBox.ClearValue(System.Windows.Controls.TextBox.CaretBrushProperty); + InputMethod.SetIsInputMethodSuspended(_queryTextBox, false); return false; } @@ -280,6 +282,8 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { SaveVisualRange(); _queryTextBox.SelectionLength = 0; + _pendingCommand = ""; + _awaitingCharCommand = ""; _vimEngine.SwitchToNormal(); e.Handled = true; return true; @@ -297,6 +301,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { _lastEscapeTime = DateTime.Now; _pendingCommand = ""; + _awaitingCharCommand = ""; e.Handled = true; return true; } @@ -1264,6 +1269,7 @@ private void EnterVisualMode() _queryTextBox.CaretIndex = _queryTextBox.Text.Length - 1; _visualAnchor = _queryTextBox.CaretIndex; _visualCaret = _queryTextBox.CaretIndex; + _pendingCommand = ""; _vimEngine.SwitchToVisual(); UpdateVisualSelection(); } @@ -1273,6 +1279,7 @@ private void EnterVisualLineMode() if (_queryTextBox.Text.Length == 0) return; _visualAnchor = 0; _visualCaret = _queryTextBox.Text.Length - 1; + _pendingCommand = ""; _vimEngine.SwitchToVisualLine(); _queryTextBox.Select(0, _queryTextBox.Text.Length); UpdateCaretPosition(); From 1246cd27bbcb3ab1e429994e56731727e4d5b45a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 01:23:49 -0500 Subject: [PATCH 22/30] Fix Vim mode bugs: disable-lock, g-prefix, visual text objects, state leaks - Disabling Vim mode while in a non-Insert mode no longer blocks typing: PreviewTextInput now respects EnableVimMode, and toggling the setting off forces a return to Insert mode and clears the overlays. - The multi-key 'g' prefix (gu/gU/g~/gv/g_) is now triggered by lowercase 'g' instead of Shift+G, matching the documented bindings. - Visual-mode text objects (vi(, va", viw, ...) are reachable again; the unreachable insert-at-boundary cases that shadowed them were removed. - Wire up gu/gU in Visual mode (previously documented but not implemented). - Reset transient command state (count, pending operator, g-prefix, awaited text object) on Escape and on mode entry so commands like 3dd don't leak count. - 'p' now honors counts, leaves the caret on the last pasted character, and is repeatable with '.'. - Guard MoveEndWord/MoveEndWordBig against empty text (no longer return -1). - Add unit tests for BIG-word motions, bracket matching, and all text objects. - Reconcile README: dot indicator (not pill), document '.' and counts, and add a Known Limitations section. --- Flow.Launcher.Test/VimMotionEngineTest.cs | 83 ++++++++++++++ Flow.Launcher/VimMode/VimManager.cs | 132 ++++++++++++++++------ Flow.Launcher/VimMode/VimMotionEngine.cs | 2 + README.md | 11 +- 4 files changed, 191 insertions(+), 37 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index 5d0c6ddabff..d769cbeb014 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -90,5 +90,88 @@ public void FindCharBackwardTest() Assert.That(VimMotionEngine.FindCharBackward(Sample, 0, 'o', false), Is.EqualTo(0)); Assert.That(VimMotionEngine.FindCharBackward(Sample, 4, 'o', false), Is.EqualTo(4)); } + + [Test] + public void MoveEndWordEmptyOrEndDoesNotReturnNegative() + { + Assert.That(VimMotionEngine.MoveEndWord("", 0), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveEndWordBig("", 0), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveEndWord("a", 0), Is.EqualTo(0)); + } + + [Test] + public void MoveLastNonBlankTest() + { + Assert.That(VimMotionEngine.MoveLastNonBlank("hello "), Is.EqualTo(5)); + Assert.That(VimMotionEngine.MoveLastNonBlank("hello"), Is.EqualTo(5)); + Assert.That(VimMotionEngine.MoveLastNonBlank(" "), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MoveLastNonBlank(""), Is.EqualTo(0)); + } + + [Test] + public void MoveNextWordBigTest() + { + Assert.That(VimMotionEngine.MoveNextWordBig("foo bar baz", 0), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MoveNextWordBig("foo bar baz", 4), Is.EqualTo(8)); + // Punctuation is part of a BIG word (unlike w). + Assert.That(VimMotionEngine.MoveNextWordBig("foo.bar baz", 0), Is.EqualTo(8)); + Assert.That(VimMotionEngine.MoveNextWordBig("foo", 3), Is.EqualTo(3)); + } + + [Test] + public void MovePrevWordBigTest() + { + Assert.That(VimMotionEngine.MovePrevWordBig("foo bar baz", 8), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MovePrevWordBig("foo bar", 4), Is.EqualTo(0)); + Assert.That(VimMotionEngine.MovePrevWordBig("foo", 0), Is.EqualTo(0)); + } + + [Test] + public void MoveEndWordBigTest() + { + Assert.That(VimMotionEngine.MoveEndWordBig("foo bar", 0), Is.EqualTo(2)); + Assert.That(VimMotionEngine.MoveEndWordBig("foo bar", 2), Is.EqualTo(6)); + } + + [Test] + public void MoveToMatchingBracketTest() + { + Assert.That(VimMotionEngine.MoveToMatchingBracket("a(bc)d", 1), Is.EqualTo(4)); + Assert.That(VimMotionEngine.MoveToMatchingBracket("a(bc)d", 4), Is.EqualTo(1)); + Assert.That(VimMotionEngine.MoveToMatchingBracket("(a(b)c)", 0), Is.EqualTo(6)); + // No bracket under the caret: stay put. + Assert.That(VimMotionEngine.MoveToMatchingBracket("abc", 1), Is.EqualTo(1)); + } + + [Test] + public void TextObjectWordTest() + { + Assert.That(VimMotionEngine.TextObjectWord("foo bar", 1, false), Is.EqualTo((0, 2))); + // aw includes the trailing whitespace. + Assert.That(VimMotionEngine.TextObjectWord("foo bar", 1, true), Is.EqualTo((0, 3))); + Assert.That(VimMotionEngine.TextObjectWord("foo bar", 5, false), Is.EqualTo((4, 6))); + // No trailing whitespace at end of string -> aw extends over leading whitespace instead. + Assert.That(VimMotionEngine.TextObjectWord("foo bar", 5, true), Is.EqualTo((3, 6))); + Assert.That(VimMotionEngine.TextObjectWord("", 0, false), Is.EqualTo((0, 0))); + } + + [Test] + public void TextObjectQuoteTest() + { + Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\" now", 5, '"', false), Is.EqualTo((5, 6))); + Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\" now", 5, '"', true), Is.EqualTo((4, 7))); + Assert.That(VimMotionEngine.TextObjectQuote("hello", 1, '"', false), Is.EqualTo((-1, -1))); + } + + [Test] + public void TextObjectDelimitedTest() + { + Assert.That(VimMotionEngine.TextObjectDelimited("f(a, b)", 3, '(', ')', false), Is.EqualTo((2, 5))); + Assert.That(VimMotionEngine.TextObjectDelimited("f(a, b)", 3, '(', ')', true), Is.EqualTo((1, 6))); + // Nested: caret inside the inner pair selects the inner pair. + Assert.That(VimMotionEngine.TextObjectDelimited("(a(b)c)", 3, '(', ')', false), Is.EqualTo((3, 3))); + Assert.That(VimMotionEngine.TextObjectDelimited("(a(b)c)", 3, '(', ')', true), Is.EqualTo((2, 4))); + Assert.That(VimMotionEngine.TextObjectDelimited("abc", 1, '(', ')', false), Is.EqualTo((-1, -1))); + } } } diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 19361f61b95..82f375ebf08 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -109,6 +109,22 @@ public VimManager(MainWindow mainWindow, MainViewModel viewModel, TextBox queryT _queryTextBox.TextChanged += QueryTextBox_TextChanged; _mainWindow.Loaded += MainWindow_Loaded; _viewModel.PropertyChanged += ViewModel_PropertyChanged; + _settings.PropertyChanged += OnSettingsPropertyChanged; + } + + private void OnSettingsPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + // When Vim mode is turned off, return to Insert so native typing is not blocked + // by QueryTextBox_PreviewTextInput and clear any lingering overlays/state. + if (e.PropertyName == nameof(Flow.Launcher.Infrastructure.UserSettings.Settings.EnableVimMode) && !_settings.EnableVimMode) + { + _mainWindow.Dispatcher.BeginInvoke(new Action(() => + { + _queryTextBox.SelectionLength = 0; + ResetPendingState(); + _vimEngine.SwitchToInsert(); + })); + } } private void MainWindow_Loaded(object sender, RoutedEventArgs e) @@ -282,8 +298,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) { SaveVisualRange(); _queryTextBox.SelectionLength = 0; - _pendingCommand = ""; - _awaitingCharCommand = ""; + ResetPendingState(); _vimEngine.SwitchToNormal(); e.Handled = true; return true; @@ -300,8 +315,7 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) else { _lastEscapeTime = DateTime.Now; - _pendingCommand = ""; - _awaitingCharCommand = ""; + ResetPendingState(); e.Handled = true; return true; } @@ -406,10 +420,10 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex)); return true; case Key.G: - if (modifiers.HasFlag(ModifierKeys.Shift)) + // 'g' is the prefix for multi-key commands (gu, gU, g~, gv, g_). + if (modifiers == ModifierKeys.None) { _gPending = true; - return true; } return true; case Key.D0: @@ -518,18 +532,7 @@ private bool HandleVimKey(KeyEventArgs e) return false; case Key.P: - try - { - string text = Clipboard.GetText(); - if (!string.IsNullOrEmpty(text)) - { - int c = _queryTextBox.CaretIndex; - if (c < _queryTextBox.Text.Length) c++; - SetText(_queryTextBox.Text.Insert(c, text)); - _queryTextBox.CaretIndex = c; - } - } - catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard/Redo operation failed", ex); } + PasteAfterCursor(GetCount()); return true; case Key.U: VimUndo(); @@ -561,6 +564,7 @@ private bool HandleVimKey(KeyEventArgs e) _vimEngine.SwitchToInsert(); _lastChange = cmd + cmd; _pendingCommand = ""; + _count = 0; } else if (_pendingCommand != "" && _pendingCommand != cmd) { @@ -629,21 +633,9 @@ private bool HandleVimKey(KeyEventArgs e) case Key.O when modifiers == ModifierKeys.None: SwapVisualEnds(); return true; - case Key.I when modifiers == ModifierKeys.None: - { - int pos = Math.Min(_visualAnchor, _visualCaret); - _queryTextBox.SelectionLength = 0; - _queryTextBox.CaretIndex = pos; - _vimEngine.SwitchToInsert(); - } - return true; - case Key.A when modifiers == ModifierKeys.None: - { - int pos = Math.Max(_visualAnchor, _visualCaret) + 1; - _queryTextBox.SelectionLength = 0; - _queryTextBox.CaretIndex = Math.Min(pos, _queryTextBox.Text.Length); - _vimEngine.SwitchToInsert(); - } + case Key.G when modifiers == ModifierKeys.None: + // 'g' prefix in Visual mode (e.g. gu / gU on the selection). + _gPending = true; return true; case Key.H: ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i))); @@ -891,6 +883,12 @@ private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) case VimModeType.Visual: switch (e.Key) { + case Key.U when modifiers == ModifierKeys.None: + ChangeSelectionCase(toUpper: false); + return true; + case Key.U when modifiers.HasFlag(ModifierKeys.Shift): + ChangeSelectionCase(toUpper: true); + return true; case Key.V: if (_lastVisualRange != null) { @@ -996,6 +994,20 @@ private int GetCount() return c; } + /// + /// Clears all transient command state (pending operator, awaited char/text-object, + /// the g-prefix flag, and the numeric count). Called on Escape and on mode entry + /// so partially-typed commands never leak into the next one. + /// + private void ResetPendingState() + { + _pendingCommand = ""; + _awaitingCharCommand = ""; + _awaitingTextObject = ""; + _gPending = false; + _count = 0; + } + private void RepeatLastChange() { switch (_lastChange) @@ -1027,6 +1039,9 @@ private void RepeatLastChange() case "Y_eol": SetClipboardText(_queryTextBox.Text); break; + case "p": + PasteAfterCursor(GetCount()); + break; case "x": { int n = GetCount(); @@ -1105,6 +1120,30 @@ private void RepeatLastChange() } } + /// + /// Pastes the clipboard text after the cursor (Vim 'p'), times, + /// leaving the caret on the last pasted character. Records the change for '.' repeat. + /// + private void PasteAfterCursor(int count) + { + try + { + string clip = Clipboard.GetText(); + if (string.IsNullOrEmpty(clip)) return; + if (count < 1) count = 1; + + string pasted = clip; + for (int i = 1; i < count; i++) pasted += clip; + + int c = _queryTextBox.CaretIndex; + if (c < _queryTextBox.Text.Length) c++; + SetText(_queryTextBox.Text.Insert(c, pasted)); + _queryTextBox.CaretIndex = Math.Min(c + pasted.Length - 1, Math.Max(0, _queryTextBox.Text.Length - 1)); + _lastChange = "p"; + } + catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard paste operation failed", ex); } + } + private void ExecuteCharCommand(string cmd, char c) { _awaitingCharCommand = ""; @@ -1269,7 +1308,7 @@ private void EnterVisualMode() _queryTextBox.CaretIndex = _queryTextBox.Text.Length - 1; _visualAnchor = _queryTextBox.CaretIndex; _visualCaret = _queryTextBox.CaretIndex; - _pendingCommand = ""; + ResetPendingState(); _vimEngine.SwitchToVisual(); UpdateVisualSelection(); } @@ -1279,7 +1318,7 @@ private void EnterVisualLineMode() if (_queryTextBox.Text.Length == 0) return; _visualAnchor = 0; _visualCaret = _queryTextBox.Text.Length - 1; - _pendingCommand = ""; + ResetPendingState(); _vimEngine.SwitchToVisualLine(); _queryTextBox.Select(0, _queryTextBox.Text.Length); UpdateCaretPosition(); @@ -1293,6 +1332,26 @@ private void SwapVisualEnds() UpdateVisualSelection(); } + /// + /// Applies a case transform to the current Visual-mode selection (gu / gU) and + /// returns to Normal mode, mirroring the '~' selection handler. + /// + private void ChangeSelectionCase(bool toUpper) + { + int selStart = _queryTextBox.SelectionStart; + int selLength = _queryTextBox.SelectionLength; + if (selLength > 0) + { + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = selStart; i < selStart + selLength && i < chars.Length; i++) + chars[i] = toUpper ? char.ToUpper(chars[i]) : char.ToLower(chars[i]); + SetText(new string(chars)); + _queryTextBox.CaretIndex = selStart; + _queryTextBox.SelectionLength = 0; + } + _vimEngine.SwitchToNormal(); + } + private void SaveVisualRange() { _lastVisualRange = (_visualAnchor, _visualCaret); @@ -1349,7 +1408,7 @@ private static char GetCharFromKey(Key key, ModifierKeys modifiers) private void QueryTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { - if (_vimEngine.CurrentMode != VimModeType.Insert) + if (_settings.EnableVimMode && _vimEngine.CurrentMode != VimModeType.Insert) { e.Handled = true; } @@ -1378,6 +1437,7 @@ protected virtual void Dispose(bool disposing) _vimEngine.ModeChanged -= VimEngine_ModeChanged; _mainWindow.Loaded -= MainWindow_Loaded; _viewModel.PropertyChanged -= ViewModel_PropertyChanged; + _settings.PropertyChanged -= OnSettingsPropertyChanged; _queryTextBox.PreviewTextInput -= QueryTextBox_PreviewTextInput; _queryTextBox.SelectionChanged -= QueryTextBox_SelectionChanged; _queryTextBox.TextChanged -= QueryTextBox_TextChanged; diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index 9cbdf51520e..8807c62b556 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -128,6 +128,7 @@ public static int MovePrevWord(string text, int caret) /// The new caret index. public static int MoveEndWord(string text, int caret) { + if (text.Length == 0) return 0; if (caret >= text.Length - 1) return text.Length - 1; int i = caret + 1; @@ -195,6 +196,7 @@ public static int MovePrevWordBig(string text, int caret) /// The new caret index. public static int MoveEndWordBig(string text, int caret) { + if (text.Length == 0) return 0; if (caret >= text.Length - 1) return text.Length - 1; int i = caret + 1; diff --git a/README.md b/README.md index 0d12f24cdf1..eab436325b4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Vim Mode transforms the Flow Launcher search bar into a modal editor, offering * You can instantly toggle this feature on or off via the `General` tab in the Flow Launcher Settings. ### Modes & Intercepts -- **Mode Indicator**: A slick, color-coded pill indicator sits at the far left of the search bar to clearly show your current mode (`NORMAL`, `VISUAL`, etc.). +- **Mode Indicator**: A small, color-coded dot sits at the far left of the search bar to show your current mode at a glance — accent/blue for **Normal**, purple for **Visual**, orange for **Visual Line**. In Insert mode the dot is hidden (you're just typing normally). - **Insert Mode**: The default state. Works exactly like standard Flow Launcher (blinking text caret). - **Normal Mode**: Replaces the blinking caret with a solid block caret, and intercepts alphanumeric keystrokes to execute text manipulation commands. - **Visual Mode**: Char-wise selection. Movements extend the selection from a fixed anchor point; operators apply to the selected region. @@ -59,6 +59,10 @@ You can instantly toggle this feature on or off via the `General` tab in the Flo - `u` : Undo the last Vim operation (operation-level, not character-level) - `Ctrl+R` : Redo the last undone operation +#### Repeat & Counts +- `.` : Repeat the last change (e.g. `x`, `dw`, `r{char}`, `p`). Note: a change that ends in Insert mode (like `cw`) repeats the deletion but does not re-type the inserted text. +- `{count}` : Most motions and operators accept a numeric prefix, e.g. `3w` (forward 3 words), `5x` (delete 5 chars), `2p` (paste twice), `d3w` / `3dw` (delete 3 words). + #### Text Objects (For Operators & Visual Mode) Use these immediately after an operator (like `d`, `c`, `y`, `gu`) to target specific text structures. - **Modifiers**: `i` (inner), `a` (around) @@ -100,6 +104,11 @@ Use these immediately after an operator (like `d`, `c`, `y`, `gu`) to target spe - `V` : In Visual mode, switch to Visual Line mode (selects entire query) - `j` / `k` : Navigate search results (same as Normal mode) +### Known Limitations +- **Keyboard layout**: Character-pending commands (`f`/`F`/`t`/`T`, `r`, and the quote/bracket text objects) reconstruct the target character from the physical key, assuming a US-QWERTY layout. On other layouts, symbols and punctuation may resolve incorrectly. Letters and digits work everywhere. +- **Dot-repeat of inserts**: `.` replays the operator/motion of the last change but not text typed in Insert mode, so `cwfoo.` re-deletes a word without re-typing `foo`. +- **Single line**: The query is a single-line field, so line-wise commands (`dd`, `cc`, `V`, `0`/`$`) operate on the whole query, and `gg`/`G` are not bound. + --- *(The below is the standard Flow Launcher README)* From 84cd930546292e481de940cff1f9a4ea5525cd8a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 01:36:52 -0500 Subject: [PATCH 23/30] Fix operator+motion inclusivity and count handling in Vim mode Second audit pass. Found and fixed a class of off-by-one bugs in operator + motion combinations: - Inclusive forward motions were treated as exclusive, so de/df/dt/d% (and ce/ye/gue/...) left the final character behind. Introduced a MotionInclusivity model + VimMotionEngine.OperatorRange to compute the affected range per Vim's inclusive/exclusive rules, and tagged e/E/f/t as inclusive-forward and % as an inclusive pair (both brackets, either direction). - The gu/gU/g~ branches always extended the range by one, making guw/g~w over-inclusive on exclusive motions; they now respect motion inclusivity. Consolidated the three near-identical branches into one. - A count typed before r/f/t/;/, leaked into the next command; it is now consumed. Added count support: r{char} replaces N chars, f/t/;/, jump to the Nth occurrence. - Reset the count after every completed operator motion. - Add unit tests for OperatorRange covering exclusive, inclusive-forward (incl. backward find and failed find), and inclusive-pair cases. --- Flow.Launcher.Test/VimMotionEngineTest.cs | 34 ++++++ Flow.Launcher/VimMode/VimManager.cs | 120 ++++++++++------------ Flow.Launcher/VimMode/VimMotionEngine.cs | 35 +++++++ 3 files changed, 122 insertions(+), 67 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index d769cbeb014..5589a33cbe2 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -173,5 +173,39 @@ public void TextObjectDelimitedTest() Assert.That(VimMotionEngine.TextObjectDelimited("(a(b)c)", 3, '(', ')', true), Is.EqualTo((2, 4))); Assert.That(VimMotionEngine.TextObjectDelimited("abc", 1, '(', ')', false), Is.EqualTo((-1, -1))); } + + [Test] + public void OperatorRangeExclusiveTest() + { + // dw etc.: half-open, destination not included, in either direction. + Assert.That(VimMotionEngine.OperatorRange(0, 3, MotionInclusivity.Exclusive, 10), Is.EqualTo((0, 3))); + Assert.That(VimMotionEngine.OperatorRange(5, 2, MotionInclusivity.Exclusive, 10), Is.EqualTo((2, 5))); + } + + [Test] + public void OperatorRangeInclusiveForwardTest() + { + // de / df: forward motion consumes the destination character. + Assert.That(VimMotionEngine.OperatorRange(0, 3, MotionInclusivity.InclusiveForward, 10), Is.EqualTo((0, 4))); + // At the last index it still extends to the end of the text. + Assert.That(VimMotionEngine.OperatorRange(0, 9, MotionInclusivity.InclusiveForward, 10), Is.EqualTo((0, 10))); + // Never extends past the text length. + Assert.That(VimMotionEngine.OperatorRange(0, 10, MotionInclusivity.InclusiveForward, 10), Is.EqualTo((0, 10))); + // Backward find (F/T): the original caret char is NOT consumed. + Assert.That(VimMotionEngine.OperatorRange(5, 2, MotionInclusivity.InclusiveForward, 10), Is.EqualTo((2, 5))); + // Failed find (target == caret): empty range, nothing deleted. + Assert.That(VimMotionEngine.OperatorRange(5, 5, MotionInclusivity.InclusiveForward, 10), Is.EqualTo((5, 5))); + } + + [Test] + public void OperatorRangeInclusivePairTest() + { + // d% with caret on '(': forward, both brackets included. + Assert.That(VimMotionEngine.OperatorRange(1, 4, MotionInclusivity.InclusivePair, 6), Is.EqualTo((1, 5))); + // d% with caret on ')': backward, the far-end (the ')') is still included. + Assert.That(VimMotionEngine.OperatorRange(4, 0, MotionInclusivity.InclusivePair, 6), Is.EqualTo((0, 5))); + // No matching bracket (target == caret): empty range, nothing deleted. + Assert.That(VimMotionEngine.OperatorRange(3, 3, MotionInclusivity.InclusivePair, 10), Is.EqualTo((3, 3))); + } } } diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 82f375ebf08..72bbbde1d76 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -405,7 +405,7 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWord(_queryTextBox.Text, i))); return true; case Key.E when modifiers == ModifierKeys.None: - ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWord(_queryTextBox.Text, i))); + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWord(_queryTextBox.Text, i)), MotionInclusivity.InclusiveForward); return true; case Key.W when modifiers.HasFlag(ModifierKeys.Shift): ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveNextWordBig(_queryTextBox.Text, i))); @@ -414,10 +414,10 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MovePrevWordBig(_queryTextBox.Text, i))); return true; case Key.E when modifiers.HasFlag(ModifierKeys.Shift): - ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWordBig(_queryTextBox.Text, i))); + ExecuteMotion(ApplyCountMove(i => VimMotionEngine.MoveEndWordBig(_queryTextBox.Text, i)), MotionInclusivity.InclusiveForward); return true; case Key.D5 when modifiers.HasFlag(ModifierKeys.Shift): - ExecuteMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex)); + ExecuteMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex), MotionInclusivity.InclusivePair); return true; case Key.G: // 'g' is the prefix for multi-key commands (gu, gU, g~, gv, g_). @@ -454,7 +454,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.OemSemicolon: if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { - ExecuteFindCommand(_lastFindCmd, _lastFindChar); + ExecuteFindCommand(_lastFindCmd, _lastFindChar, GetCount()); return true; } return false; @@ -462,7 +462,7 @@ private bool HandleVimKey(KeyEventArgs e) if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { string reverseCmd = _lastFindCmd == "f" ? "F" : _lastFindCmd == "F" ? "f" : _lastFindCmd == "t" ? "T" : "t"; - ExecuteFindCommand(reverseCmd, _lastFindChar); + ExecuteFindCommand(reverseCmd, _lastFindChar, GetCount()); return true; } return false; @@ -692,7 +692,7 @@ private bool HandleVimKey(KeyEventArgs e) case Key.OemSemicolon: if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { - ExecuteFindCommand(_lastFindCmd, _lastFindChar); + ExecuteFindCommand(_lastFindCmd, _lastFindChar, GetCount()); return true; } return true; @@ -700,7 +700,7 @@ private bool HandleVimKey(KeyEventArgs e) if (!modifiers.HasFlag(ModifierKeys.Shift) && _lastFindChar != '\0') { string reverseCmd = _lastFindCmd == "f" ? "F" : _lastFindCmd == "F" ? "f" : _lastFindCmd == "t" ? "T" : "t"; - ExecuteFindCommand(reverseCmd, _lastFindChar); + ExecuteFindCommand(reverseCmd, _lastFindChar, GetCount()); return true; } return true; @@ -1147,6 +1147,9 @@ private void PasteAfterCursor(int count) private void ExecuteCharCommand(string cmd, char c) { _awaitingCharCommand = ""; + // Consume any count typed before the operator (e.g. 3rx, 2f,) so it never leaks + // into the next command. + int count = GetCount(); string text = _queryTextBox.Text; int caret = _queryTextBox.CaretIndex; @@ -1169,47 +1172,61 @@ private void ExecuteCharCommand(string cmd, char c) } else if (caret < text.Length) { - SetText(text.Remove(caret, 1).Insert(caret, c.ToString())); - _queryTextBox.CaretIndex = caret; - _lastChange = "r"; - _lastReplaceChar = c; + // r{char} with a count replaces that many characters (vim no-ops if fewer remain). + int len = Math.Min(count, text.Length - caret); + if (count <= text.Length - caret && len > 0) + { + char[] chars = text.ToCharArray(); + for (int i = caret; i < caret + len; i++) chars[i] = c; + SetText(new string(chars)); + _queryTextBox.CaretIndex = caret + len - 1; + _lastChange = "r"; + _lastReplaceChar = c; + } } } else if (cmd == "f" || cmd == "F" || cmd == "t" || cmd == "T") { _lastFindCmd = cmd; _lastFindChar = c; - ExecuteFindCommand(cmd, c); + ExecuteFindCommand(cmd, c, count); } } - private void ExecuteFindCommand(string cmd, char c) + private void ExecuteFindCommand(string cmd, char c, int count = 1) { string text = _queryTextBox.Text; int caret = (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) ? _visualCaret : _queryTextBox.CaretIndex; - int target = caret; - if (cmd == "f") target = VimMotionEngine.FindCharForward(text, caret, c, false); - else if (cmd == "F") target = VimMotionEngine.FindCharBackward(text, caret, c, false); - else if (cmd == "t") target = VimMotionEngine.FindCharForward(text, caret, c, true); - else if (cmd == "T") target = VimMotionEngine.FindCharBackward(text, caret, c, true); + int target = caret; + for (int k = 0; k < (count < 1 ? 1 : count); k++) + { + int next = cmd switch + { + "f" => VimMotionEngine.FindCharForward(text, target, c, false), + "F" => VimMotionEngine.FindCharBackward(text, target, c, false), + "t" => VimMotionEngine.FindCharForward(text, target, c, true), + "T" => VimMotionEngine.FindCharBackward(text, target, c, true), + _ => target + }; + if (next == target) break; // not found / no further progress + target = next; + } if (_vimEngine.CurrentMode == VimModeType.Visual || _vimEngine.CurrentMode == VimModeType.VisualLine) ExecuteVisualMotion(target); else - ExecuteMotion(target); + ExecuteMotion(target, MotionInclusivity.InclusiveForward); } - private void ExecuteMotion(int targetCaret) + private void ExecuteMotion(int targetCaret, MotionInclusivity inclusivity = MotionInclusivity.Exclusive) { if (_pendingCommand == "d" || _pendingCommand == "c" || _pendingCommand == "y") { - int start = Math.Max(0, _queryTextBox.CaretIndex); - int end = Math.Max(0, targetCaret); - if (start > end) { var temp = start; start = end; end = temp; } - + var (start, end) = VimMotionEngine.OperatorRange(_queryTextBox.CaretIndex, targetCaret, inclusivity, _queryTextBox.Text.Length); + if (end > start) { SetClipboardText(_queryTextBox.Text.Substring(start, end - start)); @@ -1219,64 +1236,33 @@ private void ExecuteMotion(int targetCaret) _queryTextBox.CaretIndex = start; } } - + if (_pendingCommand == "c") _vimEngine.SwitchToInsert(); _lastChange = _pendingCommand + "_motion"; _lastChangeLen = end - start; _pendingCommand = ""; + _count = 0; } - else if (_pendingCommand == "~") + else if (_pendingCommand == "~" || _pendingCommand == "gu" || _pendingCommand == "gU") { - int start = Math.Max(0, _queryTextBox.CaretIndex); - int end = Math.Max(0, targetCaret); - if (start > end) { var temp = start; start = end; end = temp; } - if (end < _queryTextBox.Text.Length) end++; - + var (start, end) = VimMotionEngine.OperatorRange(_queryTextBox.CaretIndex, targetCaret, inclusivity, _queryTextBox.Text.Length); + if (end > start) { char[] chars = _queryTextBox.Text.ToCharArray(); for (int i = start; i < end && i < chars.Length; i++) - chars[i] = char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); + { + chars[i] = _pendingCommand == "gu" ? char.ToLower(chars[i]) + : _pendingCommand == "gU" ? char.ToUpper(chars[i]) + : char.IsUpper(chars[i]) ? char.ToLower(chars[i]) : char.ToUpper(chars[i]); + } SetText(new string(chars)); _queryTextBox.CaretIndex = Math.Min(start, _queryTextBox.Text.Length); } - _lastChange = "~"; - _pendingCommand = ""; - } - else if (_pendingCommand == "gu") - { - int start = Math.Max(0, _queryTextBox.CaretIndex); - int end = Math.Max(0, targetCaret); - if (start > end) { var temp = start; start = end; end = temp; } - if (end < _queryTextBox.Text.Length) end++; - - if (end > start) - { - char[] chars = _queryTextBox.Text.ToCharArray(); - for (int i = start; i < end && i < chars.Length; i++) - chars[i] = char.ToLower(chars[i]); - SetText(new string(chars)); - _queryTextBox.CaretIndex = start; - } - _pendingCommand = ""; - } - else if (_pendingCommand == "gU") - { - int start = Math.Max(0, _queryTextBox.CaretIndex); - int end = Math.Max(0, targetCaret); - if (start > end) { var temp = start; start = end; end = temp; } - if (end < _queryTextBox.Text.Length) end++; - - if (end > start) - { - char[] chars = _queryTextBox.Text.ToCharArray(); - for (int i = start; i < end && i < chars.Length; i++) - chars[i] = char.ToUpper(chars[i]); - SetText(new string(chars)); - _queryTextBox.CaretIndex = start; - } + if (_pendingCommand == "~") _lastChange = "~"; _pendingCommand = ""; + _count = 0; } else { diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index 8807c62b556..eca4967ec3f 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -2,11 +2,46 @@ namespace Flow.Launcher.VimMode { + /// + /// Describes how an operator (d/c/y/gu/gU/g~) treats the character at the far end of a motion. + /// + public enum MotionInclusivity + { + /// Half-open range; the destination character is not affected (w, b, h, l, 0, ^, $). + Exclusive, + /// Forward motions that include the destination character (e, E, f, t). + InclusiveForward, + /// Paired motions that include the far-end character in either direction (%). + InclusivePair + } + /// /// Provides pure static methods for calculating caret movements and text object selections. /// public class VimMotionEngine { + /// + /// Computes the character range [start, end) an operator should act on, given the caret, + /// the motion target, and the motion's inclusivity. Handles the Vim rule that inclusive + /// forward motions consume the destination character while backward find motions do not. + /// + public static (int start, int end) OperatorRange(int caret, int target, MotionInclusivity inclusivity, int textLength) + { + int start = Math.Max(0, Math.Min(caret, target)); + int end = Math.Max(0, Math.Max(caret, target)); + + // Inclusive-forward (e/f/t) consumes the destination only when the motion moved forward; + // a backward find (F/T) leaves the original caret char untouched. A paired motion (%) + // consumes the far-end char in either direction, but only when it actually spans a range. + bool extend = (inclusivity == MotionInclusivity.InclusiveForward && target > caret) + || (inclusivity == MotionInclusivity.InclusivePair && end > start); + + if (extend && end < textLength) + end++; + + return (start, end); + } + /// /// Calculates the new caret position after moving left one character. /// From bd31efe0e78a3ab57b7b2acb2495e49dbb5a876a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 10:49:54 -0500 Subject: [PATCH 24/30] Clean up Vim mode PR for upstream: restore README, move docs, drop fork CI Prepares the upstream PR for review by removing fork-only artifacts so the change touches feature files only: - Restore README.md to match upstream exactly. The branch had rebranded it to a "Vim Edition" README, deleting upstream's badges and content. - Move the Vim documentation to a dedicated Flow.Launcher/VimMode/README.md co-located with the feature: modes, keybindings, counts/repeat, the engine architecture, and known limitations. - Drop the fork-specific "vim-mode" branch trigger from dotnet.yml. - Revert incidental packages.lock.json drift (Flow.Launcher.Plugin 5.0.0 -> 5.3.1) that came from a local restore. - Restore the UTF-8 BOM on MainWindow.xaml.cs to avoid an encoding-only diff. - Fix stray indentation on two VimMotionEngine text-object methods. --- .github/workflows/dotnet.yml | 1 - Flow.Launcher/MainWindow.xaml.cs | 2 +- Flow.Launcher/VimMode/README.md | 92 ++++ Flow.Launcher/VimMode/VimMotionEngine.cs | 4 +- Flow.Launcher/packages.lock.json | 4 +- README.md | 556 ++++++++++++++++++----- 6 files changed, 534 insertions(+), 125 deletions(-) create mode 100644 Flow.Launcher/VimMode/README.md diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 33904c9fcae..c22699b0e31 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -9,7 +9,6 @@ on: branches: - dev - master - - vim-mode pull_request: jobs: diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index c1a9f619d2b..5d0809ea0e4 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Media; diff --git a/Flow.Launcher/VimMode/README.md b/Flow.Launcher/VimMode/README.md new file mode 100644 index 00000000000..0dfedf2b848 --- /dev/null +++ b/Flow.Launcher/VimMode/README.md @@ -0,0 +1,92 @@ +# Vim Mode + +An **opt-in**, terminal-style Vim editing layer for the Flow Launcher search bar. It turns the query box +into a small modal editor so you can navigate and fix long queries without leaving the home row. + +The feature is **disabled by default**. Enable it under **Settings → General → "Enable Advanced Vim Mode"**. +When off, the search bar behaves exactly as it always has. + +## Design + +The implementation is split into three pieces so the logic stays testable: + +| Type | Responsibility | +| --- | --- | +| [`VimEngine`](VimEngine.cs) | The mode state machine (Insert / Normal / Visual / Visual Line) and the `ModeChanged` event. | +| [`VimMotionEngine`](VimMotionEngine.cs) | Pure, side-effect-free caret/range math (motions, text objects, operator ranges). Fully unit-tested. | +| [`VimManager`](VimManager.cs) | Wires the engines into the WPF `MainWindow`: key interception, the block-caret overlay, the mode indicator, and clipboard operations. Implements `IDisposable` and is torn down in `MainWindow.Dispose`. | + +Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineTest`, `VimMotionEngineTest`). + +## Modes + +- **Mode indicator** — a small, color-coded dot at the left of the search bar shows the current mode: + accent for **Normal**, purple for **Visual**, orange for **Visual Line**. In Insert mode the dot is hidden. +- **Insert** — the default. Works exactly like the standard search bar (blinking caret). +- **Normal** — a solid block caret; alphanumeric keys are interpreted as commands instead of text. +- **Visual** — character-wise selection; motions extend the selection from a fixed anchor. +- **Visual Line** — selects the whole query; operators apply to all of it. +- **`Esc`** — from Insert, switches to Normal. **Double-`Esc`** (within 400 ms) hides the launcher. +- **`Ctrl`/`Alt` chords pass through untouched**, so existing Flow Launcher hotkeys are unaffected. + +## Normal mode + +### Navigation +- `h` / `l` — move left / right +- `j` / `k` — move down / up through the search results +- `0` — start of the query · `^` — first non-blank · `$` — end of the query + +### Word motions +- `w` / `W` — start of next word / WORD +- `e` / `E` — end of word / WORD +- `b` / `B` — start of previous word / WORD + +### Character search +- `f{char}` / `F{char}` — to next / previous `{char}` +- `t{char}` / `T{char}` — till just before / after `{char}` +- `;` / `,` — repeat the last character search, same / opposite direction +- `%` — jump to the matching bracket + +### Editing (integrated with the system clipboard) +- `x` / `X` — delete the character under / before the cursor +- `s` / `S` — substitute the character / whole query, then enter Insert +- `r{char}` — replace the character(s) under the cursor with `{char}` +- `~` — toggle the case of the character under the cursor +- `gu` / `gU` — lowercase / uppercase (operator + motion, e.g. `guw`) +- `dd` / `cc` — delete / change the whole query +- `D` / `C` — delete / change from the cursor to the end +- `Y` — yank the whole query · `p` — paste after the cursor +- `u` — undo the last operation · `Ctrl+R` — redo + +### Operators + text objects +Use a text object after an operator (`d`, `c`, `y`, `gu`, …): +- modifiers: `i` (inner), `a` (around) +- targets: `w` (word), `"` `'` (quotes), `(` `[` `{` (brackets) +- examples: `diw` (delete inner word), `ci"` (change inside quotes), `ya(` (yank around parens) + +### Repeat & counts +- `.` — repeat the last change (e.g. `x`, `dw`, `r{char}`, `p`). +- `{count}` — most motions and operators take a numeric prefix: `3w`, `5x`, `2p`, `d3w` / `3dw`, `3f,`, `2;`. + +### Mode switches +- `i` / `I` — insert at the cursor / start of the query +- `a` / `A` — insert after the cursor / at the end of the query +- `v` / `V` — Visual / Visual Line mode + +## Visual mode + +- Extend the selection with any motion (`h` `l` `w` `b` `e` `0` `^` `$` `f`/`t`/`F`/`T`, `;` `,`). +- Operators on the selection: `d` / `x` (delete), `y` (yank), `c` / `s` (change), `r{char}` (replace), + `~` (toggle case), `gu` / `gU` (lower / upper). +- `i` / `a` start a text object (e.g. `vi(`), `o` swaps the selection ends. +- `v` toggles between Visual and Visual Line; `Esc` returns to Normal; `j` / `k` navigate results. + +## Known limitations + +- **Keyboard layout** — character-pending commands (`f`/`F`/`t`/`T`, `r`, and the quote/bracket text + objects) reconstruct the target character from the physical key, assuming a US-QWERTY layout. Letters and + digits work on any layout; some symbols/punctuation may resolve incorrectly on others. +- **Dot-repeat of inserts** — `.` replays the operator/motion of the last change but not text typed in + Insert mode, so `cwfoo.` re-deletes a word without re-typing `foo`. +- **Single line** — the query is a single line, so line-wise commands (`dd`, `cc`, `V`, `0`/`$`) act on the + whole query and `gg`/`G` are not bound. diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index eca4967ec3f..c1cd6a642f9 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -364,7 +364,7 @@ public static (int start, int end) TextObjectWord(string text, int caret, bool a /// The closing delimiter. /// True to include the delimiters (a object), false to exclude (i object). /// A tuple containing the start and end indices, or (-1, -1) if invalid. - public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) + public static (int start, int end) TextObjectDelimited(string text, int caret, char open, char close, bool around) { int openPos = -1; int depth = 0; @@ -410,7 +410,7 @@ public static (int start, int end) TextObjectDelimited(string text, int caret, c /// The quote character. /// True to include the quotes, false to exclude. /// A tuple containing the start and end indices, or (-1, -1) if invalid. - public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) + public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) { int first = -1; diff --git a/Flow.Launcher/packages.lock.json b/Flow.Launcher/packages.lock.json index 3208907704c..b4b929d1965 100644 --- a/Flow.Launcher/packages.lock.json +++ b/Flow.Launcher/packages.lock.json @@ -1619,7 +1619,7 @@ "FSharp.Core": "[9.0.303, )", "Flow.Launcher.Infrastructure": "[1.0.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.3.1, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "Meziantou.Framework.Win32.Jobs": "[3.4.5, )", "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "SemanticVersioning": "[3.0.0, )", @@ -1634,7 +1634,7 @@ "BitFaster.Caching": "[2.5.4, )", "CommunityToolkit.Mvvm": "[8.4.0, )", "Flow.Launcher.Localization": "[0.0.6, )", - "Flow.Launcher.Plugin": "[5.3.1, )", + "Flow.Launcher.Plugin": "[5.0.0, )", "InputSimulator": "[1.0.4, )", "MemoryPack": "[1.21.4, )", "Microsoft.VisualStudio.Threading": "[17.14.15, )", diff --git a/README.md b/README.md index eab436325b4..a1a5107412c 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,437 @@ -# Flow Launcher - Vim Edition - -This is a specialized fork of [Flow Launcher](https://github.com/Flow-Launcher/Flow.Launcher), equipped with a fully featured, terminal-grade inline **Vim Mode** directly integrated into the search bar. - -## Why does this exist? -This fork was originally created to streamline journaling workflows. By using Flow Launcher to quickly input journal entries into a personal journaling system, typing out long strings quickly became tedious to edit. Standard text box navigation isn't fast enough. This fork adds native Vim motions directly to the search bar so that when you make a mistake typing a long entry or command, you can instantly drop into Normal mode, hop across word boundaries, and fix the typo without your hands ever leaving the home row. - ---- - -## 🛠️ Advanced Vim Features - -Vim Mode transforms the Flow Launcher search bar into a modal editor, offering **Insert**, **Normal**, **Visual**, and **Visual Line** modes, complete with an overlaid block caret that physically tracks to the text layout. - -You can instantly toggle this feature on or off via the `General` tab in the Flow Launcher Settings. - -### Modes & Intercepts -- **Mode Indicator**: A small, color-coded dot sits at the far left of the search bar to show your current mode at a glance — accent/blue for **Normal**, purple for **Visual**, orange for **Visual Line**. In Insert mode the dot is hidden (you're just typing normally). -- **Insert Mode**: The default state. Works exactly like standard Flow Launcher (blinking text caret). -- **Normal Mode**: Replaces the blinking caret with a solid block caret, and intercepts alphanumeric keystrokes to execute text manipulation commands. -- **Visual Mode**: Char-wise selection. Movements extend the selection from a fixed anchor point; operators apply to the selected region. -- **Visual Line Mode**: Line-wise selection. Selects the entire query; operators apply to the whole line. -- **Escape**: Pressing `Escape` while typing will unconditionally kick you into Normal mode. -- **Double-Escape**: Quickly double-tapping `Escape` (within 400ms) will immediately hide the Flow Launcher UI. - -### Normal Mode Keybindings - -#### Basic Navigation -- `h` / `l` : Move cursor left / right -- `j` / `k` : Navigate up / down through Flow Launcher search results -- `0` : Snap to the absolute beginning of the query -- `^` : Snap to the first non-blank character of the query -- `$` : Snap to the end of the query - -#### Word Boundaries -- `w` / `W` (Shift) : Jump to the start of the next word / BIG word boundary -- `e` / `E` (Shift) : Jump to the end of the current word / BIG word boundary -- `b` / `B` (Shift) : Jump back to the start of the previous word / BIG word boundary - -#### Character Lookup -- `f{char}` : Find the next occurrence of `{char}` forward -- `F{char}` : Find the previous occurrence of `{char}` backward -- `t{char}` : Jump till just before the next occurrence of `{char}` -- `T{char}` : Jump till just after the previous occurrence of `{char}` -- `;` : Repeat the last find lookup in the same direction -- `,` : Repeat the last find lookup in the opposite direction - -#### Text Manipulation (Integrated with System Clipboard) -- `x` : Delete the character under the cursor -- `X` (Shift+X) : Delete the character before the cursor (Backspace equivalent) -- `s` : Substitute character (Deletes under cursor and enters Insert mode) -- `S` (Shift+S) : Substitute line (Clears query and enters Insert mode) -- `r{char}` : Replace the character under the cursor with `{char}` -- `~` : Toggle the casing of the character under the cursor -- `gu` / `gU` : Make lowercase / uppercase (operator pending, e.g., `guw` to lower a word) -- `dd` / `cc` : Delete or Change the entire query -- `D` / `C` : Delete or Change from the cursor to the end of the line -- `Y` (Shift+Y) : Yank (Copy) the entire query -- `p` : Paste from system clipboard -- `u` : Undo the last Vim operation (operation-level, not character-level) -- `Ctrl+R` : Redo the last undone operation - -#### Repeat & Counts -- `.` : Repeat the last change (e.g. `x`, `dw`, `r{char}`, `p`). Note: a change that ends in Insert mode (like `cw`) repeats the deletion but does not re-type the inserted text. -- `{count}` : Most motions and operators accept a numeric prefix, e.g. `3w` (forward 3 words), `5x` (delete 5 chars), `2p` (paste twice), `d3w` / `3dw` (delete 3 words). - -#### Text Objects (For Operators & Visual Mode) -Use these immediately after an operator (like `d`, `c`, `y`, `gu`) to target specific text structures. -- **Modifiers**: `i` (inner), `a` (around) -- **Targets**: `w` (word), `"` (double quotes), `'` (single quotes), `(` (parentheses), `[` (brackets), `{` (braces) -- *Example*: `diw` (delete inner word), `ci"` (change inside quotes), `ya(` (yank around parentheses) - -#### Mode Switching -- `i` : Enter Insert mode at the cursor -- `I` (Shift+I) : Enter Insert mode at the beginning of the query -- `a` : Enter Insert mode after the cursor -- `A` (Shift+A) : Enter Insert mode at the end of the query -- `v` : Enter Visual mode (char-wise selection) -- `V` (Shift+V) : Enter Visual Line mode (select entire query) - -### Visual Mode Keybindings - -#### Entering Visual Mode -- `v` : From Normal mode, enter char-wise Visual mode at the cursor -- `V` (Shift+V) : From Normal mode, enter Visual Line mode (selects entire query) - -#### Extending the Selection (Visual mode only) -- `h` / `l` : Extend selection left / right -- `w` / `b` / `e` (and `W`/`B`/`E`) : Extend by word boundary -- `0` / `^` / `$` : Extend to beginning / first non-blank / end of query -- `f{char}` / `F{char}` / `t{char}` / `T{char}` : Extend to character lookup -- `;` / `,` : Repeat last character lookup - -#### Operators (work on the selected region) -- `x` / `d` : Delete the selection (copies to clipboard) -- `y` : Yank (copy) the selection -- `c` / `s` : Change the selection (delete and enter Insert mode) -- `r{char}` : Replace every character in the selection with `{char}` -- `~` : Toggle casing of every character in the selection -- `gu` / `gU` : Make every character in the selection lowercase / uppercase - -#### Mode Transitions -- `Esc` : Return to Normal mode (clears selection) -- `v` : In Visual Line mode, switch to char-wise Visual mode -- `V` : In Visual mode, switch to Visual Line mode (selects entire query) -- `j` / `k` : Navigate search results (same as Normal mode) - -### Known Limitations -- **Keyboard layout**: Character-pending commands (`f`/`F`/`t`/`T`, `r`, and the quote/bracket text objects) reconstruct the target character from the physical key, assuming a US-QWERTY layout. On other layouts, symbols and punctuation may resolve incorrectly. Letters and digits work everywhere. -- **Dot-repeat of inserts**: `.` replays the operator/motion of the last change but not text typed in Insert mode, so `cwfoo.` re-deletes a word without re-typing `foo`. -- **Single line**: The query is a single-line field, so line-wise commands (`dd`, `cc`, `V`, `0`/`$`) operate on the whole query, and `gg`/`G` are not bound. - ---- - -*(The below is the standard Flow Launcher README)* - -# Flow Launcher -Flow Launcher is a quick file search and app launcher for Windows with community-made plugins. - -[Website](https://flowlauncher.com) | [Documentation](https://flowlauncher.com/docs/) | [Plugin Store](https://flowlauncher.com/docs/plugins.html) +

+ + +
+ +

+

+ + + +
+ + + + +

+ +

+A quick file search and app launcher for Windows with community-made plugins.

+ +

+Dedicated to making your workflow more seamless. Search everything from applications, files, bookmarks, YouTube, Twitter and more. Flow will continue to evolve, designed to be open and built with the community at heart.

+ +

Remember to star it, Flow will love you more :)

+ + + +

+ Official Channels • + Getting Started • + Features • + Plugins • + Hotkeys • + Sponsors • + Questions/Suggestions • + Development • + Docs +

+ + + +## 📣 Official Channels + +- Website: [https://flowlauncher.com](https://flowlauncher.com) +- GitHub Organization: [https://github.com/Flow-Launcher](https://github.com/Flow-Launcher) +- Discord: [https://discord.gg/AvgAQgh](https://discord.gg/AvgAQgh) +- Reddit: [https://www.reddit.com/r/FlowLauncher/](https://www.reddit.com/r/FlowLauncher/) + +⚠️ Only trust official channels for downloads and announcements, and be careful of similar-looking domains. + +For installation, use the official methods listed below. + + + +## 🚗 Getting Started + +### Installation + +[Windows 10+ Installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) or [Portable Version](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) + +#### Winget + +``` +winget install "Flow Launcher" +``` + +#### Scoop + +``` +scoop install Flow-Launcher +``` + +#### Chocolatey + +``` +choco install Flow-Launcher +``` + +> When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo then you are good to continue the set up. + +Or download the [early access version](https://github.com/Flow-Launcher/Prereleases/releases). + + + +## 🎁 Features + +### Applications & Files + + + +- Search for apps, files or file contents. +- Supports Everything and Windows Index. + + + +- Supports search using environment variable paths. + +### Web Searches & URLs + + + + + +### Browser Bookmarks + + + +### System Commands + + + +- Provides system related commands. shutdown, lock, settings, etc. +- [System command list](#system-command-list) + +### Calculator + + + +- Do mathematical calculations and copy the result to clipboard. + +### Shell Command + + + +- Run batch and PowerShell commands as Administrator or a different user. +- Ctrl+Shift+Enter to Run as Administrator. + +### Explorer + + + +- Save file or folder locations for quick access. + +### Windows & Control Panel Settings + + + +- Search for Windows & Control Panel settings. + +### Dialog Jump + + + +- Search for a file/folder and quickly jump to its path in the Open/Save As dialog window. +- Use Alt+G to quickly jump the Open/Save As dialog window path to the path of the active file manager. + +### Drag & Drop + + + +- Drag a file/folder to File Explorer, or even Discord. +- Copy/move behavior can be changed via Ctrl or Shift, and the operation is displayed on the mouse cursor. + +### Priority + + + +- Prioritize the order of each plugin's results. + +### Preview Panel + + + +- Use F1 to toggle the preview panel. +- Media files will be displayed as large images, otherwise a large icon and full path will be displayed. +- Turn on preview permanently via Settings (Always Preview). +- Use Ctrl++/- and Ctrl+[/] to adjust search window width and height quickly if the preview area is too narrow. + +### Customizations + + + +- Window size adjustment, animation, and sound +- Color Scheme (aka Dark Mode) + + + +- There are various themes and you also can make your own. + +### Date & Time Display In Search Window + + + +- Display date and time in search window. + +### 💬 Languages + +- Supports languages from Chinese to Italian and more. +- Supports Pinyin (拼音) search. +- [Crowdin](https://crowdin.com/project/flow-launcher) support for language translations. + +
+Supported languages +
    +
  • English
  • +
  • 中文
  • +
  • 中文(繁体)
  • +
  • Українська
  • +
  • Русский
  • +
  • Français
  • +
  • 日本語
  • +
  • Dutch
  • +
  • Polski
  • +
  • Dansk
  • +
  • de, Deutsch
  • +
  • ko, 한국어
  • +
  • Srpski
  • +
  • Српски
  • +
  • Português
  • +
  • Português (Brasil)
  • +
  • Spanish
  • +
  • es-419, Spanish (Latin America)
  • +
  • Italiano
  • +
  • Norsk Bokmål
  • +
  • Slovenčina
  • +
  • Türkçe
  • +
  • čeština
  • +
  • اللغة العربية
  • +
  • Tiếng Việt
  • +
  • עברית
  • +
+
+ +### Portable + +- Fully portable. +- Type `flow user data` to open your saved user settings folder. They are located at: + - If using roaming: `%APPDATA%\FlowLauncher` + - If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData` + - Type `open log location` to open your logs folder, they are saved along with your user settings folder. + +### 🎮 Game Mode + + + +- Pause hotkey activation when you are playing games. +- When in search window use Ctrl+F12 to toggle on/off. +- Type `Toggle Game Mode` + + + +## 📦 Plugins + +- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio. +- Publish your own plugin to Flow! Create plugins in: + +

+ + + + + +

+ +### [SpotifyPremium](https://github.com/fow5040/Flow.Launcher.Plugin.SpotifyPremium) + + + +### [Steam Search](https://github.com/Garulf/Steam-Search) + + + +### [Clipboard+](https://github.com/Jack251970/Flow.Launcher.Plugin.ClipboardPlus) + + + +### [Home Assistant Commander](https://github.com/Garulf/HA-Commander) + + + +### [Colors](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Color) + + + +### [GitHub](https://github.com/JohnTheGr8/Flow.Plugin.Github) + + + +### [Window Walker](https://github.com/taooceros/Flow.Plugin.WindowWalker) + + + +......and [more!](https://flowlauncher.com/docs/#/plugins) + + + +### 🛒 Plugin Store + + + +- You can view the full plugin list or quickly install a plugin via the Plugin Store menu inside Settings + +- or type `pm` `install`/`uninstall`/`update` + the plugin name in the search window, + + + +## ⌨️ Hotkeys + +| Hotkey | Description | +| ------------------------------------------------------------------------- | ----------------------------------------------- | +| Alt+Space | Open search window (default and configurable) | +| Enter | Execute | +| Ctrl+Enter | Open containing folder | +| Ctrl+Shift+Enter | Run as admin | +| /, Shift+Tab/Tab | Previous / Next result | +| / | Back to result / Open Context Menu | +| Ctrl+O , Shift+Enter | Open Context Menu | +| Ctrl+Tab | Autocomplete | +| F1 | Toggle Preview Panel (default and configurable) | +| Esc | Back to results / hide search window | +| Ctrl+C | Copy folder / file | +| Ctrl+Shift+C | Copy folder / file path | +| Ctrl+I | Open Flow's settings | +| Ctrl+R | Run the current query again (refresh results) | +| F5 | Reload all plugin data | +| Ctrl+F12 | Toggle Game Mode when in search window | +| Ctrl++,- | Adjust maximum results shown | +| Ctrl+[,] | Adjust search window width | +| Ctrl+H | Open search history | +| Ctrl+Backspace | Back to previous directory | +| PageUp/PageDown | Previous / Next Page | + +## System Command List + +| Command | Description | +| ---------------------------------- | --------------------------------------------------------------------------- | +| Shutdown | Shutdown computer | +| Restart | Restart computer | +| Restart With Advanced Boot Options | Restart the computer with Advanced Boot option for safe and debugging modes | +| Log Off/Sign Out | Log off | +| Lock | Lock computer | +| Sleep | Put computer to sleep | +| Hibernate | Hibernate computer | +| Empty Recycle Bin | Empty recycle bin | +| Open Recycle Bin | Open recycle bin | +| Exit | Close Flow Launcher | +| Save Settings | Save all Flow Launcher settings | +| Restart Flow Launcher | Restart Flow Launcher | +| Settings | Tweak this app | +| Reload Plugin Data | Refreshes plugin data with new content | +| Check For Update | Check for new Flow Launcher update | +| Open Log Location | Open Flow Launcher's log location | +| Index Option | Open Windows Search Index window | +| Flow Launcher Tips | Visit Flow Launcher's documentation for more help and usage tips | +| Flow Launcher UserData Folder | Open the location where Flow Launcher's settings are stored | +| Toggle Game Mode | Toggle Game Mode | +| Set Flow Launcher Theme | Set the Flow Launcher Theme | + +### 💁‍♂️ Tips + +- [More tips](https://flowlauncher.com/docs/#/usage-tips) + + + +## Sponsors +

+ + Coderabbit Logo + +
+
+ + + + + + +

+

+ + Appwrite Logo + +
+

+

+ + + + + + + + + +

+ +### Mentions + +- [Why I Chose to Support Flow-Launcher](https://dev.to/appwrite/appwrite-loves-open-source-why-i-chose-to-support-flow-launcher-54pj) - Appwrite +- [Softpedia Editor's Pick](https://www.softpedia.com/get/System/Launchers-Shutdown-Tools/Flow-Launcher.shtml) + + + +## ❔ Questions/Suggestions + +Yes please, let us know in the [Q&A](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/q-a) section. **Join our community on [Discord](https://discord.gg/AvgAQgh)!** + + + +## Development + +### Localization + +Our project localization is based on [Crowdin](https://crowdin.com). If you would like to change them, please go to https://crowdin.com/project/flow-launcher. + +### WPF UI Library + +Our UI library is using [iNKORE.UI.WPF.Modern](https://github.com/iNKORE-NET/UI.WPF.Modern). + + + iNKORE.UI.WPF.Modern + + +### New Changes + +All changes to Flow are captured via pull requests. Some new changes may have been merged but are still pending release. This means that while a change may not exist in the current release, it may have been accepted and merged into the dev branch and is available as a pre-release download. It is therefore a good idea to search through the open and closed pull requests before you start to make changes to ensure the change you intend to make is not already done. + +Each of the pull requests will be marked with a milestone indicating the planned release version for the change. + +### Contributing + +Contributions are very welcome, in addition to the main project (C#) there are also [documentation](https://github.com/Flow-Launcher/docs) (md), [website](https://github.com/Flow-Launcher/flow-launcher.github.io) (html/css) and [others](https://github.com/Flow-Launcher) that can be contributed to. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to submit a pull request. + +You will find the main goals of Flow placed under the [Projects board](https://github.com/orgs/Flow-Launcher/projects/4), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well. + +Get in touch if you would like to join the Flow-Launcher Team and help build this great tool. + +### Developing/Debugging + +- Flow Launcher's target framework is .Net 9 + +- Install Visual Studio 2022 (v17.12+) + +- Install .Net 9 SDK + - via Visual Studio installer + - via winget `winget install Microsoft.DotNet.SDK.9` + - Manually from [here](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) From a1bbaf4ed017e91c88eaddd17cd89e2d698e3e1a Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 12:56:35 -0500 Subject: [PATCH 25/30] Document the color-coded dot mode indicator design choice The mode is shown as a small color-coded dot rather than a NORMAL/VISUAL text label, deliberately, so the indicator never overlaps the query text or alters Flow Launcher's search-bar layout (and non-Vim users see no change). Documented in both VimManager.ApplyModeUI and the Vim mode README; a text label can be added later if preferred. Addresses the reviewer note about the indicator not showing mode text. --- Flow.Launcher/VimMode/README.md | 3 +++ Flow.Launcher/VimMode/VimManager.cs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher/VimMode/README.md b/Flow.Launcher/VimMode/README.md index 0dfedf2b848..53279c60902 100644 --- a/Flow.Launcher/VimMode/README.md +++ b/Flow.Launcher/VimMode/README.md @@ -22,6 +22,9 @@ Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineT - **Mode indicator** — a small, color-coded dot at the left of the search bar shows the current mode: accent for **Normal**, purple for **Visual**, orange for **Visual Line**. In Insert mode the dot is hidden. + A dot (rather than a `NORMAL`/`VISUAL` text label) is used deliberately: it conveys the mode at a glance + without overlapping the query text or changing Flow Launcher's existing search-bar layout, so users who + never enable Vim mode see no difference. A text label could be added later if reviewers prefer one. - **Insert** — the default. Works exactly like the standard search bar (blinking caret). - **Normal** — a solid block caret; alphanumeric keys are interpreted as commands instead of text. - **Visual** — character-wise selection; motions extend the selection from a fixed anchor. diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index 72bbbde1d76..d4d4c3ac04c 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -206,8 +206,12 @@ private void ApplyModeUI(VimModeType mode) if (_vimModeIndicator != null) { + // The mode is shown as a small color-coded dot rather than a text label + // (Normal = accent, Visual = purple, Visual Line = orange). This keeps the + // indicator from overlapping the query text or otherwise altering Flow + // Launcher's search-bar layout; a text label can be added later if desired. _vimModeIndicator.Visibility = _settings.EnableVimMode && mode != VimModeType.Insert ? Visibility.Visible : Visibility.Collapsed; - + _vimModeIndicator.Background = mode switch { VimModeType.Normal => (System.Windows.Media.Brush)Application.Current.FindResource("BasicSystemAccentColor") ?? CreateBrush(0, 120, 215), From 7171df474691f2fd377118636064787b282548ff Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 17:08:35 -0500 Subject: [PATCH 26/30] Add P (paste before cursor), the counterpart to p Normal-mode P pastes the clipboard before the cursor (p pastes after). Small Vim-completeness addition backported from the multi-line fork; documented in the Vim mode README. --- Flow.Launcher/VimMode/README.md | 2 +- Flow.Launcher/VimMode/VimManager.cs | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/VimMode/README.md b/Flow.Launcher/VimMode/README.md index 53279c60902..cb297662a4e 100644 --- a/Flow.Launcher/VimMode/README.md +++ b/Flow.Launcher/VimMode/README.md @@ -58,7 +58,7 @@ Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineT - `gu` / `gU` — lowercase / uppercase (operator + motion, e.g. `guw`) - `dd` / `cc` — delete / change the whole query - `D` / `C` — delete / change from the cursor to the end -- `Y` — yank the whole query · `p` — paste after the cursor +- `Y` — yank the whole query · `p` / `P` — paste after / before the cursor - `u` — undo the last operation · `Ctrl+R` — redo ### Operators + text objects diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index d4d4c3ac04c..bc46930f831 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -536,7 +536,10 @@ private bool HandleVimKey(KeyEventArgs e) return false; case Key.P: - PasteAfterCursor(GetCount()); + if (modifiers.HasFlag(ModifierKeys.Shift)) + PasteBeforeCursor(GetCount()); // P: paste before the cursor + else + PasteAfterCursor(GetCount()); // p: paste after the cursor return true; case Key.U: VimUndo(); @@ -1148,6 +1151,29 @@ private void PasteAfterCursor(int count) catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard paste operation failed", ex); } } + /// + /// Pastes the clipboard text before the cursor (Vim 'P'), times, + /// leaving the caret on the last pasted character. Records the change for '.' repeat. + /// + private void PasteBeforeCursor(int count) + { + try + { + string clip = Clipboard.GetText(); + if (string.IsNullOrEmpty(clip)) return; + if (count < 1) count = 1; + + string pasted = clip; + for (int i = 1; i < count; i++) pasted += clip; + + int c = _queryTextBox.CaretIndex; // P inserts at the caret (before it) + SetText(_queryTextBox.Text.Insert(c, pasted)); + _queryTextBox.CaretIndex = Math.Min(c + pasted.Length - 1, Math.Max(0, _queryTextBox.Text.Length - 1)); + _lastChange = "p"; + } + catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "Clipboard paste operation failed", ex); } + } + private void ExecuteCharCommand(string cmd, char c) { _awaitingCharCommand = ""; From af6097a4f08d19e984fff2409b6c6fe8c2bca72e Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 17:36:58 -0500 Subject: [PATCH 27/30] Vim: fix Insert-mode undo, counted dot-repeat, and quote text-object at EOL Addresses three issues from code review: - `u` now reverts the whole Insert session. Entering Insert via i/I/a/A pushes an undo snapshot first, so after `i``` pressing `u` removes the typed text (change-operators already snapshot via SetText, so they are unaffected and need no second snapshot). - `.` after a counted `x`/`r` repeats the count. `r` now stores its length and RepeatLastChange falls back to the stored count for `x`/`r` instead of defaulting to 1, so `3x`/`3rx` then `.` repeat 3 chars. - `ci"`/`di"` work at end-of-query. TextObjectQuote clamps the caret to the last index (CaretIndex can equal text.Length), so a quoted region ending the query still matches. Covered by new unit-test cases. --- Flow.Launcher.Test/VimMotionEngineTest.cs | 4 ++++ Flow.Launcher/VimMode/VimManager.cs | 17 ++++++++++++++--- Flow.Launcher/VimMode/VimMotionEngine.cs | 4 ++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index 5589a33cbe2..6d3f740e85a 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -161,6 +161,10 @@ public void TextObjectQuoteTest() Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\" now", 5, '"', false), Is.EqualTo((5, 6))); Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\" now", 5, '"', true), Is.EqualTo((4, 7))); Assert.That(VimMotionEngine.TextObjectQuote("hello", 1, '"', false), Is.EqualTo((-1, -1))); + // caret past the last char (CaretIndex == text.Length) clamps onto the closing quote + Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\"", 8, '"', false), Is.EqualTo((5, 6))); + Assert.That(VimMotionEngine.TextObjectQuote("say \"hi\"", 8, '"', true), Is.EqualTo((4, 7))); + Assert.That(VimMotionEngine.TextObjectQuote("", 0, '"', false), Is.EqualTo((-1, -1))); } [Test] diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index bc46930f831..24d88dd48a1 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -590,13 +590,16 @@ private bool HandleVimKey(KeyEventArgs e) _awaitingTextObject = "i"; return true; case Key.I when modifiers == ModifierKeys.None && _pendingCommand == "": + PushUndo(); // snapshot pre-insert state so `u` reverts the whole insert session _vimEngine.SwitchToInsert(); return true; case Key.I when modifiers.HasFlag(ModifierKeys.Shift): + PushUndo(); _vimEngine.SwitchToInsert(); _queryTextBox.CaretIndex = 0; return true; case Key.A when modifiers == ModifierKeys.None && _pendingCommand == "": + PushUndo(); _vimEngine.SwitchToInsert(); if (_queryTextBox.CaretIndex < _queryTextBox.Text.Length) { @@ -604,6 +607,7 @@ private bool HandleVimKey(KeyEventArgs e) } return true; case Key.A when modifiers.HasFlag(ModifierKeys.Shift): + PushUndo(); _vimEngine.SwitchToInsert(); _queryTextBox.CaretIndex = _queryTextBox.Text.Length; return true; @@ -1051,7 +1055,8 @@ private void RepeatLastChange() break; case "x": { - int n = GetCount(); + int n = _count > 0 ? GetCount() : _lastChangeLen; // bare `.` repeats the original count + if (n < 1) n = 1; int c = _queryTextBox.CaretIndex; int len = Math.Min(n, _queryTextBox.Text.Length - c); if (len > 0) @@ -1116,11 +1121,16 @@ private void RepeatLastChange() break; case "r": { + int n = _count > 0 ? GetCount() : _lastChangeLen; // bare `.` repeats the original count + if (n < 1) n = 1; int c = _queryTextBox.CaretIndex; if (_lastReplaceChar != '\0' && c < _queryTextBox.Text.Length) { - SetText(_queryTextBox.Text.Remove(c, 1).Insert(c, _lastReplaceChar.ToString())); - _queryTextBox.CaretIndex = c; + int len = Math.Min(n, _queryTextBox.Text.Length - c); + char[] chars = _queryTextBox.Text.ToCharArray(); + for (int i = c; i < c + len; i++) chars[i] = _lastReplaceChar; + SetText(new string(chars)); + _queryTextBox.CaretIndex = c + len - 1; } } break; @@ -1211,6 +1221,7 @@ private void ExecuteCharCommand(string cmd, char c) SetText(new string(chars)); _queryTextBox.CaretIndex = caret + len - 1; _lastChange = "r"; + _lastChangeLen = len; // so a bare `.` repeats the counted replace _lastReplaceChar = c; } } diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index c1cd6a642f9..b7bfc65530d 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -412,6 +412,10 @@ public static (int start, int end) TextObjectDelimited(string text, int caret, c /// A tuple containing the start and end indices, or (-1, -1) if invalid. public static (int start, int end) TextObjectQuote(string text, int caret, char quote, bool around) { + if (text.Length == 0) return (-1, -1); + // CaretIndex can be text.Length (caret past the last char); clamp so end-of-query + // resolves onto the closing quote and ci"/di" still match the last quoted region. + caret = Math.Max(0, Math.Min(caret, text.Length - 1)); int first = -1; for (int i = 0; i < text.Length; i++) From 2ad946811528874082cd66cb1ebc49b0a89eae99 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 19:07:51 -0500 Subject: [PATCH 28/30] Vim: flash a highlight over yanked text as confirmation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Yank operations (y{motion}, yiw/yi", yy, Y, and Visual-mode y) now briefly flash an accent highlight over the copied text, like Neovim's on-yank highlight, so you get visual confirmation the yank happened. Drawn on a VimYankFlash canvas overlay (one rectangle per line of the range) and faded out over 300ms; purely visual — it never touches the text, caret, or selection, and only fires on pure yanks, never on cuts (x/d/c/s). --- Flow.Launcher/MainWindow.xaml | 5 ++ Flow.Launcher/VimMode/README.md | 1 + Flow.Launcher/VimMode/VimManager.cs | 85 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index dcfde657a69..0abdbff3a82 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -350,6 +350,11 @@ X2="0" Y1="0" Y2="0" /> + + /// Briefly highlights a just-yanked text range (like Neovim's on-yank flash) so the user gets + /// visual confirmation the yank happened. Draws one accent rectangle per line of the range on the + /// VimYankFlash canvas overlay and fades it out. Purely visual — does not touch text, caret, or + /// selection. Only call this for pure yanks (y/Y/yy/visual y), never for cuts (x/d/c/s). + ///
+ private void FlashYank(int start, int length) + { + if (length <= 0) return; + if (_vimYankFlash == null) + _vimYankFlash = _mainWindow.FindName("VimYankFlash") as System.Windows.Controls.Canvas; + if (_vimYankFlash == null) return; + + try + { + string text = _queryTextBox.Text; + if (start < 0 || start >= text.Length) return; + int end = Math.Min(start + length, text.Length); // exclusive + + _vimYankFlash.Children.Clear(); + var m = _queryTextBox.Margin; + var fill = (System.Windows.Media.Brush)Application.Current.TryFindResource("BasicSystemAccentColor") + ?? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 120, 215)); + + // Split the range into per-line segments (at '\n') and draw a rectangle for each, so a + // multi-line yank highlights every line rather than a single oversized box. + int segStart = start; + for (int i = start; i <= end; i++) + { + bool atBreak = i == end || (i < text.Length && text[i] == '\n'); + if (!atBreak) continue; + if (i > segStart) AddFlashRect(segStart, i, m, fill); + segStart = i + 1; // skip the newline + } + if (_vimYankFlash.Children.Count == 0) return; + + _yankFlashToken++; + int token = _yankFlashToken; + _vimYankFlash.Visibility = Visibility.Visible; + var anim = new System.Windows.Media.Animation.DoubleAnimation(1.0, 0.0, new Duration(TimeSpan.FromMilliseconds(300))); + anim.Completed += (_, __) => + { + if (_yankFlashToken != token) return; // a newer flash superseded this one + _vimYankFlash.Children.Clear(); + _vimYankFlash.Visibility = Visibility.Collapsed; + }; + _vimYankFlash.BeginAnimation(UIElement.OpacityProperty, anim); + } + catch (Exception ex) { Flow.Launcher.Infrastructure.Logger.Log.Exception("VimManager", "FlashYank layout exception", ex); } + } + + private void AddFlashRect(int segStart, int segEnd, Thickness m, System.Windows.Media.Brush fill) + { + var r1 = _queryTextBox.GetRectFromCharacterIndex(segStart); + var r2 = _queryTextBox.GetRectFromCharacterIndex(segEnd); // leading edge of the char past the segment + if (r1.IsEmpty || r2.IsEmpty) return; + + double left = r1.Left + m.Left; + double top = Math.Min(r1.Top, r2.Top) + m.Top; + double width = Math.Max(r2.Left - r1.Left, 2); + double height = Math.Max(r1.Bottom, r2.Bottom) - Math.Min(r1.Top, r2.Top); + + var rect = new System.Windows.Shapes.Rectangle + { + Width = width, + Height = height, + Fill = fill, + Opacity = 0.35, + RadiusX = 2, + RadiusY = 2, + IsHitTestVisible = false, + }; + System.Windows.Controls.Canvas.SetLeft(rect, left); + System.Windows.Controls.Canvas.SetTop(rect, top); + _vimYankFlash.Children.Add(rect); + } + private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == nameof(MainViewModel.MainWindowVisibilityStatus)) @@ -561,6 +641,7 @@ private bool HandleVimKey(KeyEventArgs e) if (!string.IsNullOrEmpty(_queryTextBox.Text)) { SetClipboardText(_queryTextBox.Text); + if (cmd == "y") FlashYank(0, _queryTextBox.Text.Length); } if (cmd == "d" || cmd == "c") { @@ -742,6 +823,7 @@ private bool HandleVimKey(KeyEventArgs e) if (selLength > 0) { SetClipboardText(_queryTextBox.Text.Substring(selStart, selLength)); + FlashYank(selStart, selLength); } _queryTextBox.CaretIndex = selStart; _queryTextBox.SelectionLength = 0; @@ -813,6 +895,7 @@ private bool HandleVimKey(KeyEventArgs e) return true; case Key.Y: SetClipboardText(_queryTextBox.Text); + FlashYank(0, _queryTextBox.Text.Length); _queryTextBox.CaretIndex = 0; _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToNormal(); @@ -978,6 +1061,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) SetText(text.Remove(range.start, len)); _queryTextBox.CaretIndex = range.start; } + else FlashYank(range.start, len); if (_pendingCommand == "c") _vimEngine.SwitchToInsert(); } @@ -1276,6 +1360,7 @@ private void ExecuteMotion(int targetCaret, MotionInclusivity inclusivity = Moti SetText(_queryTextBox.Text.Remove(start, end - start)); _queryTextBox.CaretIndex = start; } + else FlashYank(start, end - start); } if (_pendingCommand == "c") From f860288b452a6f35866e77089949f2833243acc3 Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Sat, 20 Jun 2026 19:45:42 -0500 Subject: [PATCH 29/30] Vim: add gg/G, Ctrl-A/Ctrl-X, ib/aB aliases, and counted text objects - gg / G jump to the start / end of the query (Normal and Visual; work as operator targets, e.g. dgg / dG). - Ctrl-A / Ctrl-X increment / decrement the number at or after the cursor, count-aware (5 Ctrl-A adds 5), Normal mode only so Insert-mode select-all is unaffected. Pure logic in VimMotionEngine.ChangeNumber (+ unit tests). - ib/ab and iB/aB block-object aliases for i(/a( and i{/a{. - Counts on word text objects: 2daw / 3iw extend through additional words. --- Flow.Launcher.Test/VimMotionEngineTest.cs | 12 +++++ Flow.Launcher/VimMode/README.md | 8 ++-- Flow.Launcher/VimMode/VimManager.cs | 53 ++++++++++++++++++++--- Flow.Launcher/VimMode/VimMotionEngine.cs | 31 +++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher.Test/VimMotionEngineTest.cs b/Flow.Launcher.Test/VimMotionEngineTest.cs index 6d3f740e85a..3b06b297fd2 100644 --- a/Flow.Launcher.Test/VimMotionEngineTest.cs +++ b/Flow.Launcher.Test/VimMotionEngineTest.cs @@ -167,6 +167,18 @@ public void TextObjectQuoteTest() Assert.That(VimMotionEngine.TextObjectQuote("", 0, '"', false), Is.EqualTo((-1, -1))); } + [Test] + public void ChangeNumberTest() + { + // Ctrl-A / Ctrl-X on the number at or to the right of the caret. + Assert.That(VimMotionEngine.ChangeNumber("port 8080", 0, 1), Is.EqualTo((true, "port 8081", 8))); + Assert.That(VimMotionEngine.ChangeNumber("v1", 0, 1), Is.EqualTo((true, "v2", 1))); + Assert.That(VimMotionEngine.ChangeNumber("9", 0, 1), Is.EqualTo((true, "10", 1))); + Assert.That(VimMotionEngine.ChangeNumber("5", 0, -1), Is.EqualTo((true, "4", 0))); + Assert.That(VimMotionEngine.ChangeNumber("count -5 x", 0, 1), Is.EqualTo((true, "count -4 x", 7))); + Assert.That(VimMotionEngine.ChangeNumber("abc", 0, 1).found, Is.False); + } + [Test] public void TextObjectDelimitedTest() { diff --git a/Flow.Launcher/VimMode/README.md b/Flow.Launcher/VimMode/README.md index ca942db7fd5..ae691950765 100644 --- a/Flow.Launcher/VimMode/README.md +++ b/Flow.Launcher/VimMode/README.md @@ -38,6 +38,7 @@ Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineT - `h` / `l` — move left / right - `j` / `k` — move down / up through the search results - `0` — start of the query · `^` — first non-blank · `$` — end of the query +- `gg` / `G` — start / end of the query ### Word motions - `w` / `W` — start of next word / WORD @@ -55,6 +56,7 @@ Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineT - `s` / `S` — substitute the character / whole query, then enter Insert - `r{char}` — replace the character(s) under the cursor with `{char}` - `~` — toggle the case of the character under the cursor +- `Ctrl+A` / `Ctrl+X` — increment / decrement the number at or after the cursor (count-aware) - `gu` / `gU` — lowercase / uppercase (operator + motion, e.g. `guw`) - `dd` / `cc` — delete / change the whole query - `D` / `C` — delete / change from the cursor to the end @@ -65,12 +67,12 @@ Unit tests live in [`Flow.Launcher.Test`](../../Flow.Launcher.Test) (`VimEngineT ### Operators + text objects Use a text object after an operator (`d`, `c`, `y`, `gu`, …): - modifiers: `i` (inner), `a` (around) -- targets: `w` (word), `"` `'` (quotes), `(` `[` `{` (brackets) -- examples: `diw` (delete inner word), `ci"` (change inside quotes), `ya(` (yank around parens) +- targets: `w` (word), `"` `'` (quotes), `(` `[` `{` (brackets); `b` = `(` and `B` = `{` aliases +- examples: `diw` (delete inner word), `ci"` (change inside quotes), `ya(` / `yab` (yank around parens) ### Repeat & counts - `.` — repeat the last change (e.g. `x`, `dw`, `r{char}`, `p`). -- `{count}` — most motions and operators take a numeric prefix: `3w`, `5x`, `2p`, `d3w` / `3dw`, `3f,`, `2;`. +- `{count}` — most motions and operators take a numeric prefix: `3w`, `5x`, `2p`, `d3w` / `3dw`, `2daw`, `3f,`, `2;`. ### Mode switches - `i` / `I` — insert at the cursor / start of the query diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index e134810fbfb..bc507477ec7 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -352,6 +352,22 @@ public bool HandlePreviewKeyDown(KeyEventArgs e) return true; } + // Ctrl-A / Ctrl-X increment / decrement the number at or after the cursor (count-aware). + // Normal mode only, so Insert-mode Ctrl-A (select all) is unaffected. + if (modifiers.HasFlag(ModifierKeys.Control) && !modifiers.HasFlag(ModifierKeys.Alt) + && _vimEngine.CurrentMode == VimModeType.Normal && (e.Key == Key.A || e.Key == Key.X)) + { + int delta = (e.Key == Key.A ? 1 : -1) * Math.Max(1, GetCount()); + var (found, newText, newCaret) = VimMotionEngine.ChangeNumber(_queryTextBox.Text, _queryTextBox.CaretIndex, delta); + if (found) + { + SetText(newText); + _queryTextBox.CaretIndex = Math.Min(newCaret, newText.Length); + } + e.Handled = true; + return true; + } + if (modifiers.HasFlag(ModifierKeys.Control) || modifiers.HasFlag(ModifierKeys.Alt)) { return false; @@ -504,11 +520,11 @@ private bool HandleVimKey(KeyEventArgs e) ExecuteMotion(VimMotionEngine.MoveToMatchingBracket(_queryTextBox.Text, _queryTextBox.CaretIndex), MotionInclusivity.InclusivePair); return true; case Key.G: - // 'g' is the prefix for multi-key commands (gu, gU, g~, gv, g_). - if (modifiers == ModifierKeys.None) - { + // 'g' is the prefix for multi-key commands (gu, gU, g~, gv, g_); 'G' jumps to the end. + if (modifiers.HasFlag(ModifierKeys.Shift)) + ExecuteMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + else _gPending = true; - } return true; case Key.D0: if (modifiers.HasFlag(ModifierKeys.Shift)) return false; // Handle ')' normally or ignore @@ -729,6 +745,9 @@ private bool HandleVimKey(KeyEventArgs e) // 'g' prefix in Visual mode (e.g. gu / gU on the selection). _gPending = true; return true; + case Key.G when modifiers.HasFlag(ModifierKeys.Shift): + ExecuteVisualMotion(VimMotionEngine.MoveEndOfLine(_queryTextBox.Text.Length)); + return true; case Key.H: ExecuteVisualMotion(ApplyCountMove(i => VimMotionEngine.MoveLeft(i))); return true; @@ -947,6 +966,9 @@ private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) case VimModeType.Normal: switch (e.Key) { + case Key.G: + ExecuteMotion(VimMotionEngine.MoveStartOfLine()); // gg -> start of the query + return true; case Key.OemMinus when modifiers.HasFlag(ModifierKeys.Shift): ExecuteMotion(VimMotionEngine.MoveLastNonBlank(_queryTextBox.Text)); return true; @@ -977,6 +999,9 @@ private bool HandleGKey(KeyEventArgs e, ModifierKeys modifiers) case VimModeType.Visual: switch (e.Key) { + case Key.G: + ExecuteVisualMotion(VimMotionEngine.MoveStartOfLine()); // gg -> start of the query + return true; case Key.U when modifiers == ModifierKeys.None: ChangeSelectionCase(toUpper: false); return true; @@ -1005,8 +1030,10 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) _awaitingTextObject = ""; char delim = GetCharFromKey(e.Key, modifiers); - if (delim == '\0' && e.Key != Key.W) return true; + if (delim == '\0' && e.Key != Key.W && e.Key != Key.B) return true; if (e.Key == Key.W) delim = 'w'; + // Vim block-object aliases: ib/ab == i(/a( and iB/aB == i{/a{ + if (e.Key == Key.B) delim = modifiers.HasFlag(ModifierKeys.Shift) ? 'B' : 'b'; string text = _queryTextBox.Text; int caret = (_vimEngine.CurrentMode == VimModeType.Visual) ? _visualCaret : _queryTextBox.CaretIndex; @@ -1026,6 +1053,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) break; case '(': case ')': + case 'b': range = VimMotionEngine.TextObjectDelimited(text, caret, '(', ')', around); break; case '[': @@ -1034,6 +1062,7 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) break; case '{': case '}': + case 'B': range = VimMotionEngine.TextObjectDelimited(text, caret, '{', '}', around); break; default: @@ -1042,6 +1071,20 @@ private bool HandleTextObjectKey(KeyEventArgs e, ModifierKeys modifiers) if (range.start < 0) return true; + // Counted word text objects (2aw / 3iw) extend the range through additional words. + int toCount = Math.Max(1, GetCount()); + if (toCount > 1 && delim == 'w') + { + int wEnd = range.end; + for (int k = 1; k < toCount; k++) + { + var next = VimMotionEngine.TextObjectWord(text, wEnd + 1, around); + if (next.start < 0 || next.end <= wEnd) break; + wEnd = next.end; + } + range.end = wEnd; + } + if (_vimEngine.CurrentMode == VimModeType.Visual) { _visualAnchor = range.start; diff --git a/Flow.Launcher/VimMode/VimMotionEngine.cs b/Flow.Launcher/VimMode/VimMotionEngine.cs index b7bfc65530d..acbd8718fa8 100644 --- a/Flow.Launcher/VimMode/VimMotionEngine.cs +++ b/Flow.Launcher/VimMode/VimMotionEngine.cs @@ -483,5 +483,36 @@ public static int FindCharBackward(string text, int caret, char target, bool til if (index == -1) return caret; return till ? index + 1 : index; } + + /// + /// Vim Ctrl-A / Ctrl-X: finds the (decimal, optionally negative) number at or to the right of the + /// caret and adds to it. Pure: returns whether a number was found, the new + /// text, and the caret position (on the last digit of the result), matching Vim's behaviour. + /// + public static (bool found, string newText, int newCaret) ChangeNumber(string text, int caret, int delta) + { + if (string.IsNullOrEmpty(text)) return (false, text, caret); + int n = text.Length; + int p = Math.Min(Math.Max(caret, 0), n); + + // Scan right (within the current line) for the first digit. + while (p < n && text[p] != '\n' && !char.IsDigit(text[p])) p++; + if (p >= n || text[p] == '\n') return (false, text, caret); + + // Expand to the full digit run, then absorb a leading '-' sign. + int start = p; + while (start > 0 && char.IsDigit(text[start - 1])) start--; + int end = p; + while (end < n && char.IsDigit(text[end])) end++; + if (start > 0 && text[start - 1] == '-') start--; + + if (!long.TryParse(text.Substring(start, end - start), out long value)) + return (false, text, caret); + + string replacement = (value + delta).ToString(); + string newText = text.Substring(0, start) + replacement + text.Substring(end); + int newCaret = start + replacement.Length - 1; // cursor lands on the last digit + return (true, newText, newCaret); + } } } From d3205861fcf9ef555fc9ed7d54542831219f7f7e Mon Sep 17 00:00:00 2001 From: grey <***@***.***> Date: Tue, 23 Jun 2026 02:12:18 -0500 Subject: [PATCH 30/30] Vim: select the restored query on reopen when dismissed in Normal mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On show, the non-Insert branch cleared the selection before switching to Insert, wiping the select-all the launcher sets up on reopen — so dismissing in Normal mode reopened with an unselected query. Switch to Insert then SelectAll, matching the Insert-mode behaviour, so the first keystroke replaces. --- Flow.Launcher/VimMode/VimManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/VimMode/VimManager.cs b/Flow.Launcher/VimMode/VimManager.cs index bc507477ec7..64237c65f62 100644 --- a/Flow.Launcher/VimMode/VimManager.cs +++ b/Flow.Launcher/VimMode/VimManager.cs @@ -255,8 +255,8 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop { if (_vimEngine.CurrentMode != VimModeType.Insert) { - _queryTextBox.SelectionLength = 0; _vimEngine.SwitchToInsert(); + _queryTextBox.SelectAll(); // select the restored query so the first keystroke replaces it (match Insert) } })); }