Files
galaxy/app/src/code/editor/view/actions.rs
T

1353 lines
47 KiB
Rust

#![cfg_attr(target_family = "wasm", allow(dead_code, unused_imports))]
// Adding this file level gate as some of the code around editability is not used in WASM yet.
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::Range;
use lazy_static::lazy_static;
use rangemap::RangeSet;
use string_offset::CharOffset;
use galaxy_editor::content::version::BufferVersion;
use galaxy_editor::editor::{EmbeddedItemModel, RunnableCommandModel, TextDecoration};
use galaxy_editor::model::{CoreEditorModel, PlainTextEditorModel};
use galaxy_editor::render::element::RichTextAction;
use galaxy_editor::render::model::{ExpansionType, LineCount, Location};
use galaxy_editor::selection::{TextDirection, TextUnit};
use galaxy_util::user_input::UserInput;
use galaxyui::actions::StandardAction;
use galaxyui::elements::Axis;
use galaxyui::event::ModifiersState;
use galaxyui::keymap::{EditableBinding, FixedBinding, Keystroke, PerPlatformKeystroke};
use galaxyui::units::Pixels;
use galaxyui::{AppContext, TypedActionView, ViewContext, WeakViewHandle};
use crate::cmd_or_ctrl_shift;
use crate::code::editor::line::EditorLineLocation;
use crate::code::editor::model::CodeEditorModel;
use crate::code::editor::view::{CodeEditorEvent, CodeEditorView, VimMode};
use crate::code_review::comments::CommentId;
use crate::editor::InteractionState;
use crate::features::FeatureFlag;
use crate::notebooks::editor::model::word_unit;
use crate::util::bindings::CustomAction;
/// Limit the keybindings that conflict with the Agent Mode embedded editor.
const NON_EDITABLE_KEYMAP_CONTEXT: &str = "NonEditableKeymapContext";
lazy_static! {
static ref AUTOCOMPLETE_SYMBOLS: HashMap<char, char> =
HashMap::from([('(', ')'), ('[', ']'), ('{', '}'), ('\'', '\''), ('"', '"'),]);
static ref CLOSING_SYMBOLS: HashSet<char> = AUTOCOMPLETE_SYMBOLS.values().cloned().collect();
}
pub fn init(app: &mut AppContext) {
use galaxyui::keymap::macros::*;
let text_entry = id!("CodeEditorView") & !id!("IMEOpen");
// We use this to disable some keybindings that would conflict with the Agent Mode embedded editor.
let editable_state = text_entry.clone() & !id!(NON_EDITABLE_KEYMAP_CONTEXT);
app.register_fixed_bindings([
FixedBinding::new(
"enter",
CodeEditorViewAction::Enter,
editable_state.clone() & !id!("Vim"),
),
FixedBinding::new(
"enter",
CodeEditorViewAction::VimEnter,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"numpadenter",
CodeEditorViewAction::Enter,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"numpadenter",
CodeEditorViewAction::VimEnter,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"shift-enter",
CodeEditorViewAction::Enter,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"shift-enter",
CodeEditorViewAction::VimShiftEnter,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"backspace",
CodeEditorViewAction::Backspace,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"backspace",
CodeEditorViewAction::VimBackspace,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"shift-backspace",
CodeEditorViewAction::Backspace,
text_entry.clone(),
),
FixedBinding::new(
"shift-backspace",
CodeEditorViewAction::VimBackspace,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"delete",
CodeEditorViewAction::Delete,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"delete",
CodeEditorViewAction::VimDelete,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"shift-up",
CodeEditorViewAction::SelectUp,
text_entry.clone(),
),
FixedBinding::new(
"shift-down",
CodeEditorViewAction::SelectDown,
text_entry.clone(),
),
FixedBinding::new(
"shift-left",
CodeEditorViewAction::SelectLeft,
text_entry.clone(),
),
FixedBinding::new(
"shift-right",
CodeEditorViewAction::SelectRight,
text_entry.clone(),
),
FixedBinding::new("up", CodeEditorViewAction::MoveUp, editable_state.clone()),
FixedBinding::new(
"down",
CodeEditorViewAction::MoveDown,
editable_state.clone(),
),
FixedBinding::new(
"left",
CodeEditorViewAction::MoveLeft,
editable_state.clone(),
),
FixedBinding::new(
"right",
CodeEditorViewAction::MoveRight,
editable_state.clone(),
),
FixedBinding::new(
"home",
CodeEditorViewAction::MoveToLineStart,
text_entry.clone(),
),
FixedBinding::new(
"end",
CodeEditorViewAction::MoveToLineEnd,
text_entry.clone(),
),
FixedBinding::new(
"tab",
CodeEditorViewAction::Tab,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"tab",
CodeEditorViewAction::VimTab,
text_entry.clone() & id!("Vim"),
),
FixedBinding::new(
"shift-tab",
CodeEditorViewAction::ShiftTab,
text_entry.clone() & !id!("Vim"),
),
FixedBinding::new(
"shift-tab",
CodeEditorViewAction::VimShiftTab,
text_entry.clone() & id!("Vim"),
),
// Also create the word movement shortcuts with `meta` in place of `alt`, to accommodate
// the "Left Option is Meta" and "Right Option is Meta" settings.
FixedBinding::new(
"meta-left",
CodeEditorViewAction::MoveBackwardsByWord,
text_entry.clone(),
),
FixedBinding::new(
"meta-right",
CodeEditorViewAction::MoveForwardsByWord,
text_entry.clone(),
),
FixedBinding::new_per_platform(
PerPlatformKeystroke {
mac: "shift-alt-left",
linux_and_windows: "shift-ctrl-left",
},
CodeEditorViewAction::SelectBackwardsByWord,
text_entry.clone(),
),
FixedBinding::new(
"shift-meta-left",
CodeEditorViewAction::SelectBackwardsByWord,
text_entry.clone(),
),
FixedBinding::new_per_platform(
PerPlatformKeystroke {
mac: "shift-alt-right",
linux_and_windows: "shift-ctrl-right",
},
CodeEditorViewAction::SelectForwardsByWord,
text_entry.clone(),
),
FixedBinding::new(
"shift-meta-right",
CodeEditorViewAction::SelectForwardsByWord,
text_entry.clone(),
),
FixedBinding::new(
"shift-home",
CodeEditorViewAction::SelectToLineStart,
text_entry.clone(),
),
FixedBinding::new(
"shift-end",
CodeEditorViewAction::SelectToLineEnd,
text_entry.clone(),
),
FixedBinding::new(
"cmdorctrl-a",
CodeEditorViewAction::SelectAll,
text_entry.clone(),
),
// TODO(kevin): Only for testing purposes.
FixedBinding::new(
"cmdorctrl-shift-X",
CodeEditorViewAction::ToggleDiffNav(None),
text_entry.clone(),
),
FixedBinding::custom(
CustomAction::Copy,
CodeEditorViewAction::Copy,
"Copy",
text_entry.clone(),
),
// Bindings for paste require the StandardAction and CustomAction binding to work on all platforms.
FixedBinding::custom(
CustomAction::Paste,
CodeEditorViewAction::Paste,
"Paste",
text_entry.clone(),
),
FixedBinding::standard(
StandardAction::Paste,
CodeEditorViewAction::Paste,
text_entry.clone(),
),
#[cfg(windows)]
FixedBinding::custom(
CustomAction::WindowsPaste,
CodeEditorViewAction::Paste,
"Paste",
text_entry.clone(),
),
#[cfg(windows)]
FixedBinding::custom(
CustomAction::WindowsCopy,
CodeEditorViewAction::WindowsCtrlC,
"Copy",
text_entry.clone(),
),
FixedBinding::custom(
CustomAction::Cut,
CodeEditorViewAction::Cut,
"Cut",
text_entry.clone(),
),
FixedBinding::custom(
CustomAction::Undo,
CodeEditorViewAction::Undo,
"Undo",
text_entry.clone(),
),
FixedBinding::custom(
CustomAction::Redo,
CodeEditorViewAction::Redo,
"Redo",
text_entry.clone(),
),
FixedBinding::new("escape", CodeEditorViewAction::Escape, text_entry.clone()),
]);
// Bind Ctrl-R to Redo when in Vim normal mode.
app.register_fixed_bindings([FixedBinding::new(
"ctrl-r",
CodeEditorViewAction::Redo,
text_entry.clone() & id!("VimNormalMode"),
)]);
// Editable navigation keybindings:
app.register_editable_bindings([
EditableBinding::new(
"editor_view:move_backward_one_word",
"Move Backward One Word",
CodeEditorViewAction::MoveBackwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("alt-left")
.with_linux_or_windows_key_binding("ctrl-left"),
EditableBinding::new(
"editor_view:move_forward_one_word",
"Move Forward One Word",
CodeEditorViewAction::MoveForwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("alt-right")
.with_linux_or_windows_key_binding("ctrl-right"),
EditableBinding::new(
"editor_view:move_forward_one_word",
"Move forward one word",
CodeEditorViewAction::MoveForwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("meta-f"),
EditableBinding::new(
"editor_view:move_backward_one_word",
"Move backward one word",
CodeEditorViewAction::MoveBackwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("meta-b"),
EditableBinding::new(
"editor_view:up",
"Move cursor up",
CodeEditorViewAction::MoveUp,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-p"),
EditableBinding::new(
"editor_view:down",
"Move cursor down",
CodeEditorViewAction::MoveDown,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-n"),
EditableBinding::new(
"editor_view:left",
"Move cursor left",
CodeEditorViewAction::MoveLeft,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-b"),
EditableBinding::new(
"editor_view:right",
"Move cursor right",
CodeEditorViewAction::MoveRight,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-f"),
EditableBinding::new(
"editor_view:move_to_line_start",
"Move to line start",
CodeEditorViewAction::MoveToLineStart,
)
.with_context_predicate(text_entry.clone())
// Mac-only to not conflict with SelectAll on Linux and Windows.
.with_mac_key_binding("ctrl-a"),
EditableBinding::new(
"editor_view:home",
"Home",
CodeEditorViewAction::MoveToLineStart,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("cmd-left")
.with_linux_or_windows_key_binding("home"),
EditableBinding::new(
"editor_view:move_to_line_end",
"Move to line end",
CodeEditorViewAction::MoveToLineEnd,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("ctrl-e"),
EditableBinding::new(
"editor_view:end",
"End",
CodeEditorViewAction::MoveToLineEnd,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("cmd-right")
.with_linux_or_windows_key_binding("end"),
EditableBinding::new(
"editor_view:cursor_at_buffer_start",
"Cursor at buffer start",
CodeEditorViewAction::CursorAtBufferStart,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("cmdorctrl-up"),
EditableBinding::new(
"editor_view:cursor_at_buffer_end",
"Cursor at buffer end",
CodeEditorViewAction::CursorAtBufferEnd,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("cmdorctrl-down"),
]);
// Editable selection keybindings:
app.register_editable_bindings([
EditableBinding::new(
"editor_view:select_left_by_word",
"Select one word to the left",
CodeEditorViewAction::SelectBackwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("shift-meta-B"),
EditableBinding::new(
"editor_view:select_right_by_word",
"Select one word to the right",
CodeEditorViewAction::SelectForwardsByWord,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("shift-meta-F"),
EditableBinding::new(
"editor_view:select_left",
"Select one character to the left",
CodeEditorViewAction::SelectLeft,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("shift-ctrl-B"),
EditableBinding::new(
"editor_view:select_right",
"Select one character to the right",
CodeEditorViewAction::SelectRight,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("shift-ctrl-F"),
EditableBinding::new(
"editor_view:select_up",
"Select up",
CodeEditorViewAction::SelectUp,
)
.with_context_predicate(text_entry.clone())
// Set this to Mac only since otherwise it could conflict with opening the command
// palette. NOTE `shift-up` still exists as a cross platform keybinding for this action.
.with_mac_key_binding("shift-ctrl-P"),
EditableBinding::new(
"editor_view:select_down",
"Select down",
CodeEditorViewAction::SelectDown,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("shift-ctrl-N"),
EditableBinding::new(
"editor_view:select_all",
"Select all",
CodeEditorViewAction::SelectAll,
)
.with_context_predicate(text_entry.clone())
.with_custom_action(CustomAction::SelectAll),
EditableBinding::new(
"editor:select_to_line_start",
"Select to start of line",
CodeEditorViewAction::SelectToLineStart,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("shift-ctrl-A"),
EditableBinding::new(
"editor:select_to_line_end",
"Select to end of line",
CodeEditorViewAction::SelectToLineEnd,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("shift-ctrl-E"),
// `shift-end` is registered on all platforms for this action.
EditableBinding::new(
"editor_view:select_to_line_end",
"Select To Line End",
CodeEditorViewAction::SelectToLineEnd,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("cmd-shift-right"),
// `end` is registered on all platforms for this action.
EditableBinding::new(
"editor_view:select_to_line_start",
"Select To Line Start",
CodeEditorViewAction::SelectToLineStart,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("cmd-shift-left"),
]);
// Editable text-manipulation bindings
app.register_editable_bindings([
EditableBinding::new(
"editor_view:backspace",
"Remove the previous character",
CodeEditorViewAction::Backspace,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-h"),
EditableBinding::new(
"editor_view:toggle_comment",
"Toggle comment",
CodeEditorViewAction::ToggleComment,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("cmdorctrl-/"),
EditableBinding::new("editor_view:delete", "Delete", CodeEditorViewAction::Delete)
.with_context_predicate(
text_entry.clone() & !id!("VimNormalMode") & !id!("VimVisualMode"),
)
.with_key_binding("ctrl-d"),
EditableBinding::new(
"editor_view:vim_scroll_half_page_down",
"Scroll down half a page (vim)",
CodeEditorViewAction::ScrollHalfPageDown,
)
.with_context_predicate(text_entry.clone() & (id!("VimNormalMode") | id!("VimVisualMode")))
.with_key_binding("ctrl-d"),
EditableBinding::new(
"editor_view:vim_scroll_half_page_up",
"Scroll up half a page (vim)",
CodeEditorViewAction::ScrollHalfPageUp,
)
.with_context_predicate(text_entry.clone() & (id!("VimNormalMode") | id!("VimVisualMode")))
.with_key_binding("ctrl-u"),
EditableBinding::new(
"editor_view:cut_word_left",
"Cut word left",
CodeEditorViewAction::CutWordLeft,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-w"),
EditableBinding::new(
"editor:delete_word_left",
"Delete word left",
CodeEditorViewAction::DeleteWordLeft,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("alt-backspace")
.with_linux_or_windows_key_binding("ctrl-backspace"),
EditableBinding::new(
"editor_view:cut_word_right",
"Cut word right",
CodeEditorViewAction::CutWordRight,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("alt-d"),
EditableBinding::new(
"editor:delete_word_right",
"Delete word right",
CodeEditorViewAction::DeleteWordRight,
)
.with_context_predicate(text_entry.clone())
.with_mac_key_binding("alt-delete")
.with_linux_or_windows_key_binding("ctrl-delete"),
EditableBinding::new(
"editor_view:cut_all_left",
"Cut all left",
CodeEditorViewAction::CutLineLeft,
)
.with_context_predicate(text_entry.clone()),
EditableBinding::new(
"editor_view:delete_all_left",
"Delete all left",
CodeEditorViewAction::DeleteLineLeft,
)
.with_context_predicate(text_entry.clone())
// Intellij uses `ctrl-Y` to delete a line on Windows/Linux whereas VSCode uses
// `ctrl-shift-k`. We use the former because `ctrl-shift-k` would interfere with the binding
// to clear all blocks within the blocklist.
.with_mac_key_binding("cmd-backspace")
.with_linux_or_windows_key_binding("ctrl-y"),
EditableBinding::new(
"editor_view:cut_all_right",
"Cut all right",
CodeEditorViewAction::CutLineRight,
)
.with_context_predicate(text_entry.clone())
.with_key_binding("ctrl-k"),
EditableBinding::new(
"editor_view:delete_all_right",
"Delete all right",
CodeEditorViewAction::DeleteLineRight,
)
.with_context_predicate(text_entry.clone())
// VSCode only binds a default binding on Mac.
.with_mac_key_binding("cmd-delete"),
]);
// Editable Vim keybindings
app.register_editable_bindings([EditableBinding::new(
"editor_view:vim_exit_insert_mode",
"Exit Vim insert mode",
CodeEditorViewAction::VimEscape,
)
.with_context_predicate(text_entry.clone() & id!("Vim"))
.with_key_binding("ctrl-[")]);
// Editable Find Bar keybindings
app.register_editable_bindings([EditableBinding::new(
"code_editor:find",
"Find in code editor",
CodeEditorViewAction::ShowFindBar,
)
.with_key_binding(cmd_or_ctrl_shift("f"))
.with_custom_action(CustomAction::Find)
.with_context_predicate(text_entry.clone() & id!("FindBarAvailable"))
.with_enabled(|| FeatureFlag::CodeFindReplace.is_enabled())]);
// Editable Go to Line keybinding
app.register_editable_bindings([EditableBinding::new(
"editor_view:go_to_line",
"Go to line",
CodeEditorViewAction::ShowGoToLine,
)
.with_key_binding("ctrl-g") // Matches VSCode; editor-scoped via text_entry predicate
.with_custom_action(CustomAction::GoToLine)
.with_context_predicate(text_entry.clone())]);
}
#[derive(Debug, Clone)]
pub enum CodeEditorViewAction {
UserTyped(UserInput<String>),
VimUserTyped(UserInput<String>),
Enter,
Delete,
Backspace,
VimBackspace,
ToggleComment,
ScrollVertical(Pixels),
ScrollHorizontal(Pixels),
ScrollHalfPageDown,
ScrollHalfPageUp,
SelectUp,
SelectDown,
SelectLeft,
SelectRight,
SelectBackwardsByWord,
SelectForwardsByWord,
SelectToLineStart,
SelectToLineEnd,
SelectAll,
ToggleDiffNav(Option<Range<LineCount>>),
HiddenSectionExpansion {
line_range: Range<LineCount>,
expansion_type: ExpansionType,
},
/// Add diff hunk content as context (when clicking plus icon)
AddDiffHunkContext {
line_range: Range<LineCount>,
},
/// Revert diff hunk changes (when clicking revert icon)
RevertDiffHunk {
line_range: Range<LineCount>,
},
/// Open comment line (when opening a comment on a specific line)
NewCommentOnLine {
line: EditorLineLocation,
},
RequestOpenSavedComment {
uuid: CommentId,
},
DeleteLineLeft,
DeleteLineRight,
DeleteWordLeft,
DeleteWordRight,
CutLineLeft,
CutLineRight,
CutWordLeft,
CutWordRight,
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
MoveBackwardsByWord,
MoveForwardsByWord,
MoveToLineStart,
MoveToLineEnd,
CursorAtBufferStart,
CursorAtBufferEnd,
SelectWord {
offset: CharOffset,
modifiers: ModifiersState,
},
SelectLine {
offset: CharOffset,
modifiers: ModifiersState,
},
SelectionStart {
offset: CharOffset,
modifiers: ModifiersState,
},
SelectionUpdate(CharOffset),
SelectionEnd,
MaybeClickOnHoveredLink(CharOffset),
MouseHovered {
offset: CharOffset,
cmd: bool,
clamped: bool,
/// Whether the mouse move event was covered by an element above the editor.
is_covered: bool,
},
RightMouseDown {
offset: CharOffset,
},
Paste,
Cut,
Copy,
#[cfg(windows)]
WindowsCtrlC,
Undo,
Redo,
Tab,
ShiftTab,
ShowFindBar,
ShowGoToLine,
Escape,
VimEnter,
VimTab,
VimDelete,
VimShiftTab,
VimShiftEnter,
VimEscape,
}
impl CodeEditorViewAction {
pub fn allowed_in_interaction_state(&self, state: InteractionState) -> bool {
match state {
InteractionState::Editable => true,
InteractionState::Selectable | InteractionState::EditableWithInvalidSelection => {
self.allowed_in_selectable_state()
}
InteractionState::Disabled => false,
}
}
fn allowed_in_selectable_state(&self) -> bool {
match self {
Self::UserTyped(_)
| Self::VimUserTyped(_)
| Self::Enter
| Self::Delete
| Self::Backspace
| Self::VimBackspace
| Self::ToggleComment
| Self::DeleteLineLeft
| Self::DeleteLineRight
| Self::DeleteWordLeft
| Self::DeleteWordRight
| Self::CutLineLeft
| Self::CutLineRight
| Self::CutWordLeft
| Self::CutWordRight
| Self::Paste
| Self::Cut
| Self::Undo
| Self::Redo
| Self::Tab
| Self::ShiftTab
| Self::VimEnter
| Self::VimTab
| Self::VimDelete
| Self::VimShiftTab
| Self::VimShiftEnter
| Self::VimEscape => false,
#[cfg(windows)]
Self::WindowsCtrlC => true,
Self::ScrollVertical(_)
| Self::ScrollHorizontal(_)
| Self::ScrollHalfPageDown
| Self::ScrollHalfPageUp
| Self::SelectUp
| Self::SelectDown
| Self::SelectLeft
| Self::SelectRight
| Self::SelectBackwardsByWord
| Self::SelectForwardsByWord
| Self::SelectToLineStart
| Self::SelectToLineEnd
| Self::SelectAll
| Self::ToggleDiffNav(_)
| Self::MoveUp
| Self::MoveDown
| Self::MoveLeft
| Self::MoveRight
| Self::MoveBackwardsByWord
| Self::MoveForwardsByWord
| Self::MoveToLineStart
| Self::MoveToLineEnd
| Self::SelectWord { .. }
| Self::SelectLine { .. }
| Self::SelectionStart { .. }
| Self::SelectionUpdate(_)
| Self::SelectionEnd
| Self::CursorAtBufferStart
| Self::CursorAtBufferEnd
| Self::Copy
| Self::ShowFindBar
| Self::ShowGoToLine
| Self::Escape
| Self::HiddenSectionExpansion { .. }
| Self::AddDiffHunkContext { .. }
| Self::RevertDiffHunk { .. }
| Self::NewCommentOnLine { .. }
| Self::RequestOpenSavedComment { .. }
| Self::MouseHovered { .. }
| Self::MaybeClickOnHoveredLink(_)
| Self::RightMouseDown { .. } => true,
}
}
}
impl TypedActionView for CodeEditorView {
type Action = CodeEditorViewAction;
fn handle_action(&mut self, action: &CodeEditorViewAction, ctx: &mut ViewContext<Self>) {
use CodeEditorViewAction::*;
// Some actions are not allowed if editing or selection is disabled.
if !action.allowed_in_interaction_state(self.interaction_state(ctx)) {
return;
}
// When completion menu is active, redirect navigation/confirm keys to the parent.
if self.completion_intercept_keys {
match action {
MoveUp => {
ctx.emit(super::CodeEditorEvent::CompletionNavigateUp);
return;
}
MoveDown => {
ctx.emit(super::CodeEditorEvent::CompletionNavigateDown);
return;
}
Tab | Enter => {
ctx.emit(super::CodeEditorEvent::CompletionConfirm);
return;
}
Escape => {
// Let escape fall through to normal handling (which emits EscapePressed)
}
_ => {}
}
}
match action {
UserTyped(content) => self.user_insert(content, ctx),
VimUserTyped(content) => {
// Don't handle vim input when the find input is active
let allow_vim = match &self.find_bar {
Some(find_bar) => {
let open = find_bar.as_ref(ctx).is_open();
let editable = find_bar.as_ref(ctx).is_find_input_editable(ctx);
!(open && editable)
}
None => true,
};
if allow_vim {
self.vim_user_insert(content, ctx);
}
}
ToggleDiffNav(line_range) => self.toggle_diff_nav(line_range.clone(), ctx),
Enter => self.model.update(ctx, |model, ctx| model.enter(ctx)),
VimEnter => self.vim_keystroke(&Keystroke::parse("enter").expect("enter parses"), ctx),
Delete => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Forwards, TextUnit::Character, false, ctx);
}),
VimDelete => {
self.vim_keystroke(&Keystroke::parse("delete").expect("delete parses"), ctx)
}
Backspace => {
self.model.update(ctx, |model, ctx| {
model.backspace(ctx);
});
}
VimBackspace => {
self.vim_keystroke(
&Keystroke::parse("backspace").expect("backspace parses"),
ctx,
);
}
ToggleComment => {
match self.vim_mode(ctx) {
Some(VimMode::Visual(_)) => {
// In Vim Visual mode, if we get a ToggleComment request via the keyboard
// shortcut (cmd+/), simulate `gc` to the VimModel so that we correctly
// calculate the current visual selections, apply the toggle, and exit to
// normal mode.
self.vim_user_insert("gc", ctx);
}
_ => {
self.model.update(ctx, |model, ctx| {
model.toggle_comments(ctx);
});
}
}
}
ScrollVertical(delta) => self.model.update(ctx, |model, ctx| {
model.render_state().update(ctx, |render_state, ctx| {
render_state.scroll(*delta, ctx);
})
}),
ScrollHorizontal(delta) => self.model.update(ctx, |model, ctx| {
model.render_state().update(ctx, |render_state, ctx| {
render_state.scroll_horizontal(*delta, ctx);
})
}),
ScrollHalfPageDown => {
self.vim_keystroke(&Keystroke::parse("ctrl-d").expect("ctrl-d parses"), ctx)
}
ScrollHalfPageUp => {
self.vim_keystroke(&Keystroke::parse("ctrl-u").expect("ctrl-u parses"), ctx)
}
SelectUp => self.model.update(ctx, |model, ctx| {
model.select_up(ctx);
}),
SelectDown => self.model.update(ctx, |model, ctx| {
model.select_down(ctx);
}),
SelectLeft => self.model.update(ctx, |model, ctx| {
model.select_left(ctx);
}),
SelectRight => self.model.update(ctx, |model, ctx| {
model.select_right(ctx);
}),
SelectBackwardsByWord => self.model.update(ctx, |model, ctx| {
model.backward_word(true, ctx);
}),
SelectForwardsByWord => self.model.update(ctx, |model, ctx| {
model.forward_word(true, ctx);
}),
SelectToLineStart => self.model.update(ctx, |model, ctx| {
model.select_to_line_start(ctx);
}),
SelectToLineEnd => self.model.update(ctx, |model, ctx| {
model.select_to_paragraph_end(ctx);
}),
SelectAll => self.model.update(ctx, |model, ctx| {
model.select_all(ctx);
}),
CursorAtBufferStart => self.model.update(ctx, |model, ctx| {
model.cursor_at(CharOffset::from(1), ctx);
}),
CursorAtBufferEnd => self.model.update(ctx, |model, ctx| {
model.cursor_at(model.max_character_offset(ctx), ctx);
}),
DeleteLineLeft => self.delete_line_left(ctx),
DeleteLineRight => self.model.update(ctx, |model, ctx| {
model.delete(
TextDirection::Forwards,
TextUnit::ParagraphBoundary,
false,
ctx,
);
}),
DeleteWordLeft => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Backwards, word_unit(ctx), false, ctx);
}),
DeleteWordRight => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Forwards, word_unit(ctx), false, ctx);
}),
CutLineLeft => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Backwards, TextUnit::LineBoundary, true, ctx);
}),
CutLineRight => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Forwards, TextUnit::LineBoundary, true, ctx);
}),
CutWordLeft => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Backwards, word_unit(ctx), true, ctx);
}),
CutWordRight => self.model.update(ctx, |model, ctx| {
model.delete(TextDirection::Forwards, word_unit(ctx), true, ctx);
}),
MoveUp => self.model.update(ctx, |model, ctx| model.move_up(ctx)),
MoveDown => self.model.update(ctx, |model, ctx| model.move_down(ctx)),
MoveLeft => self.model.update(ctx, |model, ctx| {
model.move_left(ctx);
}),
MoveRight => self.model.update(ctx, |model, ctx| {
model.move_right(ctx);
}),
MoveBackwardsByWord => self.model.update(ctx, |model, ctx| {
model.backward_word(false, ctx);
}),
MoveForwardsByWord => self.model.update(ctx, |model, ctx| {
model.forward_word(false, ctx);
}),
MoveToLineStart => self.model.update(ctx, |model, ctx| {
model.move_to_line_start(ctx);
}),
MoveToLineEnd => self.model.update(ctx, |model, ctx| {
model.move_to_paragraph_end(ctx);
}),
SelectWord { offset, modifiers } => {
self.is_selecting = true;
let multiselect = modifiers.alt && FeatureFlag::RichTextMultiselect.is_enabled();
self.model.update(ctx, |model, ctx| {
model.select_word_at(*offset, multiselect, ctx);
});
}
SelectLine { offset, modifiers } => {
self.is_selecting = true;
let multiselect = modifiers.alt && FeatureFlag::RichTextMultiselect.is_enabled();
self.model.update(ctx, |model, ctx| {
model.select_line_at(*offset, multiselect, ctx);
});
}
SelectionStart { offset, modifiers } => self.selection_start(*offset, *modifiers, ctx),
SelectionUpdate(offset) => {
if self.is_selecting {
self.selection_update(*offset, ctx);
} else {
self.selection_extend(*offset, ctx);
}
}
SelectionEnd => self.selection_end(ctx),
Paste => self.model.update(ctx, |model, ctx| {
model.paste(ctx);
}),
Cut => self.model.update(ctx, |model, ctx| {
model.cut(ctx);
}),
// Note that this is _not_ the only code path that could copy selected text to the clipboard.
// This is only for the case when the editor is focused and the copy action gets dispatched directly.
// The owner of the editor can also perform a copy by accessing the selected text and copying it to the clipboard.
// This is the case when the code block is owned by an AIBlock and unfocused.
Copy => {
self.model.update(ctx, |model, ctx| {
model.copy(ctx);
});
// It's possible that the copy action was dispatched to the focused editor even when
// the user intended to copy selected text from a parent view (i.e. an `AIBlock`).
// The `CopiedEmptyText` event gives the parent view a signal to attempt a copy action.
if self.selected_text(ctx).is_none() {
ctx.emit(CodeEditorEvent::CopiedEmptyText);
}
}
#[cfg(windows)]
WindowsCtrlC => self.model.update(ctx, |model, ctx| {
model.handle_windows_ctrl_c(ctx);
}),
Undo => self.model.update(ctx, |model, ctx| {
model.undo(ctx);
}),
Redo => self.model.update(ctx, |model, ctx| {
model.redo(ctx);
}),
Tab => self.model.update(ctx, |model, ctx| {
model.indent(false, ctx);
}),
VimTab => self.vim_keystroke(&Keystroke::parse("tab").expect("tab parses"), ctx),
ShiftTab => self.model.update(ctx, |model, ctx| {
model.indent(true, ctx);
}),
VimShiftTab => self.vim_keystroke(
&Keystroke::parse("shift-tab").expect("shift-tab parses"),
ctx,
),
VimShiftEnter => self.vim_keystroke(
&Keystroke::parse("shift-enter").expect("shift-enter parses"),
ctx,
),
VimEscape => {
self.vim_keystroke(&Keystroke::parse("escape").expect("escape parses"), ctx)
}
ShowFindBar => self.show_find_bar(ctx),
ShowGoToLine => self.show_goto_line(ctx),
Escape => self.escape(ctx),
HiddenSectionExpansion {
line_range,
expansion_type,
} => {
self.expand_hidden_section(line_range.clone(), expansion_type, ctx);
}
AddDiffHunkContext { line_range } => {
// Record this range as clicked so the button disappears
self.display_states
.wrapper_state_handle
.record_clicked_range(line_range.clone());
// Emit event for parent to handle adding context
ctx.emit(CodeEditorEvent::DiffHunkContextAdded {
line_range: line_range.clone(),
});
// Notify to re-render and hide the button
ctx.notify();
}
RevertDiffHunk { line_range } => {
if FeatureFlag::RevertDiffHunk.is_enabled() {
// Convert line range to diff hunk index and revert it
let hunk_index = self
.model
.as_ref(ctx)
.diff()
.as_ref(ctx)
.diff_hunk_count_before_line(line_range.start.as_usize());
self.model.update(ctx, |model, ctx| {
model.reverse_diff_by_index(hunk_index, ctx);
});
// Emit event for parent to handle
ctx.emit(CodeEditorEvent::DiffReverted);
// Notify to re-render
ctx.notify();
}
}
NewCommentOnLine { line: line_info } => {
if FeatureFlag::InlineCodeReview.is_enabled() {
self.model.update(ctx, |model: &mut CodeEditorModel, ctx| {
model.open_comment_line(line_info, ctx);
});
ctx.emit(CodeEditorEvent::CommentEditorOpened);
ctx.focus(&self.active_comment_editor);
ctx.notify();
}
}
RequestOpenSavedComment { uuid } => {
if FeatureFlag::InlineCodeReview.is_enabled() {
ctx.emit(CodeEditorEvent::RequestOpenComment(*uuid))
}
}
MouseHovered {
offset,
cmd,
clamped,
is_covered,
} => {
ctx.emit(CodeEditorEvent::MouseHovered {
offset: *offset,
cmd: *cmd,
clamped: *clamped,
is_covered: *is_covered,
});
}
RightMouseDown { offset } => {
// Right mouse down should set the cursor at the offset location. This matches the behavior with other editors.
self.model.update(ctx, |model, ctx| {
model.cursor_at(*offset, ctx);
});
}
MaybeClickOnHoveredLink(offset) => {
self.model.update(ctx, |model, ctx| {
model.maybe_click_on_hovered_link(offset, ctx)
});
}
}
}
}
impl galaxy_editor::editor::EditorView for CodeEditorView {
type RichTextAction = CodeEditorViewAction;
fn runnable_command_at<'a>(
&self,
_block_offset: CharOffset,
_ctx: &'a AppContext,
) -> Option<&'a dyn RunnableCommandModel> {
None
}
fn embedded_item_at<'a>(
&self,
_block_offset: CharOffset,
_ctx: &'a AppContext,
) -> Option<&'a dyn EmbeddedItemModel> {
None
}
fn text_decorations<'a>(
&'a self,
viewport_ranges: RangeSet<CharOffset>,
buffer_version: Option<BufferVersion>,
ctx: &'a AppContext,
) -> TextDecoration<'a> {
self.model
.as_ref(ctx)
.text_decoration_for_ranges(viewport_ranges, buffer_version, ctx)
}
}
impl RichTextAction<CodeEditorView> for CodeEditorViewAction {
fn scroll(delta: Pixels, axis: Axis) -> Option<Self> {
Some(match axis {
Axis::Horizontal => CodeEditorViewAction::ScrollHorizontal(delta),
Axis::Vertical => CodeEditorViewAction::ScrollVertical(delta),
})
}
fn user_typed(
chars: String,
view: &WeakViewHandle<CodeEditorView>,
ctx: &AppContext,
) -> Option<Self> {
let view = view.upgrade(ctx)?;
if !view.as_ref(ctx).is_editable(ctx) {
return None;
}
Some(CodeEditorViewAction::UserTyped(UserInput::new(chars)))
}
fn vim_user_typed(
chars: String,
parent_view: &WeakViewHandle<CodeEditorView>,
ctx: &AppContext,
) -> Option<Self> {
let view = parent_view.upgrade(ctx)?;
if !view.as_ref(ctx).is_editable(ctx) {
return None;
}
Some(CodeEditorViewAction::VimUserTyped(UserInput::new(chars)))
}
fn left_mouse_down(
location: Location,
modifiers: ModifiersState,
click_count: u32,
is_first_mouse: bool,
_view: &WeakViewHandle<CodeEditorView>,
_ctx: &AppContext,
) -> Option<Self> {
log::debug!(
"Clicked {click_count} times on {location:?}, cmd = {}, shift = {}",
modifiers.cmd,
modifiers.shift
);
// The first mouse down to bring focus to a Warp window will not have a corresponding mouse up.
// We ignore it, and they can click again.
if is_first_mouse {
return None;
}
match location {
Location::Text { char_offset, .. } => match click_count {
// TODO(CLD-558): We need to align render model with the content model offset.
1 if modifiers.shift => {
Some(CodeEditorViewAction::SelectionUpdate(char_offset + 1))
}
1 => Some(CodeEditorViewAction::SelectionStart {
offset: char_offset + 1,
modifiers,
}),
2 => Some(CodeEditorViewAction::SelectWord {
offset: char_offset + 1,
modifiers,
}),
3 => Some(CodeEditorViewAction::SelectLine {
offset: char_offset + 1,
modifiers,
}),
_ => None,
},
_ => None,
}
}
fn left_mouse_dragged(
location: Location,
_cmd: bool,
_shift: bool,
view: &WeakViewHandle<CodeEditorView>,
ctx: &AppContext,
) -> Option<Self> {
let view = view.upgrade(ctx)?;
match location {
Location::Text { char_offset, .. } if view.as_ref(ctx).is_selecting => {
Some(CodeEditorViewAction::SelectionUpdate(char_offset + 1))
}
_ => None,
}
}
fn left_mouse_up(
location: Location,
cmd: bool,
_shift: bool,
view: &WeakViewHandle<CodeEditorView>,
ctx: &AppContext,
) -> Vec<Self> {
let mut actions_to_dispatch = vec![];
let Some(view) = view.upgrade(ctx) else {
return actions_to_dispatch;
};
if view.as_ref(ctx).is_selecting {
actions_to_dispatch.push(CodeEditorViewAction::SelectionEnd);
} else if cmd {
if let Location::Text { char_offset, .. } = location {
actions_to_dispatch
.push(CodeEditorViewAction::MaybeClickOnHoveredLink(char_offset));
}
}
actions_to_dispatch
}
fn mouse_hovered(
location: Option<Location>,
_parent_view: &WeakViewHandle<CodeEditorView>,
cmd: bool,
is_covered: bool,
_ctx: &AppContext,
) -> Option<Self> {
match location {
Some(Location::Text {
char_offset,
clamped,
..
}) => Some(CodeEditorViewAction::MouseHovered {
offset: char_offset,
clamped,
cmd,
is_covered,
}),
// When the mouse moves outside the editor bounds (location is None),
// emit a clamped hover event to clear any active hover state.
_ => Some(CodeEditorViewAction::MouseHovered {
offset: CharOffset::default(),
clamped: true,
cmd,
is_covered,
}),
}
}
fn task_list_clicked(
_block_start: CharOffset,
_parent_view: &WeakViewHandle<CodeEditorView>,
_ctx: &AppContext,
) -> Option<Self> {
None
}
fn middle_mouse_down(_ctx: &AppContext) -> Option<Self> {
None
}
fn right_mouse_down(
location: Location,
_parent_view: &WeakViewHandle<CodeEditorView>,
_ctx: &AppContext,
) -> Option<Self> {
match location {
Location::Text { char_offset, .. } => Some(CodeEditorViewAction::RightMouseDown {
offset: char_offset + 1,
}),
_ => None,
}
}
}