Repository: microsoft/terminal (language: cpp)

Issue:
Search behavior in Terminal -- add capability to insert the matched hit
# Allow Search functionality to easily expand found text, and insert the expanded found text at current cursor location

as an example take a look at this Kubernetes example:

```
╭─root@nonprod /home/nonprod
╰─# k get po -n dev | grep notifications
oom-ent-notifications-1-0-775c6fb9dc-n476x                1/1     Running   3          71d
╭─root@nonprod /home/nonprod
╰─#

```

The system listed a container name - a complex generated name.  I need the full container name to delete the container, for instance, what I want is to execute the following

```sh
k delete po -n dev oom-ent-notifications-1-0-775c6fb9dc-n476x
```

Now the problem is -- how do I get the long name in?  The mac iterm2 solves this by allowing a search
1. User hits `ctrl+f` (or `ctrl+shift+f`)
2. User types in `oom-ent`
3. System matches `oom-ent`  (below the line ending in grep notifications)
4. User presses `Tab`
5. System highlights `oom-ent-notifications-1-0-775c6fb9dc-n476x` AND copies it to clipboard
6. User types `k delete po -n dev` [`Ctrl+V`]
7. System displays `k delete po -n dev oom-ent-notifications-1-0-775c6fb9dc-n476x`
8. User hits `enter` to execute the command.
Shift-Tab should work to select up to the beginning of the word.  Shift-Tab followed by Tab should select the whole word if letters in the middle of the word are matched.

Allowing this functionality is super-useful.
F.



Discussion / hints:
Out of curiosity, is there any terminal out there that actually has this particular feature?
Yes.  Mac iTerm2 has that feature.

F.

So, I'll throw this on the Search v2 backlog, but we'll need to definitely think about how this will work exactly. Right now, <kbd>tab</kbd> in the find dialog will move the focus to the "Find Up" button. We'd need to be able to offer some sort of configurable way of intercepting that and performing some other action - in this case, something like "expandSelection(word)".

~~Presumably, in your example, if the user had searched for `ent-not`, that would expand in both directions, to select the entire `oom-ent-notifications-1-0-775c6fb9dc-n476x` word.~~ Nevermind, just read the following:

>  <kbd>Shift-Tab</kbd> should work to select up to the beginning of the word. <kbd>Shift-Tab</kbd> followed by <kbd>Tab</kbd> should select the whole word if letters in the middle of the word are matched.

So idk if we have actions for both of those spec'd in #2840, I'd have to check

Then, we'd need to figure out how this plays with copy on select - I'm thinking that we'd probably _not_ have it auto-copy (unless `copyOnSelect` was enabled, and instead require the user to manually press the `copy` keybinding.



The specification of this feature needs to be way more detailed than my
feature request.  I can put in an effort in doing this on 2 conditions:
1. Someone takes the time to properly review/discuss the spec with me.
2. This feature request does not end in an endless backlog, and actually
gets into a released product within 1 year.

Otherwise, I see no point on working on a spec for something that ends up
in a drawer.  Depending on the language the terminal is written in, and if
it's open source, I might also contribute.

Best,
F.

I would _love_ to help you write a spec 😃

Right now, we've got a spec template [over here](https://github.com/microsoft/terminal/blob/main/doc/specs/spec-template.md). I'm gonna level with you, I don't _love_ the template, and I'm working on updating it now, but it's certainly a good enough starting point. I'd think that the "Capabilities" header should definitely be renamed to "Concerns" or something similar. There's a bunch of other completed specs in [the same folder](https://github.com/microsoft/terminal/tree/main/doc/specs), with a variety of levels of detail ([less](https://github.com/microsoft/terminal/blob/main/doc/specs/%231337%20-%20Per-Profile%20Tab%20Colors/%231337%20-%20Per-Profile%20Tab%20Colors.md), [more](https://github.com/microsoft/terminal/blob/main/doc/specs/%231502%20-%20Advanced%20Tab%20Switcher/spec.md), [most](https://github.com/microsoft/terminal/blob/dev/migrie/s/5000/doc/specs/%235000%20-%20Process%20Model%202.0/%235000%20-%20Process%20Model%202.0.md)). Those can be a good reference for what we're looking for. 

As far as details go, I'm a _little_ worried that this feature might run up against the "Keyboard Selection Spec" over in #2840. I'm pretty sure that spec is actually in its final form and has all the details that the team discussed, but we're mostly just backed up a _little_ bit bureaucratically on that on. So I think that's fine to use as a base to build off of.

