mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
feat(editor): text commands in the palette and keymap
Transform to Upper/Lower/Title Case, Trim Trailing Whitespace, Join Lines and Remove Surrounding Brackets, as tty7 actions (bindable on the Keybindings page, unbound by default) and as palette rows offered while a buffer is open. Both run gpui-component's editing action on the active buffer. Join Lines gets VS Code's ctrl-j as a fixed CodeEditor binding on macOS, where the terminal keeps it as a line feed. Strings in en, zh, ja.
This commit is contained in:
@@ -135,6 +135,12 @@ actions!(
|
||||
EditorQuickFix,
|
||||
EditorRenameSymbol,
|
||||
EditorFormatDocument,
|
||||
EditorTransformUppercase,
|
||||
EditorTransformLowercase,
|
||||
EditorTransformTitleCase,
|
||||
EditorTrimTrailingWhitespace,
|
||||
EditorJoinLines,
|
||||
EditorRemoveSurroundingBrackets,
|
||||
EditorNextChange,
|
||||
EditorPrevChange,
|
||||
EditorRevertChange,
|
||||
|
||||
@@ -5914,6 +5914,37 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
|
||||
// The editor's text commands, while there is a buffer to run them on.
|
||||
if self.editor_can_dispatch() {
|
||||
actions.extend(
|
||||
[
|
||||
Item::localized(
|
||||
L10nKey::CmdEditorTransformUppercase,
|
||||
CommandKind::EditorTransformUppercase,
|
||||
),
|
||||
Item::localized(
|
||||
L10nKey::CmdEditorTransformLowercase,
|
||||
CommandKind::EditorTransformLowercase,
|
||||
),
|
||||
Item::localized(
|
||||
L10nKey::CmdEditorTransformTitleCase,
|
||||
CommandKind::EditorTransformTitleCase,
|
||||
),
|
||||
Item::localized(
|
||||
L10nKey::CmdEditorTrimTrailingWhitespace,
|
||||
CommandKind::EditorTrimTrailingWhitespace,
|
||||
),
|
||||
Item::localized(L10nKey::CmdEditorJoinLines, CommandKind::EditorJoinLines),
|
||||
Item::localized(
|
||||
L10nKey::CmdEditorRemoveSurroundingBrackets,
|
||||
CommandKind::EditorRemoveSurroundingBrackets,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|item| item.in_group(CommandGroup::View)),
|
||||
);
|
||||
}
|
||||
|
||||
// Offered only where it would do something. A connection opened from a
|
||||
// saved host has nothing to save, and a pane that is not an SSH one has
|
||||
// no connection at all — either would be a row that quietly did nothing
|
||||
@@ -6313,6 +6344,28 @@ impl Tty7App {
|
||||
}
|
||||
ToggleDocumentPreview => self.toggle_document_preview(cx),
|
||||
ToggleDocumentWrap => self.toggle_document_wrap(window, cx),
|
||||
EditorTransformUppercase => {
|
||||
self.editor_dispatch(&gpui_component::input::TransformToUppercase, window, cx);
|
||||
}
|
||||
EditorTransformLowercase => {
|
||||
self.editor_dispatch(&gpui_component::input::TransformToLowercase, window, cx);
|
||||
}
|
||||
EditorTransformTitleCase => {
|
||||
self.editor_dispatch(&gpui_component::input::TransformToTitleCase, window, cx);
|
||||
}
|
||||
EditorTrimTrailingWhitespace => {
|
||||
self.editor_dispatch(&gpui_component::input::TrimTrailingWhitespace, window, cx);
|
||||
}
|
||||
EditorJoinLines => {
|
||||
self.editor_dispatch(&gpui_component::input::JoinLines, window, cx);
|
||||
}
|
||||
EditorRemoveSurroundingBrackets => {
|
||||
self.editor_dispatch(
|
||||
&gpui_component::input::RemoveSurroundingBrackets,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RestartSshSession => self.restart_ssh_session(window, cx),
|
||||
SetTheme(i) => {
|
||||
if let Some(id) = crate::ui::presets::all(cx).get(i).map(|t| t.id.clone()) {
|
||||
@@ -9528,6 +9581,7 @@ impl Render for Tty7App {
|
||||
.children(self.render_switcher(window, cx))
|
||||
.when_some(self.search.clone(), |this, search| this.child(search))
|
||||
.children(gpui_component::Root::render_notification_layer(window, cx));
|
||||
let root = Self::with_editor_text_commands(root, cx);
|
||||
|
||||
if let Some(start) = prof {
|
||||
crate::ui::perf::record("window", start.elapsed());
|
||||
|
||||
@@ -1540,6 +1540,101 @@ impl Tty7App {
|
||||
self.editor_note_edit(id, cx);
|
||||
}
|
||||
|
||||
/// Run one of gpui-component's editing actions on the active buffer, as
|
||||
/// if it had been pressed there. Returns false with no buffer to run it
|
||||
/// on. The palette closes before its row runs, so this focuses the
|
||||
/// buffer first rather than relying on where the focus happens to be.
|
||||
pub(crate) fn editor_dispatch(
|
||||
&mut self,
|
||||
action: &dyn gpui::Action,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if !self.code_panel_visible() {
|
||||
return false;
|
||||
}
|
||||
let Some(input) = self.active_buffer().map(|f| f.input.clone()) else {
|
||||
return false;
|
||||
};
|
||||
let handle = input.read(cx).focus_handle(cx);
|
||||
window.focus(&handle, cx);
|
||||
handle.dispatch_action(action, window, cx);
|
||||
true
|
||||
}
|
||||
|
||||
/// The window's listeners for the editor's text commands, when the user
|
||||
/// has bound one: each runs on the editor only while it has the focus.
|
||||
pub(crate) fn with_editor_text_commands(
|
||||
root: gpui::Stateful<gpui::Div>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
use crate::core::actions::{
|
||||
EditorJoinLines, EditorRemoveSurroundingBrackets, EditorTransformLowercase,
|
||||
EditorTransformTitleCase, EditorTransformUppercase, EditorTrimTrailingWhitespace,
|
||||
};
|
||||
root.on_action(
|
||||
cx.listener(|this, _: &EditorTransformUppercase, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(&gpui_component::input::TransformToUppercase, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &EditorTransformLowercase, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(&gpui_component::input::TransformToLowercase, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &EditorTransformTitleCase, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(&gpui_component::input::TransformToTitleCase, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &EditorTrimTrailingWhitespace, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(&gpui_component::input::TrimTrailingWhitespace, window, cx);
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|this, _: &EditorJoinLines, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(&gpui_component::input::JoinLines, window, cx);
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &EditorRemoveSurroundingBrackets, window, cx| {
|
||||
if !this.editor_has_focus(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
this.editor_dispatch(
|
||||
&gpui_component::input::RemoveSurroundingBrackets,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether an editor exists to run [`Self::editor_dispatch`] on.
|
||||
pub(crate) fn editor_can_dispatch(&self) -> bool {
|
||||
self.code_panel_visible() && self.active_buffer().is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn editor_has_focus(&self, window: &Window, cx: &Context<Self>) -> bool {
|
||||
self.code_panel_visible()
|
||||
&& self.active_buffer().is_some_and(|f| {
|
||||
|
||||
@@ -1717,6 +1717,12 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::CmdDocumentWidthTwoThirds => "Document: Two-Thirds Width",
|
||||
L10nKey::CmdToggleDocumentPreview => "Document: Toggle Markdown Preview",
|
||||
L10nKey::CmdToggleDocumentWrap => "Document: Toggle Word Wrap",
|
||||
L10nKey::CmdEditorTransformUppercase => "Document: Transform to Uppercase",
|
||||
L10nKey::CmdEditorTransformLowercase => "Document: Transform to Lowercase",
|
||||
L10nKey::CmdEditorTransformTitleCase => "Document: Transform to Title Case",
|
||||
L10nKey::CmdEditorTrimTrailingWhitespace => "Document: Trim Trailing Whitespace",
|
||||
L10nKey::CmdEditorJoinLines => "Document: Join Lines",
|
||||
L10nKey::CmdEditorRemoveSurroundingBrackets => "Document: Remove Surrounding Brackets",
|
||||
L10nKey::CmdGitCommit => "Git: Commit",
|
||||
L10nKey::CmdGitStageAll => "Git: Stage All Changes",
|
||||
L10nKey::CmdGitUnstageAll => "Git: Unstage All Changes",
|
||||
|
||||
@@ -1762,6 +1762,12 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CmdDocumentWidthTwoThirds => "ドキュメント: 幅3分の2",
|
||||
L10nKey::CmdToggleDocumentPreview => "ドキュメント: Markdown プレビューを切り替え",
|
||||
L10nKey::CmdToggleDocumentWrap => "ドキュメント: 折り返しを切り替え",
|
||||
L10nKey::CmdEditorTransformUppercase => "ドキュメント: 大文字に変換",
|
||||
L10nKey::CmdEditorTransformLowercase => "ドキュメント: 小文字に変換",
|
||||
L10nKey::CmdEditorTransformTitleCase => "ドキュメント: 単語の先頭を大文字に変換",
|
||||
L10nKey::CmdEditorTrimTrailingWhitespace => "ドキュメント: 末尾の空白を削除",
|
||||
L10nKey::CmdEditorJoinLines => "ドキュメント: 行を結合",
|
||||
L10nKey::CmdEditorRemoveSurroundingBrackets => "ドキュメント: 外側の括弧を削除",
|
||||
L10nKey::CmdGitCommit => "Git: コミット",
|
||||
L10nKey::CmdGitStageAll => "Git: すべての変更をステージ",
|
||||
L10nKey::CmdGitUnstageAll => "Git: すべてのステージを取り消す",
|
||||
|
||||
@@ -1463,6 +1463,12 @@ l10n_keys! {
|
||||
CmdDocumentWidthTwoThirds,
|
||||
CmdToggleDocumentPreview,
|
||||
CmdToggleDocumentWrap,
|
||||
CmdEditorTransformUppercase,
|
||||
CmdEditorTransformLowercase,
|
||||
CmdEditorTransformTitleCase,
|
||||
CmdEditorTrimTrailingWhitespace,
|
||||
CmdEditorJoinLines,
|
||||
CmdEditorRemoveSurroundingBrackets,
|
||||
CmdGitCommit,
|
||||
CmdGitStageAll,
|
||||
CmdGitUnstageAll,
|
||||
|
||||
@@ -1593,6 +1593,12 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::CmdDocumentWidthTwoThirds => "文档:三分之二宽",
|
||||
L10nKey::CmdToggleDocumentPreview => "文档:切换 Markdown 预览",
|
||||
L10nKey::CmdToggleDocumentWrap => "文档:切换自动换行",
|
||||
L10nKey::CmdEditorTransformUppercase => "文档:转换为大写",
|
||||
L10nKey::CmdEditorTransformLowercase => "文档:转换为小写",
|
||||
L10nKey::CmdEditorTransformTitleCase => "文档:转换为词首字母大写",
|
||||
L10nKey::CmdEditorTrimTrailingWhitespace => "文档:删除行尾空白",
|
||||
L10nKey::CmdEditorJoinLines => "文档:合并行",
|
||||
L10nKey::CmdEditorRemoveSurroundingBrackets => "文档:删除外层括号",
|
||||
L10nKey::CmdGitCommit => "Git:提交",
|
||||
L10nKey::CmdGitStageAll => "Git:暂存全部更改",
|
||||
L10nKey::CmdGitUnstageAll => "Git:取消暂存全部更改",
|
||||
|
||||
+52
-2
@@ -135,6 +135,12 @@ fn fixed_bindings() -> Vec<KeyBinding> {
|
||||
KeyBinding::new("secondary-l", SelectLine, editor),
|
||||
KeyBinding::new("secondary-shift-\\", MoveToMatchingBracket, editor),
|
||||
]);
|
||||
#[cfg(target_os = "macos")]
|
||||
bindings.push(KeyBinding::new(
|
||||
"ctrl-j",
|
||||
gpui_component::input::JoinLines,
|
||||
editor,
|
||||
));
|
||||
}
|
||||
bindings
|
||||
}
|
||||
@@ -680,6 +686,15 @@ fn shipped_bindings() -> Vec<(&'static str, &'static str)> {
|
||||
("EditorQuickFix", ""),
|
||||
("EditorRenameSymbol", ""),
|
||||
("EditorFormatDocument", ""),
|
||||
// The editor's text commands. Unbound: none of them has a chord in
|
||||
// VS Code either, except Join Lines, whose ⌃J is a fixed `CodeEditor`
|
||||
// binding on macOS (in the terminal it is a line feed).
|
||||
("EditorTransformUppercase", ""),
|
||||
("EditorTransformLowercase", ""),
|
||||
("EditorTransformTitleCase", ""),
|
||||
("EditorTrimTrailingWhitespace", ""),
|
||||
("EditorJoinLines", ""),
|
||||
("EditorRemoveSurroundingBrackets", ""),
|
||||
// Alt+F5 / Shift+Alt+F5 (VS Code's) are fixed `Input`-context
|
||||
// bindings: a default on a function key would hide it from the shell.
|
||||
("EditorNextChange", ""),
|
||||
@@ -985,6 +1000,30 @@ fn authored_entry(action: &str) -> Option<(CommandGroup, String)> {
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::LspFormatDocument).to_string(),
|
||||
),
|
||||
"EditorTransformUppercase" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorTransformUppercase).to_string(),
|
||||
),
|
||||
"EditorTransformLowercase" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorTransformLowercase).to_string(),
|
||||
),
|
||||
"EditorTransformTitleCase" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorTransformTitleCase).to_string(),
|
||||
),
|
||||
"EditorTrimTrailingWhitespace" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorTrimTrailingWhitespace).to_string(),
|
||||
),
|
||||
"EditorJoinLines" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorJoinLines).to_string(),
|
||||
),
|
||||
"EditorRemoveSurroundingBrackets" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::CmdEditorRemoveSurroundingBrackets).to_string(),
|
||||
),
|
||||
"EditorGoToSymbol" => (
|
||||
CommandGroup::Terminal,
|
||||
t(L10nKey::EditorGoToSymbolAction).to_string(),
|
||||
@@ -1730,6 +1769,16 @@ fn make_binding(action: &str, keystroke: &str) -> Option<KeyBinding> {
|
||||
"EditorQuickFix" => KeyBinding::new(keystroke, EditorQuickFix, None),
|
||||
"EditorRenameSymbol" => KeyBinding::new(keystroke, EditorRenameSymbol, None),
|
||||
"EditorFormatDocument" => KeyBinding::new(keystroke, EditorFormatDocument, None),
|
||||
"EditorTransformUppercase" => KeyBinding::new(keystroke, EditorTransformUppercase, None),
|
||||
"EditorTransformLowercase" => KeyBinding::new(keystroke, EditorTransformLowercase, None),
|
||||
"EditorTransformTitleCase" => KeyBinding::new(keystroke, EditorTransformTitleCase, None),
|
||||
"EditorTrimTrailingWhitespace" => {
|
||||
KeyBinding::new(keystroke, EditorTrimTrailingWhitespace, None)
|
||||
}
|
||||
"EditorJoinLines" => KeyBinding::new(keystroke, EditorJoinLines, None),
|
||||
"EditorRemoveSurroundingBrackets" => {
|
||||
KeyBinding::new(keystroke, EditorRemoveSurroundingBrackets, None)
|
||||
}
|
||||
"EditorNextChange" => KeyBinding::new(keystroke, EditorNextChange, None),
|
||||
"EditorPrevChange" => KeyBinding::new(keystroke, EditorPrevChange, None),
|
||||
"EditorRevertChange" => KeyBinding::new(keystroke, EditorRevertChange, None),
|
||||
@@ -2782,7 +2831,8 @@ mod gpui_tests {
|
||||
.map(|b| b.action().name())
|
||||
};
|
||||
let editor = ["Workspace", "Input CodeEditor"];
|
||||
for (keys, action) in [
|
||||
let join_lines = cfg!(target_os = "macos").then_some(("ctrl-j", "input::JoinLines"));
|
||||
for (keys, action) in join_lines.into_iter().chain([
|
||||
("secondary-/", "input::ToggleLineComment"),
|
||||
("alt-up", "input::MoveLineUp"),
|
||||
("alt-down", "input::MoveLineDown"),
|
||||
@@ -2793,7 +2843,7 @@ mod gpui_tests {
|
||||
("secondary-shift-enter", "input::InsertLineAbove"),
|
||||
("secondary-l", "input::SelectLine"),
|
||||
("secondary-shift-\\", "input::MoveToMatchingBracket"),
|
||||
] {
|
||||
]) {
|
||||
assert_eq!(first(keys, &editor), Some(action), "{keys} in the editor");
|
||||
assert_ne!(
|
||||
first(keys, &["Workspace", "Input"]),
|
||||
|
||||
@@ -90,6 +90,13 @@ pub enum CommandKind {
|
||||
DocumentWidthTwoThirds,
|
||||
ToggleDocumentPreview,
|
||||
ToggleDocumentWrap,
|
||||
/// The code editor's text commands, run on the active buffer.
|
||||
EditorTransformUppercase,
|
||||
EditorTransformLowercase,
|
||||
EditorTransformTitleCase,
|
||||
EditorTrimTrailingWhitespace,
|
||||
EditorJoinLines,
|
||||
EditorRemoveSurroundingBrackets,
|
||||
RestartSshSession,
|
||||
ScmCommit,
|
||||
ScmStageAll,
|
||||
@@ -252,6 +259,12 @@ impl CommandKind {
|
||||
DocumentWidthTwoThirds => "document-width-two-thirds",
|
||||
ToggleDocumentPreview => "document-preview",
|
||||
ToggleDocumentWrap => "document-wrap",
|
||||
EditorTransformUppercase => "editor-uppercase",
|
||||
EditorTransformLowercase => "editor-lowercase",
|
||||
EditorTransformTitleCase => "editor-title-case",
|
||||
EditorTrimTrailingWhitespace => "editor-trim-trailing-whitespace",
|
||||
EditorJoinLines => "editor-join-lines",
|
||||
EditorRemoveSurroundingBrackets => "editor-remove-brackets",
|
||||
RestartSshSession => "ssh-reconnect",
|
||||
ScmCommit => "git-commit",
|
||||
ScmStageAll => "git-stage-all",
|
||||
@@ -298,6 +311,8 @@ impl CommandKind {
|
||||
CutText => return inline("secondary-x"),
|
||||
PasteText => return inline("secondary-v"),
|
||||
SelectAllText => return inline("secondary-a"),
|
||||
// A fixed `CodeEditor` binding, not one of the keymap's slots.
|
||||
EditorJoinLines => return inline("ctrl-j"),
|
||||
LaunchAgent(agent) => {
|
||||
return crate::ui::keymap::effective_key(
|
||||
crate::ui::agent_launch::launch_action_name(*agent),
|
||||
@@ -381,6 +396,12 @@ impl CommandKind {
|
||||
DocumentWidthTwoThirds => "DocumentWidthTwoThirds",
|
||||
ToggleDocumentPreview => "ToggleDocumentPreview",
|
||||
ToggleDocumentWrap => "ToggleDocumentWrap",
|
||||
EditorTransformUppercase => "EditorTransformUppercase",
|
||||
EditorTransformLowercase => "EditorTransformLowercase",
|
||||
EditorTransformTitleCase => "EditorTransformTitleCase",
|
||||
EditorTrimTrailingWhitespace => "EditorTrimTrailingWhitespace",
|
||||
EditorJoinLines => "EditorJoinLines",
|
||||
EditorRemoveSurroundingBrackets => "EditorRemoveSurroundingBrackets",
|
||||
RestartSshSession => "RestartSshSession",
|
||||
OpenSshProfiles => "OpenSshProfiles",
|
||||
ScmCommit => "ScmCommit",
|
||||
|
||||
Reference in New Issue
Block a user