Compare commits
13 Commits
copy_line
...
fallback_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab450e39d | ||
|
|
4fb540d6d2 | ||
|
|
1e2b0fcab6 | ||
|
|
0af690080b | ||
|
|
dd52fb58fe | ||
|
|
913b9296d7 | ||
|
|
5c9363b1c4 | ||
|
|
cd9bcc7f09 | ||
|
|
65759d4316 | ||
|
|
ddd50aabba | ||
|
|
34bf6ebba6 | ||
|
|
a6956eebcb | ||
|
|
8b0ec287a5 |
@@ -220,6 +220,8 @@
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
"c": "vim::PushChange",
|
||||
@@ -353,9 +355,7 @@
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
@@ -377,6 +377,8 @@
|
||||
{
|
||||
"context": "vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
|
||||
@@ -321,46 +321,62 @@ impl AgentConfiguration {
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu.header("Compatible APIs").entry(
|
||||
"OpenAI",
|
||||
None,
|
||||
{
|
||||
let workspace =
|
||||
workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AddLlmProviderModal::toggle(
|
||||
LlmCompatibleProvider::OpenAi,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.header("Compatible APIs").entry("OpenAI", None, {
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AddLlmProviderModal::toggle(
|
||||
LlmCompatibleProvider::OpenAi,
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -365,6 +365,8 @@ actions!(
|
||||
ConvertToLowerCase,
|
||||
/// Toggles the case of selected text.
|
||||
ConvertToOppositeCase,
|
||||
/// Converts selected text to sentence case.
|
||||
ConvertToSentenceCase,
|
||||
/// Converts selected text to snake_case.
|
||||
ConvertToSnakeCase,
|
||||
/// Converts selected text to Title Case.
|
||||
|
||||
@@ -10878,17 +10878,6 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_text(window, cx, |text| {
|
||||
let has_upper_case_characters = text.chars().any(|c| c.is_uppercase());
|
||||
if has_upper_case_characters {
|
||||
text.to_lowercase()
|
||||
} else {
|
||||
text.to_uppercase()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn manipulate_immutable_lines<Fn>(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -11144,6 +11133,26 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_to_sentence_case(
|
||||
&mut self,
|
||||
_: &ConvertToSentenceCase,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.manipulate_text(window, cx, |text| text.to_case(Case::Sentence))
|
||||
}
|
||||
|
||||
pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.manipulate_text(window, cx, |text| {
|
||||
let has_upper_case_characters = text.chars().any(|c| c.is_uppercase());
|
||||
if has_upper_case_characters {
|
||||
text.to_lowercase()
|
||||
} else {
|
||||
text.to_uppercase()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_to_rot13(
|
||||
&mut self,
|
||||
_: &ConvertToRot13,
|
||||
@@ -16968,7 +16977,7 @@ impl Editor {
|
||||
now: Instant,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> Option<TransactionId> {
|
||||
self.end_selection(window, cx);
|
||||
if let Some(tx_id) = self
|
||||
.buffer
|
||||
@@ -16978,7 +16987,10 @@ impl Editor {
|
||||
.insert_transaction(tx_id, self.selections.disjoint_anchors());
|
||||
cx.emit(EditorEvent::TransactionBegun {
|
||||
transaction_id: tx_id,
|
||||
})
|
||||
});
|
||||
Some(tx_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17006,6 +17018,17 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modify_transaction_selection_history(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
modify: impl FnOnce(&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)),
|
||||
) -> bool {
|
||||
self.selection_history
|
||||
.transaction_mut(transaction_id)
|
||||
.map(modify)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selection_mark_mode {
|
||||
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
|
||||
@@ -4724,6 +4724,23 @@ async fn test_toggle_case(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
«implement-windows-supportˇ»
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
«Implement windows supportˇ»
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_manipulate_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -16864,7 +16881,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let cols = 4;
|
||||
|
||||
@@ -230,7 +230,6 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::sort_lines_case_insensitive);
|
||||
register_action(editor, window, Editor::reverse_lines);
|
||||
register_action(editor, window, Editor::shuffle_lines);
|
||||
register_action(editor, window, Editor::toggle_case);
|
||||
register_action(editor, window, Editor::convert_indentation_to_spaces);
|
||||
register_action(editor, window, Editor::convert_indentation_to_tabs);
|
||||
register_action(editor, window, Editor::convert_to_upper_case);
|
||||
@@ -241,6 +240,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::convert_to_upper_camel_case);
|
||||
register_action(editor, window, Editor::convert_to_lower_camel_case);
|
||||
register_action(editor, window, Editor::convert_to_opposite_case);
|
||||
register_action(editor, window, Editor::convert_to_sentence_case);
|
||||
register_action(editor, window, Editor::toggle_case);
|
||||
register_action(editor, window, Editor::convert_to_rot13);
|
||||
register_action(editor, window, Editor::convert_to_rot47);
|
||||
register_action(editor, window, Editor::delete_to_previous_word_start);
|
||||
|
||||
@@ -12,6 +12,7 @@ use language::{self, Buffer, Point};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
ops::Range,
|
||||
pin::pin,
|
||||
sync::Arc,
|
||||
@@ -45,38 +46,60 @@ impl TextDiffView {
|
||||
) -> Option<Task<Result<Entity<Self>>>> {
|
||||
let source_editor = diff_data.editor.clone();
|
||||
|
||||
let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| {
|
||||
let selection_data = source_editor.update(cx, |editor, cx| {
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let source_buffer = multibuffer.as_singleton()?.clone();
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
let buffer_snapshot = source_buffer.read(cx);
|
||||
let first_selection = selections.first()?;
|
||||
let selection_range = if first_selection.is_empty() {
|
||||
Point::new(0, 0)..buffer_snapshot.max_point()
|
||||
} else {
|
||||
first_selection.start..first_selection.end
|
||||
};
|
||||
let max_point = buffer_snapshot.max_point();
|
||||
|
||||
Some((source_buffer, selection_range))
|
||||
if first_selection.is_empty() {
|
||||
let full_range = Point::new(0, 0)..max_point;
|
||||
return Some((source_buffer, full_range));
|
||||
}
|
||||
|
||||
let start = first_selection.start;
|
||||
let end = first_selection.end;
|
||||
let expanded_start = Point::new(start.row, 0);
|
||||
|
||||
let expanded_end = if end.column > 0 {
|
||||
let next_row = end.row + 1;
|
||||
cmp::min(max_point, Point::new(next_row, 0))
|
||||
} else {
|
||||
end
|
||||
};
|
||||
Some((source_buffer, expanded_start..expanded_end))
|
||||
});
|
||||
|
||||
let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else {
|
||||
let Some((source_buffer, expanded_selection_range)) = selection_data else {
|
||||
log::warn!("There should always be at least one selection in Zed. This is a bug.");
|
||||
return None;
|
||||
};
|
||||
|
||||
let clipboard_text = diff_data.clipboard_text.clone();
|
||||
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
let diff_buffer = cx.new(|cx| {
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
let diff = BufferDiff::new(&source_buffer_snapshot.text, cx);
|
||||
diff
|
||||
source_editor.update(cx, |source_editor, cx| {
|
||||
source_editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(vec![
|
||||
expanded_selection_range.start..expanded_selection_range.end,
|
||||
]);
|
||||
})
|
||||
});
|
||||
|
||||
let clipboard_buffer =
|
||||
build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx);
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
let mut clipboard_text = diff_data.clipboard_text.clone();
|
||||
|
||||
if !clipboard_text.ends_with("\n") {
|
||||
clipboard_text.push_str("\n");
|
||||
}
|
||||
|
||||
let workspace = workspace.weak_handle();
|
||||
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
|
||||
let clipboard_buffer = build_clipboard_buffer(
|
||||
clipboard_text,
|
||||
&source_buffer,
|
||||
expanded_selection_range.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
|
||||
@@ -89,7 +112,7 @@ impl TextDiffView {
|
||||
clipboard_buffer,
|
||||
source_editor,
|
||||
source_buffer,
|
||||
selected_range,
|
||||
expanded_selection_range,
|
||||
diff_buffer,
|
||||
project,
|
||||
window,
|
||||
@@ -208,9 +231,9 @@ impl TextDiffView {
|
||||
}
|
||||
|
||||
fn build_clipboard_buffer(
|
||||
clipboard_text: String,
|
||||
text: String,
|
||||
source_buffer: &Entity<Buffer>,
|
||||
selected_range: Range<Point>,
|
||||
replacement_range: Range<Point>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Buffer> {
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
@@ -219,9 +242,9 @@ fn build_clipboard_buffer(
|
||||
let language = source_buffer.read(cx).language().cloned();
|
||||
buffer.set_language(language, cx);
|
||||
|
||||
let range_start = source_buffer_snapshot.point_to_offset(selected_range.start);
|
||||
let range_end = source_buffer_snapshot.point_to_offset(selected_range.end);
|
||||
buffer.edit([(range_start..range_end, clipboard_text)], None, cx);
|
||||
let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
|
||||
let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
|
||||
buffer.edit([(range_start..range_end, text)], None, cx);
|
||||
|
||||
buffer
|
||||
})
|
||||
@@ -293,7 +316,7 @@ impl Item for TextDiffView {
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Diff View Opened")
|
||||
Some("Selection Diff View Opened")
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -395,21 +418,13 @@ pub fn selection_location_text(editor: &Editor, cx: &App) -> Option<String> {
|
||||
let buffer_snapshot = buffer.snapshot(cx);
|
||||
let first_selection = editor.selections.disjoint.first()?;
|
||||
|
||||
let (start_row, start_column, end_row, end_column) =
|
||||
if first_selection.start == first_selection.end {
|
||||
let max_point = buffer_snapshot.max_point();
|
||||
(0, 0, max_point.row, max_point.column)
|
||||
} else {
|
||||
let selection_start = first_selection.start.to_point(&buffer_snapshot);
|
||||
let selection_end = first_selection.end.to_point(&buffer_snapshot);
|
||||
let selection_start = first_selection.start.to_point(&buffer_snapshot);
|
||||
let selection_end = first_selection.end.to_point(&buffer_snapshot);
|
||||
|
||||
(
|
||||
selection_start.row,
|
||||
selection_start.column,
|
||||
selection_end.row,
|
||||
selection_end.column,
|
||||
)
|
||||
};
|
||||
let start_row = selection_start.row;
|
||||
let start_column = selection_start.column;
|
||||
let end_row = selection_end.row;
|
||||
let end_column = selection_end.column;
|
||||
|
||||
let range_text = if start_row == end_row {
|
||||
format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1)
|
||||
@@ -435,14 +450,13 @@ impl Render for TextDiffView {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use editor::{actions, test::editor_test_context::assert_state_with_diff};
|
||||
use editor::test::editor_test_context::assert_state_with_diff;
|
||||
use gpui::{TestAppContext, VisualContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use unindent::unindent;
|
||||
use util::path;
|
||||
use util::{path, test::marked_text_ranges};
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
@@ -457,52 +471,236 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) {
|
||||
base_test(true, cx).await;
|
||||
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
||||
"def process_outgoing_inventory(items, warehouse_id):\n passˇ\n",
|
||||
&unindent(
|
||||
"
|
||||
- def process_incoming_inventory(items, warehouse_id):
|
||||
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
||||
pass
|
||||
",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
||||
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer(
|
||||
async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(false, cx).await;
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
||||
"«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n",
|
||||
&unindent(
|
||||
"
|
||||
- def process_incoming_inventory(items, warehouse_id):
|
||||
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
||||
pass
|
||||
",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
||||
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn base_test(select_all_text: bool, cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"«bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
"«bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
" «bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"« bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
" «bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
"« bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"«bˇ»b",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn base_test(
|
||||
project_root: &str,
|
||||
file_path: &str,
|
||||
clipboard_text: &str,
|
||||
editor_text: &str,
|
||||
expected_diff: &str,
|
||||
expected_tab_title: &str,
|
||||
expected_tab_tooltip: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let file_name = std::path::Path::new(file_path)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/test"),
|
||||
project_root,
|
||||
json!({
|
||||
"a": {
|
||||
"b": {
|
||||
"text.txt": "new line 1\nline 2\nnew line 3\nline 4"
|
||||
}
|
||||
}
|
||||
file_name: editor_text
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
let project = Project::test(fs, [project_root.as_ref()], cx).await;
|
||||
|
||||
let (workspace, mut cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/test/a/b/text.txt"), cx)
|
||||
})
|
||||
.update(cx, |project, cx| project.open_local_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx);
|
||||
|
||||
if select_all_text {
|
||||
editor.select_all(&actions::SelectAll, window, cx);
|
||||
}
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false);
|
||||
editor.set_text(unmarked_text, window, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
@@ -511,7 +709,7 @@ mod tests {
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
TextDiffView::open(
|
||||
&DiffClipboardWithSelectionData {
|
||||
clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(),
|
||||
clipboard_text: clipboard_text.to_string(),
|
||||
editor,
|
||||
},
|
||||
workspace,
|
||||
@@ -528,26 +726,14 @@ mod tests {
|
||||
assert_state_with_diff(
|
||||
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
|
||||
&mut cx,
|
||||
&unindent(
|
||||
"
|
||||
- old line 1
|
||||
+ ˇnew line 1
|
||||
line 2
|
||||
- old line 3
|
||||
+ new line 3
|
||||
line 4
|
||||
",
|
||||
),
|
||||
expected_diff,
|
||||
);
|
||||
|
||||
diff_view.read_with(cx, |diff_view, cx| {
|
||||
assert_eq!(
|
||||
diff_view.tab_content_text(0, cx),
|
||||
"Clipboard ↔ text.txt @ L1:1-L5:1"
|
||||
);
|
||||
assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title);
|
||||
assert_eq!(
|
||||
diff_view.tab_tooltip_text(cx).unwrap(),
|
||||
format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1"))
|
||||
expected_tab_tooltip
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1334,7 +1334,6 @@ impl Element for Div {
|
||||
} else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() {
|
||||
let mut state = scroll_handle.0.borrow_mut();
|
||||
state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len());
|
||||
state.bounds = bounds;
|
||||
for child_layout_id in &request_layout.child_layout_ids {
|
||||
let child_bounds = window.layout_bounds(*child_layout_id);
|
||||
child_min = child_min.min(&child_bounds.origin);
|
||||
@@ -1706,6 +1705,7 @@ impl Interactivity {
|
||||
|
||||
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
|
||||
scroll_handle_state.max_offset = scroll_max;
|
||||
scroll_handle_state.bounds = bounds;
|
||||
}
|
||||
|
||||
*scroll_offset
|
||||
@@ -3007,11 +3007,6 @@ impl ScrollHandle {
|
||||
self.0.borrow().bounds
|
||||
}
|
||||
|
||||
/// Set the bounds into which this child is painted
|
||||
pub(super) fn set_bounds(&self, bounds: Bounds<Pixels>) {
|
||||
self.0.borrow_mut().bounds = bounds;
|
||||
}
|
||||
|
||||
/// Get the bounds for a specific child.
|
||||
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
|
||||
self.0.borrow().child_bounds.get(ix).cloned()
|
||||
|
||||
@@ -295,9 +295,8 @@ impl Element for UniformList {
|
||||
bounds.bottom_right() - point(border.right + padding.right, border.bottom),
|
||||
);
|
||||
|
||||
let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
|
||||
let mut scroll_state = scroll_handle.0.borrow_mut();
|
||||
scroll_state.base_handle.set_bounds(bounds);
|
||||
let y_flipped = if let Some(scroll_handle) = &self.scroll_handle {
|
||||
let scroll_state = scroll_handle.0.borrow();
|
||||
scroll_state.y_flipped
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -845,9 +845,15 @@ impl crate::Keystroke {
|
||||
{
|
||||
if key.is_ascii_graphic() {
|
||||
key_utf8.to_lowercase()
|
||||
// map ctrl-a to a
|
||||
} else if key_utf32 <= 0x1f {
|
||||
((key_utf32 as u8 + 0x60) as char).to_string()
|
||||
// map ctrl-a to `a`
|
||||
// ctrl-0..9 may emit control codes like ctrl-[, but
|
||||
// we don't want to map them to `[`
|
||||
} else if key_utf32 <= 0x1f
|
||||
&& !name.chars().next().is_some_and(|c| c.is_ascii_digit())
|
||||
{
|
||||
((key_utf32 as u8 + 0x40) as char)
|
||||
.to_ascii_lowercase()
|
||||
.to_string()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
|
||||
@@ -228,16 +228,17 @@ impl Render for BufferSearchBar {
|
||||
if in_replace {
|
||||
key_context.add("in_replace");
|
||||
}
|
||||
let editor_border = if self.query_error.is_some() {
|
||||
let query_border = if self.query_error.is_some() {
|
||||
Color::Error.color(cx)
|
||||
} else {
|
||||
cx.theme().colors().border
|
||||
};
|
||||
let replacement_border = cx.theme().colors().border;
|
||||
|
||||
let container_width = window.viewport_size().width;
|
||||
let input_width = SearchInputWidth::calc_width(container_width);
|
||||
|
||||
let input_base_styles = || {
|
||||
let input_base_styles = |border_color| {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.w(input_width)
|
||||
@@ -246,7 +247,7 @@ impl Render for BufferSearchBar {
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(editor_border)
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
};
|
||||
|
||||
@@ -256,7 +257,7 @@ impl Render for BufferSearchBar {
|
||||
el.child(Label::new("Find in results").color(Color::Hint))
|
||||
})
|
||||
.child(
|
||||
input_base_styles()
|
||||
input_base_styles(query_border)
|
||||
.id("editor-scroll")
|
||||
.track_scroll(&self.editor_scroll_handle)
|
||||
.child(self.render_text_input(&self.query_editor, color_override, cx))
|
||||
@@ -430,11 +431,13 @@ impl Render for BufferSearchBar {
|
||||
let replace_line = should_show_replace_input.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(input_base_styles().child(self.render_text_input(
|
||||
&self.replacement_editor,
|
||||
None,
|
||||
cx,
|
||||
)))
|
||||
.child(
|
||||
input_base_styles(replacement_border).child(self.render_text_input(
|
||||
&self.replacement_editor,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_64()
|
||||
@@ -700,7 +703,11 @@ impl BufferSearchBar {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
let query_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor
|
||||
});
|
||||
cx.subscribe_in(&query_editor, window, Self::on_query_editor_event)
|
||||
.detach();
|
||||
let replacement_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
@@ -771,6 +778,7 @@ impl BufferSearchBar {
|
||||
|
||||
pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.dismissed = true;
|
||||
self.query_error = None;
|
||||
for searchable_item in self.searchable_items_with_matches.keys() {
|
||||
if let Some(searchable_item) =
|
||||
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
|
||||
|
||||
@@ -195,6 +195,7 @@ pub struct ProjectSearch {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum InputPanel {
|
||||
Query,
|
||||
Replacement,
|
||||
Exclude,
|
||||
Include,
|
||||
}
|
||||
@@ -1962,7 +1963,7 @@ impl Render for ProjectSearchBar {
|
||||
MultipleInputs,
|
||||
}
|
||||
|
||||
let input_base_styles = |base_style: BaseStyle| {
|
||||
let input_base_styles = |base_style: BaseStyle, panel: InputPanel| {
|
||||
h_flex()
|
||||
.min_w_32()
|
||||
.map(|div| match base_style {
|
||||
@@ -1974,11 +1975,11 @@ impl Render for ProjectSearchBar {
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.border_1()
|
||||
.border_color(search.border_color_for(InputPanel::Query, cx))
|
||||
.border_color(search.border_color_for(panel, cx))
|
||||
.rounded_lg()
|
||||
};
|
||||
|
||||
let query_column = input_base_styles(BaseStyle::SingleInput)
|
||||
let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query)
|
||||
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
@@ -2167,7 +2168,7 @@ impl Render for ProjectSearchBar {
|
||||
.child(h_flex().min_w_64().child(mode_column).child(matches_column));
|
||||
|
||||
let replace_line = search.replace_enabled.then(|| {
|
||||
let replace_column = input_base_styles(BaseStyle::SingleInput)
|
||||
let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement)
|
||||
.child(self.render_text_input(&search.replacement_editor, cx));
|
||||
|
||||
let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
|
||||
@@ -2241,7 +2242,7 @@ impl Render for ProjectSearchBar {
|
||||
.gap_2()
|
||||
.w(input_width)
|
||||
.child(
|
||||
input_base_styles(BaseStyle::MultipleInputs)
|
||||
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
@@ -2251,7 +2252,7 @@ impl Render for ProjectSearchBar {
|
||||
.child(self.render_text_input(&search.included_files_editor, cx)),
|
||||
)
|
||||
.child(
|
||||
input_base_styles(BaseStyle::MultipleInputs)
|
||||
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude)
|
||||
.on_action(cx.listener(|this, action, window, cx| {
|
||||
this.previous_history_query(action, window, cx)
|
||||
}))
|
||||
|
||||
@@ -41,16 +41,14 @@ pub trait Summary: Clone {
|
||||
fn add_summary(&mut self, summary: &Self, cx: &Self::Context);
|
||||
}
|
||||
|
||||
/// This type exists because we can't implement Summary for () without causing
|
||||
/// type resolution errors
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Unit;
|
||||
|
||||
impl Summary for Unit {
|
||||
/// Catch-all implementation for when you need something that implements [`Summary`] without a specific type.
|
||||
/// We implement it on a &'static, as that avoids blanket impl collisions with `impl<T: Summary> Dimension for T`
|
||||
/// (as we also need unit type to be a fill-in dimension)
|
||||
impl Summary for &'static () {
|
||||
type Context = ();
|
||||
|
||||
fn zero(_: &()) -> Self {
|
||||
Unit
|
||||
&()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, _: &Self, _: &()) {}
|
||||
|
||||
@@ -430,6 +430,7 @@ impl TerminalView {
|
||||
|
||||
fn settings_changed(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = TerminalSettings::get_global(cx);
|
||||
let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs;
|
||||
self.show_breadcrumbs = settings.toolbar.breadcrumbs;
|
||||
|
||||
let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
|
||||
@@ -441,6 +442,9 @@ impl TerminalView {
|
||||
});
|
||||
}
|
||||
|
||||
if breadcrumb_visibility_changed {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use editor::{
|
||||
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
|
||||
display_map::ToDisplayPoint,
|
||||
};
|
||||
use gpui::{Action, App, AppContext as _, Context, Global, Window, actions};
|
||||
use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Window, actions};
|
||||
use itertools::Itertools;
|
||||
use language::Point;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -202,6 +202,7 @@ actions!(
|
||||
ArgumentRequired
|
||||
]
|
||||
);
|
||||
|
||||
/// Opens the specified file for editing.
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
@@ -209,6 +210,13 @@ struct VimEdit {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
struct VimNorm {
|
||||
pub range: Option<CommandRange>,
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WrappedAction(Box<dyn Action>);
|
||||
|
||||
@@ -447,6 +455,81 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
});
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| {
|
||||
let keystrokes = action
|
||||
.command
|
||||
.chars()
|
||||
.map(|c| Keystroke::parse(&c.to_string()).unwrap())
|
||||
.collect();
|
||||
vim.switch_mode(Mode::Normal, true, window, cx);
|
||||
let initial_selections = vim.update_editor(window, cx, |_, editor, _, _| {
|
||||
editor.selections.disjoint_anchors()
|
||||
});
|
||||
if let Some(range) = &action.range {
|
||||
let result = vim.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
let range = range.buffer_range(vim, editor, window, cx)?;
|
||||
editor.change_selections(
|
||||
SelectionEffects::no_scroll().nav_history(false),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges(
|
||||
(range.start.0..=range.end.0)
|
||||
.map(|line| Point::new(line, 0)..Point::new(line, 0)),
|
||||
);
|
||||
},
|
||||
);
|
||||
anyhow::Ok(())
|
||||
});
|
||||
if let Some(Err(err)) = result {
|
||||
log::error!("Error selecting range: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(workspace) = vim.workspace(window) else {
|
||||
return;
|
||||
};
|
||||
let task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.send_keystrokes_impl(keystrokes, window, cx)
|
||||
});
|
||||
let had_range = action.range.is_some();
|
||||
|
||||
cx.spawn_in(window, async move |vim, cx| {
|
||||
task.await;
|
||||
vim.update_in(cx, |vim, window, cx| {
|
||||
vim.update_editor(window, cx, |_, editor, window, cx| {
|
||||
if had_range {
|
||||
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges([s.newest_anchor().range()]);
|
||||
})
|
||||
}
|
||||
});
|
||||
if matches!(vim.mode, Mode::Insert | Mode::Replace) {
|
||||
vim.normal_before(&Default::default(), window, cx);
|
||||
} else {
|
||||
vim.switch_mode(Mode::Normal, true, window, cx);
|
||||
}
|
||||
vim.update_editor(window, cx, |_, editor, _, cx| {
|
||||
if let Some(first_sel) = initial_selections {
|
||||
if let Some(tx_id) = editor
|
||||
.buffer()
|
||||
.update(cx, |multi, cx| multi.last_transaction_id(cx))
|
||||
{
|
||||
let last_sel = editor.selections.disjoint_anchors();
|
||||
editor.modify_transaction_selection_history(tx_id, |old| {
|
||||
old.0 = first_sel;
|
||||
old.1 = Some(last_sel);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {
|
||||
let Some(workspace) = vim.workspace(window) else {
|
||||
return;
|
||||
@@ -675,14 +758,15 @@ impl VimCommand {
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if !args.is_empty() {
|
||||
|
||||
let action = if args.is_empty() {
|
||||
action
|
||||
} else {
|
||||
// if command does not accept args and we have args then we should do no action
|
||||
if let Some(args_fn) = &self.args {
|
||||
args_fn.deref()(action, args)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if let Some(range) = range {
|
||||
self.args.as_ref()?(action, args)?
|
||||
};
|
||||
|
||||
if let Some(range) = range {
|
||||
self.range.as_ref().and_then(|f| f(action, range))
|
||||
} else {
|
||||
Some(action)
|
||||
@@ -1061,6 +1145,27 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
save_intent: Some(SaveIntent::Skip),
|
||||
close_pinned: true,
|
||||
}),
|
||||
VimCommand::new(
|
||||
("norm", "al"),
|
||||
VimNorm {
|
||||
command: "".into(),
|
||||
range: None,
|
||||
},
|
||||
)
|
||||
.args(|_, args| {
|
||||
Some(
|
||||
VimNorm {
|
||||
command: args,
|
||||
range: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
})
|
||||
.range(|action, range| {
|
||||
let mut action: VimNorm = action.as_any().downcast_ref::<VimNorm>().unwrap().clone();
|
||||
action.range.replace(range.clone());
|
||||
Some(Box::new(action))
|
||||
}),
|
||||
VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(),
|
||||
VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(),
|
||||
@@ -2298,4 +2403,78 @@ mod test {
|
||||
});
|
||||
assert!(mark.is_none())
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_normal_command(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
The quick
|
||||
brown« fox
|
||||
jumpsˇ» over
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(": n o r m space w C w o r d")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick
|
||||
brown word
|
||||
jumps worˇd
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick
|
||||
brown word
|
||||
jumps tesˇt
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a")
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
The quick
|
||||
brown word
|
||||
lˇaumps test
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇThe quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy dog
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes("c i w M y escape").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
Mˇy quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy dog
|
||||
"});
|
||||
|
||||
cx.simulate_shared_keystrokes(": n o r m space u").await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
ˇThe quick
|
||||
brown fox
|
||||
jumps over
|
||||
the lazy dog
|
||||
"});
|
||||
// Once ctrl-v to input character literals is added there should be a test for redo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,28 @@ use gpui::{Context, Window};
|
||||
use language::{CharClassifier, CharKind};
|
||||
use text::SelectionGoal;
|
||||
|
||||
use crate::{Vim, motion::Motion, state::Mode};
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::{Motion, right},
|
||||
state::Mode,
|
||||
};
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
/// Switches to normal mode after the cursor (Helix-style).
|
||||
HelixNormalAfter
|
||||
HelixNormalAfter,
|
||||
/// Inserts at the beginning of the selection.
|
||||
HelixInsert,
|
||||
/// Appends at the end of the selection.
|
||||
HelixAppend,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
Vim::action(editor, cx, Vim::helix_normal_after);
|
||||
Vim::action(editor, cx, Vim::helix_insert);
|
||||
Vim::action(editor, cx, Vim::helix_append);
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
@@ -299,6 +309,38 @@ impl Vim {
|
||||
_ => self.helix_move_and_collapse(motion, times, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.start_recording(cx);
|
||||
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|_map, selection| {
|
||||
// In helix normal mode, move cursor to start of selection and collapse
|
||||
if !selection.is_empty() {
|
||||
selection.collapse_to(selection.start, SelectionGoal::None);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
self.switch_mode(Mode::Insert, false, window, cx);
|
||||
}
|
||||
|
||||
fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.start_recording(cx);
|
||||
self.switch_mode(Mode::Insert, false, window, cx);
|
||||
self.update_editor(window, cx, |_, editor, window, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let point = if selection.is_empty() {
|
||||
right(map, selection.head(), 1)
|
||||
} else {
|
||||
selection.end
|
||||
};
|
||||
selection.collapse_to(point, SelectionGoal::None);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -497,4 +539,68 @@ mod test {
|
||||
|
||||
cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«The ˇ»quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("i");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
ˇThe quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::Insert,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_append(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
// test from the end of the selection
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«Theˇ» quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("a");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
Theˇ quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::Insert,
|
||||
);
|
||||
|
||||
// test from the beginning of the selection
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
«ˇThe» quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("a");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
Theˇ quick brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::Insert,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
fn normal_before(
|
||||
pub(crate) fn normal_before(
|
||||
&mut self,
|
||||
action: &NormalBefore,
|
||||
window: &mut Window,
|
||||
|
||||
64
crates/vim/test_data/test_normal_command.json
Normal file
64
crates/vim/test_data/test_normal_command.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{"Put":{"state":"The quick\nbrown« fox\njumpsˇ» over\nthe lazy dog\n"}}
|
||||
{"Key":":"}
|
||||
{"Key":"n"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"m"}
|
||||
{"Key":"space"}
|
||||
{"Key":"w"}
|
||||
{"Key":"C"}
|
||||
{"Key":"w"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"d"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"The quick\nbrown word\njumps worˇd\nthe lazy dog\n","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"n"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"m"}
|
||||
{"Key":"space"}
|
||||
{"Key":"_"}
|
||||
{"Key":"w"}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"t"}
|
||||
{"Key":"e"}
|
||||
{"Key":"s"}
|
||||
{"Key":"t"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"The quick\nbrown word\njumps tesˇt\nthe lazy dog\n","mode":"Normal"}}
|
||||
{"Key":"_"}
|
||||
{"Key":"l"}
|
||||
{"Key":"v"}
|
||||
{"Key":"l"}
|
||||
{"Key":":"}
|
||||
{"Key":"n"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"m"}
|
||||
{"Key":"space"}
|
||||
{"Key":"s"}
|
||||
{"Key":"l"}
|
||||
{"Key":"a"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"The quick\nbrown word\nlˇaumps test\nthe lazy dog\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n"}}
|
||||
{"Key":"c"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"M"}
|
||||
{"Key":"y"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"Mˇy quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}}
|
||||
{"Key":":"}
|
||||
{"Key":"n"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"m"}
|
||||
{"Key":"space"}
|
||||
{"Key":"u"}
|
||||
{"Key":"enter"}
|
||||
{"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}}
|
||||
@@ -934,6 +934,10 @@ impl Render for PanelButtons {
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(
|
||||
has_buttons && dock.position == DockPosition::Bottom,
|
||||
|this| this.child(Divider::vertical().color(DividerColor::Border)),
|
||||
)
|
||||
.children(buttons)
|
||||
.when(has_buttons && dock.position == DockPosition::Left, |this| {
|
||||
this.child(Divider::vertical().color(DividerColor::Border))
|
||||
|
||||
@@ -32,7 +32,7 @@ use futures::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
oneshot,
|
||||
},
|
||||
future::try_join_all,
|
||||
future::{Shared, try_join_all},
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
|
||||
@@ -87,7 +87,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
cmp,
|
||||
collections::hash_map::DefaultHasher,
|
||||
collections::{VecDeque, hash_map::DefaultHasher},
|
||||
env,
|
||||
hash::{Hash, Hasher},
|
||||
path::{Path, PathBuf},
|
||||
@@ -1043,6 +1043,13 @@ type PromptForOpenPath = Box<
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
|
||||
>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct DispatchingKeystrokes {
|
||||
dispatched: HashSet<Vec<Keystroke>>,
|
||||
queue: VecDeque<Keystroke>,
|
||||
task: Option<Shared<Task<()>>>,
|
||||
}
|
||||
|
||||
/// Collects everything project-related for a certain window opened.
|
||||
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
|
||||
///
|
||||
@@ -1080,7 +1087,7 @@ pub struct Workspace {
|
||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||
database_id: Option<WorkspaceId>,
|
||||
app_state: Arc<AppState>,
|
||||
dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
|
||||
dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_apply_leader_updates: Task<Result<()>>,
|
||||
_observe_current_user: Task<Result<()>>,
|
||||
@@ -2311,49 +2318,65 @@ impl Workspace {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut state = self.dispatching_keystrokes.borrow_mut();
|
||||
if !state.0.insert(action.0.clone()) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
let mut keystrokes: Vec<Keystroke> = action
|
||||
let keystrokes: Vec<Keystroke> = action
|
||||
.0
|
||||
.split(' ')
|
||||
.flat_map(|k| Keystroke::parse(k).log_err())
|
||||
.collect();
|
||||
keystrokes.reverse();
|
||||
let _ = self.send_keystrokes_impl(keystrokes, window, cx);
|
||||
}
|
||||
|
||||
state.1.append(&mut keystrokes);
|
||||
drop(state);
|
||||
pub fn send_keystrokes_impl(
|
||||
&mut self,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Shared<Task<()>> {
|
||||
let mut state = self.dispatching_keystrokes.borrow_mut();
|
||||
if !state.dispatched.insert(keystrokes.clone()) {
|
||||
cx.propagate();
|
||||
return state.task.clone().unwrap();
|
||||
}
|
||||
|
||||
state.queue.extend(keystrokes);
|
||||
|
||||
let keystrokes = self.dispatching_keystrokes.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
// limit to 100 keystrokes to avoid infinite recursion.
|
||||
for _ in 0..100 {
|
||||
let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
|
||||
keystrokes.borrow_mut().0.clear();
|
||||
return Ok(());
|
||||
};
|
||||
cx.update(|window, cx| {
|
||||
let focused = window.focused(cx);
|
||||
window.dispatch_keystroke(keystroke.clone(), cx);
|
||||
if window.focused(cx) != focused {
|
||||
// dispatch_keystroke may cause the focus to change.
|
||||
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
|
||||
// And we need that to happen before the next keystroke to keep vim mode happy...
|
||||
// (Note that the tests always do this implicitly, so you must manually test with something like:
|
||||
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
|
||||
// )
|
||||
window.draw(cx).clear();
|
||||
if state.task.is_none() {
|
||||
state.task = Some(
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
// limit to 100 keystrokes to avoid infinite recursion.
|
||||
for _ in 0..100 {
|
||||
let mut state = keystrokes.borrow_mut();
|
||||
let Some(keystroke) = state.queue.pop_front() else {
|
||||
state.dispatched.clear();
|
||||
state.task.take();
|
||||
return;
|
||||
};
|
||||
drop(state);
|
||||
cx.update(|window, cx| {
|
||||
let focused = window.focused(cx);
|
||||
window.dispatch_keystroke(keystroke.clone(), cx);
|
||||
if window.focused(cx) != focused {
|
||||
// dispatch_keystroke may cause the focus to change.
|
||||
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
|
||||
// And we need that to happen before the next keystroke to keep vim mode happy...
|
||||
// (Note that the tests always do this implicitly, so you must manually test with something like:
|
||||
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
|
||||
// )
|
||||
window.draw(cx).clear();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
*keystrokes.borrow_mut() = Default::default();
|
||||
anyhow::bail!("over 100 keystrokes passed to send_keystrokes");
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
*keystrokes.borrow_mut() = Default::default();
|
||||
log::error!("over 100 keystrokes passed to send_keystrokes");
|
||||
})
|
||||
.shared(),
|
||||
);
|
||||
}
|
||||
state.task.clone().unwrap()
|
||||
}
|
||||
|
||||
fn save_all_internal(
|
||||
|
||||
@@ -62,7 +62,7 @@ use std::{
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit};
|
||||
use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};
|
||||
use text::{LineEnding, Rope};
|
||||
use util::{
|
||||
ResultExt,
|
||||
@@ -407,12 +407,12 @@ struct LocalRepositoryEntry {
|
||||
}
|
||||
|
||||
impl sum_tree::Item for LocalRepositoryEntry {
|
||||
type Summary = PathSummary<Unit>;
|
||||
type Summary = PathSummary<&'static ()>;
|
||||
|
||||
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
|
||||
PathSummary {
|
||||
max_path: self.work_directory.path_key().0,
|
||||
item_summary: Unit,
|
||||
item_summary: &(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,12 +425,6 @@ impl KeyedItem for LocalRepositoryEntry {
|
||||
}
|
||||
}
|
||||
|
||||
//impl LocalRepositoryEntry {
|
||||
// pub fn repo(&self) -> &Arc<dyn GitRepository> {
|
||||
// &self.repo_ptr
|
||||
// }
|
||||
//}
|
||||
|
||||
impl Deref for LocalRepositoryEntry {
|
||||
type Target = WorkDirectory;
|
||||
|
||||
@@ -5417,7 +5411,7 @@ impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeekTarget<'a, PathSummary<Unit>, TraversalProgress<'a>> for TraversalTarget<'_> {
|
||||
impl<'a> SeekTarget<'a, PathSummary<&'static ()>, TraversalProgress<'a>> for TraversalTarget<'_> {
|
||||
fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering {
|
||||
self.cmp_progress(cursor_location)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user