I'd also keep an eye on the fact that currently, the TermControl itself is the thing responsible for handling the key events to try and dispatch an action. If we wanted to have the keybinding for this action configurable, then we'd need some way of identifying that the action is being dispatched from the find dialog, not the terminal control. If you can describe the UX well enough, we can almost certainly help figure the plumbing out.

I'm super happy to keep pushing on the discussion and keep bumping the review as much as possible, though I'd just also warn you that as we're heading into the end of the year, there are a lot more holidays and vacation days used, so our response times might be a little slower than usual.

The codebase is all C++ (more specifically, C++/WinRT). If you're interested in poking around the code, [this directory](https://github.com/microsoft/terminal/tree/dev%2Fmigrie%2Fs%2F5000/src/cascadia/TerminalControl) is probably the most relevant to the discussion.
Thank you for your detailed answer with the pointers to example specs.  Let
me see if I can come up with something spec-wise, but it may take some time.

I am an active developer by profession, but have not touched  C++ in many,
many years, favoring C# development since 2006 (.net 2.0) or so.  Now all
the stuff I do is .net 5 cross platform, asp.net, ef etc. , ruby, python,
bash and aws devops.  I do have extensive WPF desktop experience but have
never touched WinRT (windows stores apps).  I'll have a quick look at the
code when I have some time.

So for the spec, you assigned issue #8274 - should I just
1. Fork the repo
2. Clone my fork with branch main
3. Create a new branch dev/brunzefb/spec/insert-matched-search from main
4. Add a new file [root]/doc/specs/#8274 - Insert Matched Search
Hit/spec.md (the spec .md starting from the template and all the .png files
going into the folder?
5. Do the spec writing
6. Commit the branch, push to my fork and issue a PR back (my branch ->main) to the project?

Then ask for a review on the original thread?

If you can point me to other docs that specify how you work, or code of
conduct, pls send me links.

Best,
Friedrich

Take however long you need! If you only want to work on the spec, that's perfectly fine too. C++/WinRT is a bit of a weird hybrid of C++ and C#, and it can be a little tricky to get things exactly right. It's really nice once you've gotten the hang of it, but it can have some really nasty edge cases. 

As far as the process goes, what you've outlined is great. I think we're still trying to find the best way to handle community spec contributions. You'll probably get a couple reviews from the team after submitting the PR, and we tend to hold specs to a 3-approval minimum, to ensure there's a broader consensus across the team. We usually have team syncs on Mondays, so we might have additional feedback for you the Monday _after_ the PR is submitted.

I think technically our code of conduct is [here](https://github.com/microsoft/terminal/blob/main/CODE_OF_CONDUCT.md), but we operate pretty informally. If you follow the unofficial COC of "don't be an asshole", then you'll get along just fine 😄 
An idle thought: We've got mark mode now. Maybe we could have the find dialog trigger a mark mode selection, use the dialog to get to the hit you want, then hit `esc` to dismiss the dialog. At this point, you've got part of a word selected. Maybe we could bind `tab`[1] here to "expand selection to word". Then you could `ctrl+c` to copy it to the clipboard and `ctrl+v` to paste.

/cc @carlos-zamora to run my crazy idea past you

[1]: I'm aware that as of 1.16 `tab` in mark mode jumps to the next hyperlink. We didn't really make that rebindable. Maybe we go with `ctrl+space` for expandToWord? I know that's what Sublime does by default.
> An idle thought: We've got mark mode now. Maybe we could have the find dialog trigger a mark mode selection, use the dialog to get to the hit you want, then hit `esc` to dismiss the dialog. At this point, you've got part of a word selected. Maybe we could bind `tab`[1] here to "expand selection to word". Then you could `ctrl+c` to copy it to the clipboard and `ctrl+v` to paste.
> 
> [1]: I'm aware that as of 1.16 `tab` in mark mode jumps to the next hyperlink. We didn't really make that rebindable. Maybe we go with `ctrl+space` for expandToWord? I know that's what Sublime does by default.

Hmm... I don't like making `ctrl+space` expandToWord. It feels unintuitive imo. But I can't think of an intuitive default key binding. So for that reason, I think it shouldn't be bound to mark mode.

That said, I'll meet you halfway. What if we did this...
- Setup:
	- (new work) introduce `expandToWord()` action. Unbound by default. Operates like "copy" as in it _only_ expands the selection when a selection is present (otherwise the key chord just goes through to the terminal).
	- User binds `tab` to `multipleActions([ expandToWord(), copy(), paste() ])` action. This means that tab will expand the selection to encompass the current word, copy the selection, and immediately paste it.
	- We _do not_ make "Find" enter mark mode (this is the behavior today, I just want to highlight this).
- Scenario (paraphrasing what OP said):
	- User hits ctrl+shift+f to search text
	- User types in `oom-ent`
	- System matches `oom-ent`
	- User presses <kbd>Tab</kbd> --> this executes the multiple actions introduced in the setup, which then highlights `oom-ent-notifications-1-0-775c6fb9dc-n476x`, copies it, and immediately pastes it (NOTE: if you don't want to paste it, we can just edit the `multipleActions()` in setup)
	- Done

tldr: add an `expandToWord()` action and if the user binds it to <kbd>Tab</kbd>, tab works as expected. Just don't bind it by default (but heck, that's even debatable tbh since it won't interfere with mark mode's tab behavior)

@zadjii-msft shipit? This is a pretty easy task tbh. TerminalSelection already has logic to expand by word. We'd just expand "start" to the beginning of the word and "end" to the end of the word haha.
I don't hate it. `tab` seems like a dangerous keybinding for this (or anything), just cause like... I think even in the find dialog, we check keybindings (so binding `tab` to something predictably prevents tab navigation of the UI)[1]. BUT I think otherwise, this should all work as expected, yea?

We may want to sanity check something like returning `false` out of a `multipleActions` action - do we keep going with the rest of the actions? We _do_. (Maybe we shouldn't?). But that's an aside. 

I'd say :shipit:, and I'll let @brunzefb ACK if this idea would work for them

[1]: this is a mild fact, I haven't actually verified it.
Oh. Minor correction for what I said above. We should escape the find dialog first. _Then_ press tab. That way, tab is bound to something.

Hoping to write `expandToWord()` in my spare time soon. Just for fun ;)

Fix patch:
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index 9a502c422b8..311ad53a79a 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -315,6 +315,7 @@
         "commandPalette",
         "copy",
         "duplicateTab",
+        "expandSelectionToWord",
         "exportBuffer",
         "find",
         "findMatch",
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 81fc005ea3b..23960046f49 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1156,4 +1156,14 @@ namespace winrt::TerminalApp::implementation
             args.Handled(handled);
         }
     }
