| @@ -26,7 +26,10 @@ use gpui_kit::component::{ |
| 26 | 26 | use gpui_kit::prelude::FluentBuilder as _; |
| 27 | 27 | use gpui_kit::*; |
| 28 | 28 | |
| 29 | | - use crate::Interrupt; |
| 29 | + use crate::{ |
| 30 | + ComposerBranch, ComposerEffort, ComposerHost, ComposerMode, ComposerPreviousWorktree, ComposerStash, ComposerWorkspace, |
| 31 | + FindInThread, Interrupt, ToggleModelPicker, |
| 32 | + }; |
| 30 | 33 | use crate::clock; |
| 31 | 34 | use crate::git; |
| 32 | 35 | use crate::models::{Catalog, ModelEntry, SlashCommand}; |
| @@ -547,32 +550,16 @@ impl ChatView { |
| 547 | 550 | } |
| 548 | 551 | } |
| 549 | 552 | |
| 550 | | - /// T3 Code's composer shortcuts: Ctrl+Shift+M (model), E (effort), |
| 551 | | - /// A (access), G (branch), X (workspace), L (previous worktree) and |
| 552 | | - /// H (machine). |
| 553 | | - fn composer_shortcut(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) -> bool { |
| 554 | | - let keystroke = &event.keystroke; |
| 555 | | - let modifiers = &keystroke.modifiers; |
| 556 | | - if !(modifiers.control && modifiers.shift) || modifiers.alt || event.is_held { |
| 557 | | - return false; |
| 558 | | - } |
| 553 | + /// Opens (or closes) one of the composer's controls, as T3 Code's |
| 554 | + /// composer shortcuts do; a control folded into "…" opens that menu. |
| 555 | + fn open_control(&mut self, picker: Picker, window: &mut Window, cx: &mut Context<Self>) { |
| 559 | 556 | let (traits_folded, access_folded) = self.folded.get(); |
| 560 | | - let strip = self.strip_visible(cx); |
| 561 | | - let draft = self.session.read(cx).items.is_empty(); |
| 562 | | - let picker = match keystroke.key.as_str() { |
| 563 | | - "m" => Picker::Model, |
| 564 | | - "e" if traits_folded => Picker::Overflow, |
| 565 | | - "e" => Picker::Traits, |
| 566 | | - "a" if access_folded => Picker::Overflow, |
| 567 | | - "a" => Picker::Access, |
| 568 | | - "g" if strip => Picker::Branch, |
| 569 | | - "x" if strip && draft => Picker::Workspace, |
| 570 | | - "h" if strip && draft => Picker::Environment, |
| 571 | | - "l" if draft => { |
| 572 | | - self.use_previous_worktree(window, cx); |
| 573 | | - return true; |
| 574 | | - } |
| 575 | | - _ => return false, |
| 557 | + let picker = match picker { |
| 558 | + Picker::Traits if traits_folded => Picker::Overflow, |
| 559 | + Picker::Access if access_folded => Picker::Overflow, |
| 560 | + Picker::Branch | Picker::Workspace | Picker::Environment if !self.strip_visible(cx) => return, |
| 561 | + Picker::Workspace | Picker::Environment if !self.session.read(cx).items.is_empty() => return, |
| 562 | + picker => picker, |
| 576 | 563 | }; |
| 577 | 564 | self.expand(cx); |
| 578 | 565 | let open = self.open_picker != Some(picker); |
| @@ -580,7 +567,6 @@ impl ChatView { |
| 580 | 567 | if !open { |
| 581 | 568 | self.focus_composer(window, cx); |
| 582 | 569 | } |
| 583 | | - true |
| 584 | 570 | } |
| 585 | 571 | |
| 586 | 572 | /// Points a new thread at the project's previous worktree (Ctrl+Shift+L). |
| @@ -721,19 +707,7 @@ impl ChatView { |
| 721 | 707 | } |
| 722 | 708 | |
| 723 | 709 | /// 1–9 pick an option of the open question while the composer is empty. |
| 724 | | - fn pick_with_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) { |
| 725 | | - if self.composer_shortcut(event, window, cx) { |
| 726 | | - cx.stop_propagation(); |
| 727 | | - return; |
| 728 | | - } |
| 729 | | - let modifiers = &event.keystroke.modifiers; |
| 730 | | - if event.keystroke.key == "f" && modifiers.control && !modifiers.shift && !modifiers.alt { |
| 731 | | - if !self.session.read(cx).items.is_empty() && self.diff.is_none() { |
| 732 | | - cx.stop_propagation(); |
| 733 | | - self.open_find(window, cx); |
| 734 | | - } |
| 735 | | - return; |
| 736 | | - } |
| 710 | + fn pick_with_key(&mut self, event: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) { |
| 737 | 711 | let keystroke = &event.keystroke; |
| 738 | 712 | if self.diff.is_some() || keystroke.modifiers.modified() || !self.composer.read(cx).value().is_empty() { |
| 739 | 713 | return; |
| @@ -977,11 +951,7 @@ impl ChatView { |
| 977 | 951 | cx.stop_propagation(); |
| 978 | 952 | return; |
| 979 | 953 | } |
| 980 | | - if keystroke.key == "s" && modifiers.control && !modifiers.shift && !modifiers.alt { |
| 981 | | - cx.stop_propagation(); |
| 982 | | - self.stash_prompt(window, cx); |
| 983 | | - return; |
| 984 | | - } |
| 954 | + |
| 985 | 955 | if keystroke.key == "enter" && modifiers.control && modifiers.shift { |
| 986 | 956 | cx.stop_propagation(); |
| 987 | 957 | let queued = !self.session.read(cx).queue.is_empty(); |
| @@ -1144,8 +1114,10 @@ impl ChatView { |
| 1144 | 1114 | .into_any_element() |
| 1145 | 1115 | } |
| 1146 | 1116 | |
| 1147 | | - fn escape(&mut self, cx: &mut Context<Self>) { |
| 1148 | | - if self.find.take().is_some() { |
| 1117 | + fn escape(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| 1118 | + // Closing the find bar or a picker hands the keyboard back to the composer. |
| 1119 | + if self.find.take().is_some() || self.open_picker.take().is_some() { |
| 1120 | + self.focus_composer(window, cx); |
| 1149 | 1121 | cx.notify(); |
| 1150 | 1122 | return; |
| 1151 | 1123 | } |
| @@ -1159,10 +1131,6 @@ impl ChatView { |
| 1159 | 1131 | cx.notify(); |
| 1160 | 1132 | return; |
| 1161 | 1133 | } |
| 1162 | | - if self.open_picker.take().is_some() { |
| 1163 | | - cx.notify(); |
| 1164 | | - return; |
| 1165 | | - } |
| 1166 | 1134 | if self.diff.take().is_some() { |
| 1167 | 1135 | cx.notify(); |
| 1168 | 1136 | return; |
| @@ -1290,6 +1258,8 @@ impl ChatView { |
| 1290 | 1258 | } |
| 1291 | 1259 | } else if self.open_picker == Some(picker) { |
| 1292 | 1260 | self.open_picker = None; |
| 1261 | + // A picker closed with Esc or a click outside leaves no focus. |
| 1262 | + self.focus_composer(window, cx); |
| 1293 | 1263 | } |
| 1294 | 1264 | cx.notify(); |
| 1295 | 1265 | } |
| @@ -1902,8 +1872,25 @@ impl Render for ChatView { |
| 1902 | 1872 | v_flex() |
| 1903 | 1873 | .key_context("ChatView") |
| 1904 | 1874 | .size_full() |
| 1905 | | - .on_action(cx.listener(|this, _: &Interrupt, _, cx| this.escape(cx))) |
| 1906 | | - .on_action(cx.listener(|this, _: &input::Escape, _, cx| this.escape(cx))) |
| 1875 | + .on_action(cx.listener(|this, _: &Interrupt, window, cx| this.escape(window, cx))) |
| 1876 | + .on_action(cx.listener(|this, _: &ToggleModelPicker, window, cx| this.open_control(Picker::Model, window, cx))) |
| 1877 | + .on_action(cx.listener(|this, _: &ComposerEffort, window, cx| this.open_control(Picker::Traits, window, cx))) |
| 1878 | + .on_action(cx.listener(|this, _: &ComposerMode, window, cx| this.open_control(Picker::Access, window, cx))) |
| 1879 | + .on_action(cx.listener(|this, _: &ComposerBranch, window, cx| this.open_control(Picker::Branch, window, cx))) |
| 1880 | + .on_action(cx.listener(|this, _: &ComposerWorkspace, window, cx| this.open_control(Picker::Workspace, window, cx))) |
| 1881 | + .on_action(cx.listener(|this, _: &ComposerHost, window, cx| this.open_control(Picker::Environment, window, cx))) |
| 1882 | + .on_action(cx.listener(|this, _: &ComposerPreviousWorktree, window, cx| { |
| 1883 | + if this.session.read(cx).items.is_empty() { |
| 1884 | + this.use_previous_worktree(window, cx); |
| 1885 | + } |
| 1886 | + })) |
| 1887 | + .on_action(cx.listener(|this, _: &ComposerStash, window, cx| this.stash_prompt(window, cx))) |
| 1888 | + .on_action(cx.listener(|this, _: &FindInThread, window, cx| { |
| 1889 | + if !this.session.read(cx).items.is_empty() && this.diff.is_none() { |
| 1890 | + this.open_find(window, cx); |
| 1891 | + } |
| 1892 | + })) |
| 1893 | + .on_action(cx.listener(|this, _: &input::Escape, window, cx| this.escape(window, cx))) |
| 1907 | 1894 | .capture_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| this.pick_with_key(event, window, cx))) |
| 1908 | 1895 | .map(|this| match &self.diff { |
| 1909 | 1896 | Some(diff) => this.child(div().flex_1().min_h_0().child(changes::render_diff(diff, on_diff, cx))), |