+
+    void TerminalPage::_HandleExpandSelectionToWord(const IInspectable& /*sender*/,
+                                                    const ActionEventArgs& args)
+    {
+        if (const auto& control{ _GetActiveControl() })
+        {
+            const auto handled = control.ExpandSelectionToWord();
+            args.Handled(handled);
+        }
+    }
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 28fc6ae5154..fdf5d082fc8 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -1147,6 +1147,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return false;
     }
 
+    bool ControlCore::ExpandSelectionToWord()
+    {
+        if (_terminal->IsSelectionActive())
+        {
+            _terminal->ExpandSelectionToWord();
+            _updateSelectionUI();
+            return true;
+        }
+        return false;
+    }
+
     // Method Description:
     // - Pre-process text pasted (presumably from the clipboard)
     //   before sending it over the terminal's connection.
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 383d415c430..f9a6297bd57 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -87,6 +87,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void ToggleMarkMode();
         Control::SelectionInteractionMode SelectionMode() const;
         bool SwitchSelectionEndpoint();
+        bool ExpandSelectionToWord();
         bool TryMarkModeKeybinding(const WORD vkey,
                                    const ::Microsoft::Terminal::Core::ControlKeyStates modifiers);
 
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index c52c4d4b410..cb61cd2a3af 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -91,6 +91,7 @@ namespace Microsoft.Terminal.Control
         Boolean ToggleBlockSelection();
         void ToggleMarkMode();
         Boolean SwitchSelectionEndpoint();
+        Boolean ExpandSelectionToWord();
         void ClearBuffer(ClearBufferType clearType);
 
         void SetHoveredCell(Microsoft.Terminal.Core.Point terminalPosition);
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index e053fd10720..d8f25cace1d 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -1981,6 +1981,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _core.SwitchSelectionEndpoint();
     }
 
+    bool TermControl::ExpandSelectionToWord()
+    {
+        return _core.ExpandSelectionToWord();
+    }
+
     void TermControl::Close()
     {
         if (!_IsClosing())
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 4e5891aa66d..34ac6dc3520 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -41,6 +41,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         bool ToggleBlockSelection();
         void ToggleMarkMode();
         bool SwitchSelectionEndpoint();
+        bool ExpandSelectionToWord();
         void Close();
         Windows::Foundation::Size CharacterDimensions() const;
         Windows::Foundation::Size MinimumSize();
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index 101359b30e9..e4b65735ab6 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -55,6 +55,7 @@ namespace Microsoft.Terminal.Control
         Boolean ToggleBlockSelection();
         void ToggleMarkMode();
         Boolean SwitchSelectionEndpoint();
+        Boolean ExpandSelectionToWord();
         void ClearBuffer(ClearBufferType clearType);
         void Close();
         Windows.Foundation.Size CharacterDimensions { get; };
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index c0b18594e5b..5608f51db4f 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -281,6 +281,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void SelectAll();
     SelectionInteractionMode SelectionMode() const noexcept;
     void SwitchSelectionEndpoint();
+    void ExpandSelectionToWord();
     void ToggleMarkMode();
     void SelectHyperlink(const SearchDirection dir);
     bool SelectionIsTargetingUrl() const noexcept;
diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp
index 2e34e5195ba..38fa2446786 100644
--- a/src/cascadia/TerminalCore/TerminalSelection.cpp
+++ b/src/cascadia/TerminalCore/TerminalSelection.cpp
@@ -340,6 +340,16 @@ void Terminal::SwitchSelectionEndpoint()
     }
 }
 
+void Terminal::ExpandSelectionToWord()
+{
+    if (IsSelectionActive())
+    {
+        const auto& buffer = _activeBuffer();
+        _selection->start = buffer.GetWordStart(_selection->start, _wordDelimiters);
+        _selection->end = buffer.GetWordEnd(_selection->end, _wordDelimiters);
+    }
+}
+
 // Method Description:
 // - selects the next/previous hyperlink, if one is available
 // Arguments:
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index b9aaaf1aae3..10dc2b4b31f 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -84,6 +84,7 @@ static constexpr std::string_view SelectAllKey{ "selectAll" };
 static constexpr std::string_view MarkModeKey{ "markMode" };
 static constexpr std::string_view ToggleBlockSelectionKey{ "toggleBlockSelection" };
 static constexpr std::string_view SwitchSelectionEndpointKey{ "switchSelectionEndpoint" };
+static constexpr std::string_view ExpandSelectionToWordKey{ "expandSelectionToWord" };
 
 static constexpr std::string_view ActionKey{ "action" };
 
@@ -404,6 +405,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::MarkMode, RS_(L"MarkModeCommandKey") },
                 { ShortcutAction::ToggleBlockSelection, RS_(L"ToggleBlockSelectionCommandKey") },
                 { ShortcutAction::SwitchSelectionEndpoint, RS_(L"SwitchSelectionEndpointCommandKey") },
+                { ShortcutAction::ExpandSelectionToWord, RS_(L"ExpandSelectionToWordCommandKey") },
             };
         }();
 
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index 800e4352878..21c857971e8 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -97,6 +97,7 @@
     ON_ALL_ACTIONS(MarkMode)                \
     ON_ALL_ACTIONS(ToggleBlockSelection)    \
     ON_ALL_ACTIONS(SwitchSelectionEndpoint) \
+    ON_ALL_ACTIONS(ExpandSelectionToWord)   \
     ON_ALL_ACTIONS(CloseOtherPanes)
 
 #define ALL_SHORTCUT_ACTIONS_WITH_ARGS             \
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index 4f1514af722..12fac66fa82 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -567,6 +567,9 @@
   <data name="SwitchSelectionEndpointCommandKey" xml:space="preserve">
     <value>Switch selection endpoint</value>
   </data>
+  <data name="ExpandSelectionToWordCommandKey" xml:space="preserve">
+    <value>Expand selection to word</value>
+  </data>
   <data name="CloseOtherPanesCommandKey" xml:space="preserve">
     <value>Close all other panes</value>
   </data>
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 02e07a223c3..5fc079daad6 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -410,6 +410,7 @@
         { "command": "markMode", "keys": "ctrl+shift+m" },
         { "command": "toggleBlockSelection" },
         { "command": "switchSelectionEndpoint" },
+        { "command": "expandSelectionToWord" },
 
         // Scrollback
         { "command": "scrollDown", "keys": "ctrl+shift+down" },


Test patch: