mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Merge branch 'main' into storybook
This commit is contained in:
commit
362b1a44be
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -8848,12 +8848,14 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"editor",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"language",
|
||||
"language_selector",
|
||||
"log",
|
||||
"lsp",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
"project",
|
||||
|
@ -316,6 +316,7 @@
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
@ -326,15 +327,12 @@
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "editor::JoinLines",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
],
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
@ -448,13 +446,12 @@
|
||||
],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"shift-r": "vim::SubstituteLine",
|
||||
"c": "vim::Substitute",
|
||||
"~": "vim::ChangeCase",
|
||||
"shift-i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
],
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
|
@ -15,7 +15,7 @@ use gpui::{
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use std::any::Any;
|
||||
use std::any::{Any, TypeId};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemHandle},
|
||||
register_followable_item,
|
||||
@ -189,6 +189,21 @@ impl View for ChannelView {
|
||||
}
|
||||
|
||||
impl Item for ChannelView {
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a ViewHandle<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<&'a AnyViewHandle> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle)
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(&self.editor)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content<V: 'static>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
|
@ -771,7 +771,7 @@ impl CollabTitlebarItem {
|
||||
})
|
||||
.with_tooltip::<ToggleUserMenu>(
|
||||
0,
|
||||
"Toggle user menu".to_owned(),
|
||||
"Toggle User Menu".to_owned(),
|
||||
Some(Box::new(ToggleUserMenu)),
|
||||
tooltip,
|
||||
cx,
|
||||
|
@ -555,67 +555,6 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
|
||||
}
|
||||
|
||||
/// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn reverse_find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(
|
||||
self.reverse_chars_at(from),
|
||||
target.chars().rev().collect(),
|
||||
condition,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_internal<'a>(
|
||||
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
|
||||
target: Vec<char>,
|
||||
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
// List of partial matches with the index of the last seen character in target and the starting point of the match
|
||||
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
|
||||
iterator
|
||||
.take_while(move |(ch, point)| condition(*ch, *point))
|
||||
.filter_map(move |(ch, point)| {
|
||||
if Some(&ch) == target.get(0) {
|
||||
partial_matches.push((0, point));
|
||||
}
|
||||
|
||||
let mut found = None;
|
||||
// Keep partial matches that have the correct next character
|
||||
partial_matches.retain_mut(|(match_position, match_start)| {
|
||||
if target.get(*match_position) == Some(&ch) {
|
||||
*match_position += 1;
|
||||
if *match_position == target.len() {
|
||||
found = Some(match_start.clone());
|
||||
// This match is completed. No need to keep tracking it
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
found
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
@ -933,7 +872,7 @@ pub mod tests {
|
||||
use smol::stream::StreamExt;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
|
||||
use util::test::{marked_text_ranges, sample_text};
|
||||
use Bias::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
@ -1744,32 +1683,6 @@ pub mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_internal() {
|
||||
assert("This is a ˇtest of find internal", "test");
|
||||
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
|
||||
|
||||
fn assert(marked_text: &str, target: &str) {
|
||||
let (text, expected_offsets) = marked_text_offsets(marked_text);
|
||||
|
||||
let chars = text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
|
||||
let target = target.chars();
|
||||
|
||||
assert_eq!(
|
||||
expected_offsets
|
||||
.into_iter()
|
||||
.map(|offset| offset as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
|
||||
.map(|point| point.column())
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn syntax_chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
|
@ -572,7 +572,7 @@ pub struct Editor {
|
||||
project: Option<ModelHandle<Project>>,
|
||||
focused: bool,
|
||||
blink_manager: ModelHandle<BlinkManager>,
|
||||
show_local_selections: bool,
|
||||
pub show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
||||
show_gutter: bool,
|
||||
@ -2269,10 +2269,6 @@ impl Editor {
|
||||
if self.read_only {
|
||||
return;
|
||||
}
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text });
|
||||
return;
|
||||
}
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut brace_inserted = false;
|
||||
@ -3207,17 +3203,30 @@ impl Editor {
|
||||
.count();
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let mut range_to_replace: Option<Range<isize>> = None;
|
||||
let mut ranges = Vec::new();
|
||||
for selection in &selections {
|
||||
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
|
||||
let start = selection.start.saturating_sub(lookbehind);
|
||||
let end = selection.end + lookahead;
|
||||
if selection.id == newest_selection.id {
|
||||
range_to_replace = Some(
|
||||
((start + common_prefix_len) as isize - selection.start as isize)
|
||||
..(end as isize - selection.start as isize),
|
||||
);
|
||||
}
|
||||
ranges.push(start + common_prefix_len..end);
|
||||
} else {
|
||||
common_prefix_len = 0;
|
||||
ranges.clear();
|
||||
ranges.extend(selections.iter().map(|s| {
|
||||
if s.id == newest_selection.id {
|
||||
range_to_replace = Some(
|
||||
old_range.start.to_offset_utf16(&snapshot).0 as isize
|
||||
- selection.start as isize
|
||||
..old_range.end.to_offset_utf16(&snapshot).0 as isize
|
||||
- selection.start as isize,
|
||||
);
|
||||
old_range.clone()
|
||||
} else {
|
||||
s.start..s.end
|
||||
@ -3228,6 +3237,11 @@ impl Editor {
|
||||
}
|
||||
let text = &text[common_prefix_len..];
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = text.to_string();
|
||||
@ -3685,6 +3699,10 @@ impl Editor {
|
||||
|
||||
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
|
||||
}
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: suggestion.text.to_string().into(),
|
||||
});
|
||||
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
|
||||
cx.notify();
|
||||
true
|
||||
@ -8436,6 +8454,41 @@ impl Editor {
|
||||
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
|
||||
&self.inlay_hint_cache
|
||||
}
|
||||
|
||||
pub fn replay_insert_event(
|
||||
&mut self,
|
||||
text: &str,
|
||||
relative_utf16_range: Option<Range<isize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
if let Some(relative_utf16_range) = relative_utf16_range {
|
||||
let selections = self.selections.all::<OffsetUtf16>(cx);
|
||||
self.change_selections(None, cx, |s| {
|
||||
let new_ranges = selections.into_iter().map(|range| {
|
||||
let start = OffsetUtf16(
|
||||
range
|
||||
.head()
|
||||
.0
|
||||
.saturating_add_signed(relative_utf16_range.start),
|
||||
);
|
||||
let end = OffsetUtf16(
|
||||
range
|
||||
.head()
|
||||
.0
|
||||
.saturating_add_signed(relative_utf16_range.end),
|
||||
);
|
||||
start..end
|
||||
});
|
||||
s.select_ranges(new_ranges);
|
||||
});
|
||||
}
|
||||
|
||||
self.handle_input(text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn document_to_inlay_range(
|
||||
@ -8524,6 +8577,10 @@ pub enum Event {
|
||||
InputIgnored {
|
||||
text: Arc<str>,
|
||||
},
|
||||
InputHandled {
|
||||
utf16_range_to_replace: Option<Range<isize>>,
|
||||
text: Arc<str>,
|
||||
},
|
||||
ExcerptsAdded {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
predecessor: ExcerptId,
|
||||
@ -8744,8 +8801,12 @@ impl View for Editor {
|
||||
text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
if this.input_enabled {
|
||||
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
|
||||
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
|
||||
Some(this.selection_replacement_ranges(range_utf16, cx))
|
||||
@ -8753,20 +8814,38 @@ impl View for Editor {
|
||||
this.marked_text_ranges(cx)
|
||||
};
|
||||
|
||||
let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
|
||||
let newest_selection_id = this.selections.newest_anchor().id;
|
||||
this.selections
|
||||
.all::<OffsetUtf16>(cx)
|
||||
.iter()
|
||||
.zip(ranges_to_replace.iter())
|
||||
.find_map(|(selection, range)| {
|
||||
if selection.id == newest_selection_id {
|
||||
Some(
|
||||
(range.start.0 as isize - selection.head().0 as isize)
|
||||
..(range.end.0 as isize - selection.head().0 as isize),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
if let Some(new_selected_ranges) = new_selected_ranges {
|
||||
this.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(new_selected_ranges)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.handle_input(text, cx);
|
||||
});
|
||||
|
||||
if !self.input_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(transaction) = self.ime_transaction {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.group_until_transaction(transaction, cx);
|
||||
@ -8784,6 +8863,7 @@ impl View for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if !self.input_enabled {
|
||||
cx.emit(Event::InputIgnored { text: text.into() });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -8808,6 +8888,29 @@ impl View for Editor {
|
||||
None
|
||||
};
|
||||
|
||||
let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
|
||||
let newest_selection_id = this.selections.newest_anchor().id;
|
||||
this.selections
|
||||
.all::<OffsetUtf16>(cx)
|
||||
.iter()
|
||||
.zip(ranges_to_replace.iter())
|
||||
.find_map(|(selection, range)| {
|
||||
if selection.id == newest_selection_id {
|
||||
Some(
|
||||
(range.start.0 as isize - selection.head().0 as isize)
|
||||
..(range.end.0 as isize - selection.head().0 as isize),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
cx.emit(Event::InputHandled {
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
text: text.into(),
|
||||
});
|
||||
|
||||
if let Some(ranges) = ranges_to_replace {
|
||||
this.change_selections(None, cx, |s| s.select_ranges(ranges));
|
||||
}
|
||||
|
@ -7807,7 +7807,7 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
||||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range
|
||||
fn handle_completion_request<'a>(
|
||||
pub fn handle_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
|
@ -42,14 +42,14 @@ impl View for FeedbackInfoText {
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<OpenZedCommunityRepo, _>(0, cx, |state, _| {
|
||||
let contained_text = if state.hovered() {
|
||||
let style = if state.hovered() {
|
||||
&theme.feedback.link_text_hover
|
||||
} else {
|
||||
&theme.feedback.link_text_default
|
||||
};
|
||||
|
||||
Label::new("community repo", contained_text.text.clone())
|
||||
Label::new("community repo", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
@ -64,6 +64,8 @@ impl View for FeedbackInfoText {
|
||||
.with_soft_wrap(false)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.feedback.info_text_default.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.clipped()
|
||||
|
@ -3387,14 +3387,12 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> {
|
||||
handler_depth = Some(contexts.len())
|
||||
}
|
||||
|
||||
let action_contexts = if let Some(depth) = handler_depth {
|
||||
&contexts[depth..]
|
||||
} else {
|
||||
&contexts
|
||||
};
|
||||
|
||||
let handler_depth = handler_depth.unwrap_or(0);
|
||||
(0..=handler_depth).find_map(|depth| {
|
||||
let contexts = &contexts[depth..];
|
||||
self.keystroke_matcher
|
||||
.keystrokes_for_action(action, action_contexts)
|
||||
.keystrokes_for_action(action, contexts)
|
||||
})
|
||||
}
|
||||
|
||||
fn notify_if_view_ancestors_change(&mut self, view_id: usize) {
|
||||
@ -6422,7 +6420,7 @@ mod tests {
|
||||
|
||||
#[crate::test(self)]
|
||||
fn test_keystrokes_for_action(cx: &mut TestAppContext) {
|
||||
actions!(test, [Action1, Action2, GlobalAction]);
|
||||
actions!(test, [Action1, Action2, Action3, GlobalAction]);
|
||||
|
||||
struct View1 {
|
||||
child: ViewHandle<View2>,
|
||||
@ -6465,12 +6463,14 @@ mod tests {
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.add_action(|_: &mut View1, _: &Action1, _cx| {});
|
||||
cx.add_action(|_: &mut View1, _: &Action3, _cx| {});
|
||||
cx.add_action(|_: &mut View2, _: &Action2, _cx| {});
|
||||
cx.add_global_action(|_: &GlobalAction, _| {});
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("a", Action1, Some("View1")),
|
||||
Binding::new("b", Action2, Some("View1 > View2")),
|
||||
Binding::new("c", GlobalAction, Some("View3")), // View 3 does not exist
|
||||
Binding::new("c", Action3, Some("View2")),
|
||||
Binding::new("d", GlobalAction, Some("View3")), // View 3 does not exist
|
||||
]);
|
||||
});
|
||||
|
||||
@ -6493,6 +6493,14 @@ mod tests {
|
||||
.as_slice(),
|
||||
&[Keystroke::parse("b").unwrap()]
|
||||
);
|
||||
assert_eq!(layout_cx.keystrokes_for_action(view_1.id(), &Action3), None);
|
||||
assert_eq!(
|
||||
layout_cx
|
||||
.keystrokes_for_action(view_2.id(), &Action3)
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
&[Keystroke::parse("c").unwrap()]
|
||||
);
|
||||
|
||||
// The 'a' keystroke propagates up the view tree from view_2
|
||||
// to view_1. The action, Action1, is handled by view_1.
|
||||
@ -6520,7 +6528,8 @@ mod tests {
|
||||
&available_actions(window.into(), view_1.id(), cx),
|
||||
&[
|
||||
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||
("test::GlobalAction", vec![])
|
||||
("test::Action3", vec![]),
|
||||
("test::GlobalAction", vec![]),
|
||||
],
|
||||
);
|
||||
|
||||
@ -6530,6 +6539,7 @@ mod tests {
|
||||
&[
|
||||
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
|
||||
("test::Action2", vec![Keystroke::parse("b").unwrap()]),
|
||||
("test::Action3", vec![Keystroke::parse("c").unwrap()]),
|
||||
("test::GlobalAction", vec![]),
|
||||
],
|
||||
);
|
||||
|
@ -1147,7 +1147,7 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.is_fullscreen
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
|
||||
pub fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
|
||||
if let Some(view_id) = view_id {
|
||||
self.halt_action_dispatch = false;
|
||||
self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {
|
||||
|
@ -52,6 +52,7 @@ impl View for ActiveBufferLanguage {
|
||||
} else {
|
||||
"Unknown".to_string()
|
||||
};
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
|
||||
let theme = &theme::current(cx).workspace.status_bar;
|
||||
@ -68,6 +69,7 @@ impl View for ActiveBufferLanguage {
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(0, "Select Language", None, theme.tooltip.clone(), cx)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
|
@ -63,6 +63,7 @@ fn build_bridge(swift_target: &SwiftTarget) {
|
||||
let swift_target_folder = swift_target_folder();
|
||||
if !Command::new("swift")
|
||||
.arg("build")
|
||||
.arg("--disable-automatic-resolution")
|
||||
.args(["--configuration", &env::var("PROFILE").unwrap()])
|
||||
.args(["--triple", &swift_target.target.triple])
|
||||
.args(["--build-path".into(), swift_target_folder])
|
||||
|
@ -20,12 +20,11 @@ use gpui::{
|
||||
Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
search::{PathMatcher, SearchInputs, SearchQuery},
|
||||
Entry, Project,
|
||||
};
|
||||
use semantic_index::SemanticIndex;
|
||||
use semantic_index::{SemanticIndex, SemanticIndexStatus};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
@ -116,7 +115,7 @@ pub struct ProjectSearchView {
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
results_editor: ViewHandle<Editor>,
|
||||
semantic_state: Option<SemanticSearchState>,
|
||||
semantic_state: Option<SemanticState>,
|
||||
semantic_permissioned: Option<bool>,
|
||||
search_options: SearchOptions,
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
@ -129,9 +128,9 @@ pub struct ProjectSearchView {
|
||||
current_mode: SearchMode,
|
||||
}
|
||||
|
||||
struct SemanticSearchState {
|
||||
pending_file_count: usize,
|
||||
_progress_task: Task<()>,
|
||||
struct SemanticState {
|
||||
index_status: SemanticIndexStatus,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
pub struct ProjectSearchBar {
|
||||
@ -230,7 +229,7 @@ impl ProjectSearch {
|
||||
self.search_id += 1;
|
||||
self.match_ranges.clear();
|
||||
self.search_history.add(inputs.as_str().to_string());
|
||||
self.no_results = Some(true);
|
||||
self.no_results = None;
|
||||
self.pending_search = Some(cx.spawn(|this, mut cx| async move {
|
||||
let results = search?.await.log_err()?;
|
||||
let matches = results
|
||||
@ -238,9 +237,10 @@ impl ProjectSearch {
|
||||
.map(|result| (result.buffer, vec![result.range.start..result.range.start]));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.no_results = Some(true);
|
||||
this.excerpts.update(cx, |excerpts, cx| {
|
||||
excerpts.clear(cx);
|
||||
})
|
||||
});
|
||||
});
|
||||
for (buffer, ranges) in matches {
|
||||
let mut match_ranges = this.update(&mut cx, |this, cx| {
|
||||
@ -315,15 +315,20 @@ impl View for ProjectSearchView {
|
||||
}
|
||||
};
|
||||
|
||||
let semantic_status = if let Some(semantic) = &self.semantic_state {
|
||||
if semantic.pending_file_count > 0 {
|
||||
format!("Remaining files to index: {}", semantic.pending_file_count)
|
||||
let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
|
||||
let status = semantic.index_status;
|
||||
match status {
|
||||
SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
|
||||
SemanticIndexStatus::Indexing { remaining_files } => {
|
||||
if remaining_files == 0 {
|
||||
Some(format!("Indexing..."))
|
||||
} else {
|
||||
"Indexing complete".to_string()
|
||||
Some(format!("Remaining files to index: {}", remaining_files))
|
||||
}
|
||||
} else {
|
||||
"Indexing: ...".to_string()
|
||||
};
|
||||
}
|
||||
SemanticIndexStatus::NotIndexed => None,
|
||||
}
|
||||
});
|
||||
|
||||
let minor_text = if let Some(no_results) = model.no_results {
|
||||
if model.pending_search.is_none() && no_results {
|
||||
@ -333,12 +338,16 @@ impl View for ProjectSearchView {
|
||||
}
|
||||
} else {
|
||||
match current_mode {
|
||||
SearchMode::Semantic => vec![
|
||||
"".to_owned(),
|
||||
semantic_status,
|
||||
"Simply explain the code you are looking to find.".to_owned(),
|
||||
"ex. 'prompt user for permissions to index their project'".to_owned(),
|
||||
],
|
||||
SearchMode::Semantic => {
|
||||
let mut minor_text = Vec::new();
|
||||
minor_text.push("".into());
|
||||
minor_text.extend(semantic_status);
|
||||
minor_text.push("Simply explain the code you are looking to find.".into());
|
||||
minor_text.push(
|
||||
"ex. 'prompt user for permissions to index their project'".into(),
|
||||
);
|
||||
minor_text
|
||||
}
|
||||
_ => vec![
|
||||
"".to_owned(),
|
||||
"Include/exclude specific paths with the filter option.".to_owned(),
|
||||
@ -634,41 +643,29 @@ impl ProjectSearchView {
|
||||
|
||||
let project = self.model.read(cx).project.clone();
|
||||
|
||||
let mut pending_file_count_rx = semantic_index.update(cx, |semantic_index, cx| {
|
||||
semantic_index.update(cx, |semantic_index, cx| {
|
||||
semantic_index
|
||||
.index_project(project.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
semantic_index.pending_file_count(&project).unwrap()
|
||||
});
|
||||
|
||||
cx.spawn(|search_view, mut cx| async move {
|
||||
search_view.update(&mut cx, |search_view, cx| {
|
||||
cx.notify();
|
||||
let pending_file_count = *pending_file_count_rx.borrow();
|
||||
search_view.semantic_state = Some(SemanticSearchState {
|
||||
pending_file_count,
|
||||
_progress_task: cx.spawn(|search_view, mut cx| async move {
|
||||
while let Some(count) = pending_file_count_rx.recv().await {
|
||||
search_view
|
||||
.update(&mut cx, |search_view, cx| {
|
||||
if let Some(semantic_search_state) =
|
||||
&mut search_view.semantic_state
|
||||
{
|
||||
semantic_search_state.pending_file_count = count;
|
||||
cx.notify();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
self.semantic_state = Some(SemanticState {
|
||||
index_status: semantic_index.read(cx).status(&project),
|
||||
_subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic_index_changed(
|
||||
&mut self,
|
||||
semantic_index: ModelHandle<SemanticIndex>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.model.read(cx).project.clone();
|
||||
if let Some(semantic_state) = self.semantic_state.as_mut() {
|
||||
semantic_state.index_status = semantic_index.read(cx).status(&project);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@ -867,7 +864,7 @@ impl ProjectSearchView {
|
||||
SemanticIndex::global(cx)
|
||||
.map(|semantic| {
|
||||
let project = self.model.read(cx).project.clone();
|
||||
semantic.update(cx, |this, cx| this.project_previously_indexed(project, cx))
|
||||
semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
|
||||
})
|
||||
.unwrap_or(Task::ready(Ok(false)))
|
||||
}
|
||||
@ -952,11 +949,7 @@ impl ProjectSearchView {
|
||||
let mode = self.current_mode;
|
||||
match mode {
|
||||
SearchMode::Semantic => {
|
||||
if let Some(semantic) = &mut self.semantic_state {
|
||||
if semantic.pending_file_count > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.semantic_state.is_some() {
|
||||
if let Some(query) = self.build_search_query(cx) {
|
||||
self.model
|
||||
.update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
|
||||
|
@ -18,7 +18,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Instant, SystemTime},
|
||||
time::SystemTime,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
|
||||
@ -232,7 +232,6 @@ impl VectorDatabase {
|
||||
|
||||
let file_id = db.last_insert_rowid();
|
||||
|
||||
let t0 = Instant::now();
|
||||
let mut query = db.prepare(
|
||||
"
|
||||
INSERT INTO spans
|
||||
@ -240,10 +239,6 @@ impl VectorDatabase {
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
",
|
||||
)?;
|
||||
log::trace!(
|
||||
"Preparing Query Took: {:?} milliseconds",
|
||||
t0.elapsed().as_millis()
|
||||
);
|
||||
|
||||
for span in spans {
|
||||
query.execute(params![
|
||||
|
@ -35,6 +35,7 @@ use util::{
|
||||
paths::EMBEDDINGS_DIR,
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::WorkspaceCreated;
|
||||
|
||||
const SEMANTIC_INDEX_VERSION: usize = 10;
|
||||
const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
|
||||
@ -57,6 +58,35 @@ pub fn init(
|
||||
return;
|
||||
}
|
||||
|
||||
cx.subscribe_global::<WorkspaceCreated, _>({
|
||||
move |event, cx| {
|
||||
let Some(semantic_index) = SemanticIndex::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let workspace = &event.0;
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
if project.read(cx).is_local() {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let previously_indexed = semantic_index
|
||||
.update(&mut cx, |index, cx| {
|
||||
index.project_previously_indexed(&project, cx)
|
||||
})
|
||||
.await?;
|
||||
if previously_indexed {
|
||||
semantic_index
|
||||
.update(&mut cx, |index, cx| index.index_project(project, cx))
|
||||
.await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let semantic_index = SemanticIndex::new(
|
||||
fs,
|
||||
@ -79,6 +109,13 @@ pub fn init(
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum SemanticIndexStatus {
|
||||
NotIndexed,
|
||||
Indexed,
|
||||
Indexing { remaining_files: usize },
|
||||
}
|
||||
|
||||
pub struct SemanticIndex {
|
||||
fs: Arc<dyn Fs>,
|
||||
db: VectorDatabase,
|
||||
@ -94,7 +131,9 @@ struct ProjectState {
|
||||
worktrees: HashMap<WorktreeId, WorktreeState>,
|
||||
pending_file_count_rx: watch::Receiver<usize>,
|
||||
pending_file_count_tx: Arc<Mutex<watch::Sender<usize>>>,
|
||||
pending_index: usize,
|
||||
_subscription: gpui::Subscription,
|
||||
_observe_pending_file_count: Task<()>,
|
||||
}
|
||||
|
||||
enum WorktreeState {
|
||||
@ -103,6 +142,10 @@ enum WorktreeState {
|
||||
}
|
||||
|
||||
impl WorktreeState {
|
||||
fn is_registered(&self) -> bool {
|
||||
matches!(self, Self::Registered(_))
|
||||
}
|
||||
|
||||
fn paths_changed(
|
||||
&mut self,
|
||||
changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
|
||||
@ -177,14 +220,25 @@ impl JobHandle {
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
fn new(subscription: gpui::Subscription) -> Self {
|
||||
fn new(subscription: gpui::Subscription, cx: &mut ModelContext<SemanticIndex>) -> Self {
|
||||
let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0);
|
||||
let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx));
|
||||
Self {
|
||||
worktrees: Default::default(),
|
||||
pending_file_count_rx,
|
||||
pending_file_count_rx: pending_file_count_rx.clone(),
|
||||
pending_file_count_tx,
|
||||
pending_index: 0,
|
||||
_subscription: subscription,
|
||||
_observe_pending_file_count: cx.spawn_weak({
|
||||
let mut pending_file_count_rx = pending_file_count_rx.clone();
|
||||
|this, mut cx| async move {
|
||||
while let Some(_) = pending_file_count_rx.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +281,25 @@ impl SemanticIndex {
|
||||
&& *RELEASE_CHANNEL != ReleaseChannel::Stable
|
||||
}
|
||||
|
||||
pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
|
||||
if let Some(project_state) = self.projects.get(&project.downgrade()) {
|
||||
if project_state
|
||||
.worktrees
|
||||
.values()
|
||||
.all(|worktree| worktree.is_registered())
|
||||
&& project_state.pending_index == 0
|
||||
{
|
||||
SemanticIndexStatus::Indexed
|
||||
} else {
|
||||
SemanticIndexStatus::Indexing {
|
||||
remaining_files: project_state.pending_file_count_rx.borrow().clone(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SemanticIndexStatus::NotIndexed
|
||||
}
|
||||
}
|
||||
|
||||
async fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
database_path: PathBuf,
|
||||
@ -356,7 +429,7 @@ impl SemanticIndex {
|
||||
|
||||
pub fn project_previously_indexed(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
project: &ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let worktrees_indexed_previously = project
|
||||
@ -630,6 +703,10 @@ impl SemanticIndex {
|
||||
let database =
|
||||
VectorDatabase::new(fs.clone(), db_path.clone(), cx.background()).await?;
|
||||
|
||||
if phrase.len() == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let phrase_embedding = embedding_provider
|
||||
.embed_batch(vec![phrase])
|
||||
.await?
|
||||
@ -770,13 +847,15 @@ impl SemanticIndex {
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
self.projects
|
||||
.insert(project.downgrade(), ProjectState::new(subscription));
|
||||
let project_state = ProjectState::new(subscription, cx);
|
||||
self.projects.insert(project.downgrade(), project_state);
|
||||
self.project_worktrees_changed(project.clone(), cx);
|
||||
}
|
||||
let project_state = &self.projects[&project.downgrade()];
|
||||
let mut pending_file_count_rx = project_state.pending_file_count_rx.clone();
|
||||
let project_state = self.projects.get_mut(&project.downgrade()).unwrap();
|
||||
project_state.pending_index += 1;
|
||||
cx.notify();
|
||||
|
||||
let mut pending_file_count_rx = project_state.pending_file_count_rx.clone();
|
||||
let db = self.db.clone();
|
||||
let language_registry = self.language_registry.clone();
|
||||
let parsing_files_tx = self.parsing_files_tx.clone();
|
||||
@ -887,6 +966,16 @@ impl SemanticIndex {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let project_state = this
|
||||
.projects
|
||||
.get_mut(&project.downgrade())
|
||||
.ok_or_else(|| anyhow!("project was dropped"))?;
|
||||
project_state.pending_index -= 1;
|
||||
cx.notify();
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ language_selector = { path = "../language_selector"}
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
@ -47,3 +48,4 @@ util = { path = "../util", features = ["test-support"] }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
@ -34,6 +34,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
||||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.recording = false;
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
vim.active_editor = None;
|
||||
|
@ -11,8 +11,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
@ -20,7 +21,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
||||
});
|
||||
});
|
||||
});
|
||||
state.switch_mode(Mode::Normal, false, cx);
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::{cmp, sync::Arc};
|
||||
use std::cmp;
|
||||
|
||||
use editor::{
|
||||
char_kind,
|
||||
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
|
||||
movement::{self, FindRange},
|
||||
movement::{self, find_boundary, find_preceding_boundary, FindRange},
|
||||
Bias, CharKind, DisplayPoint, ToOffset,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, WindowContext};
|
||||
@ -37,8 +37,8 @@ pub enum Motion {
|
||||
StartOfDocument,
|
||||
EndOfDocument,
|
||||
Matching,
|
||||
FindForward { before: bool, text: Arc<str> },
|
||||
FindBackward { after: bool, text: Arc<str> },
|
||||
FindForward { before: bool, char: char },
|
||||
FindBackward { after: bool, char: char },
|
||||
NextLineStart,
|
||||
}
|
||||
|
||||
@ -65,9 +65,9 @@ struct PreviousWordStart {
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Up {
|
||||
pub(crate) struct Up {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
pub(crate) display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@ -93,9 +93,9 @@ struct EndOfLine {
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StartOfLine {
|
||||
pub struct StartOfLine {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
pub(crate) display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@ -233,25 +233,25 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||
|
||||
fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
|
||||
let find = match Vim::read(cx).workspace_state.last_find.clone() {
|
||||
Some(Motion::FindForward { before, text }) => {
|
||||
Some(Motion::FindForward { before, char }) => {
|
||||
if backwards {
|
||||
Motion::FindBackward {
|
||||
after: before,
|
||||
text,
|
||||
char,
|
||||
}
|
||||
} else {
|
||||
Motion::FindForward { before, text }
|
||||
Motion::FindForward { before, char }
|
||||
}
|
||||
}
|
||||
|
||||
Some(Motion::FindBackward { after, text }) => {
|
||||
Some(Motion::FindBackward { after, char }) => {
|
||||
if backwards {
|
||||
Motion::FindForward {
|
||||
before: after,
|
||||
text,
|
||||
char,
|
||||
}
|
||||
} else {
|
||||
Motion::FindBackward { after, text }
|
||||
Motion::FindBackward { after, char }
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
@ -403,12 +403,12 @@ impl Motion {
|
||||
SelectionGoal::None,
|
||||
),
|
||||
Matching => (matching(map, point), SelectionGoal::None),
|
||||
FindForward { before, text } => (
|
||||
find_forward(map, point, *before, text.clone(), times),
|
||||
FindForward { before, char } => (
|
||||
find_forward(map, point, *before, *char, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
FindBackward { after, text } => (
|
||||
find_backward(map, point, *after, text.clone(), times),
|
||||
FindBackward { after, char } => (
|
||||
find_backward(map, point, *after, *char, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
|
||||
@ -793,44 +793,55 @@ fn find_forward(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
before: bool,
|
||||
target: Arc<str>,
|
||||
target: char,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
|
||||
.skip_while(|found_at| found_at == &from)
|
||||
.nth(times - 1)
|
||||
.map(|mut found| {
|
||||
if before {
|
||||
*found.column_mut() -= 1;
|
||||
found = map.clip_point(found, Bias::Right);
|
||||
found
|
||||
} else {
|
||||
let mut to = from;
|
||||
let mut found = false;
|
||||
|
||||
for _ in 0..times {
|
||||
found = false;
|
||||
to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
|
||||
found = right == target;
|
||||
found
|
||||
});
|
||||
}
|
||||
|
||||
if found {
|
||||
if before && to.column() > 0 {
|
||||
*to.column_mut() -= 1;
|
||||
map.clip_point(to, Bias::Left)
|
||||
} else {
|
||||
to
|
||||
}
|
||||
} else {
|
||||
from
|
||||
}
|
||||
})
|
||||
.unwrap_or(from)
|
||||
}
|
||||
|
||||
fn find_backward(
|
||||
map: &DisplaySnapshot,
|
||||
from: DisplayPoint,
|
||||
after: bool,
|
||||
target: Arc<str>,
|
||||
target: char,
|
||||
times: usize,
|
||||
) -> DisplayPoint {
|
||||
map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
|
||||
.skip_while(|found_at| found_at == &from)
|
||||
.nth(times - 1)
|
||||
.map(|mut found| {
|
||||
if after {
|
||||
*found.column_mut() += 1;
|
||||
found = map.clip_point(found, Bias::Left);
|
||||
found
|
||||
} else {
|
||||
found
|
||||
let mut to = from;
|
||||
|
||||
for _ in 0..times {
|
||||
to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
|
||||
}
|
||||
|
||||
if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
|
||||
if after {
|
||||
*to.column_mut() += 1;
|
||||
map.clip_point(to, Bias::Right)
|
||||
} else {
|
||||
to
|
||||
}
|
||||
} else {
|
||||
from
|
||||
}
|
||||
})
|
||||
.unwrap_or(from)
|
||||
}
|
||||
|
||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
|
@ -2,6 +2,7 @@ mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod paste;
|
||||
mod repeat;
|
||||
mod scroll;
|
||||
mod search;
|
||||
pub mod substitute;
|
||||
@ -34,6 +35,7 @@ actions!(
|
||||
vim,
|
||||
[
|
||||
InsertAfter,
|
||||
InsertBefore,
|
||||
InsertFirstNonWhitespace,
|
||||
InsertEndOfLine,
|
||||
InsertLineAbove,
|
||||
@ -44,32 +46,42 @@ actions!(
|
||||
DeleteToEndOfLine,
|
||||
Yank,
|
||||
ChangeCase,
|
||||
JoinLines,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
paste::init(cx);
|
||||
repeat::init(cx);
|
||||
scroll::init(cx);
|
||||
search::init(cx);
|
||||
substitute::init(cx);
|
||||
|
||||
cx.add_action(insert_after);
|
||||
cx.add_action(insert_before);
|
||||
cx.add_action(insert_first_non_whitespace);
|
||||
cx.add_action(insert_end_of_line);
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
substitute::init(cx);
|
||||
search::init(cx);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
change_motion(
|
||||
vim,
|
||||
@ -83,6 +95,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
@ -94,8 +107,26 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
})
|
||||
});
|
||||
scroll::init(cx);
|
||||
paste::init(cx);
|
||||
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let mut times = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
if vim.state().mode.is_visual() {
|
||||
times = 1;
|
||||
} else if times > 1 {
|
||||
// 2J joins two lines together (same as J or 1J)
|
||||
times -= 1;
|
||||
}
|
||||
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for _ in 0..times {
|
||||
editor.join_lines(&Default::default(), cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
@ -151,6 +182,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
|
||||
|
||||
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@ -162,12 +194,20 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_first_non_whitespace(
|
||||
_: &mut Workspace,
|
||||
_: &InsertFirstNonWhitespace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@ -184,6 +224,7 @@ fn insert_first_non_whitespace(
|
||||
|
||||
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@ -197,6 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
|
||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
@ -229,6 +271,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
|
||||
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
@ -260,6 +303,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
@ -780,6 +824,7 @@ mod test {
|
||||
#[gpui::test]
|
||||
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
for count in 1..=3 {
|
||||
let test_case = indoc! {"
|
||||
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
|
||||
|
@ -7,6 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
@ -21,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
|
||||
ranges.push(start..end);
|
||||
cursor_positions.push(start..start);
|
||||
}
|
||||
Mode::Visual | Mode::VisualBlock => {
|
||||
Mode::Visual => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
Mode::VisualBlock => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
if cursor_positions.len() == 0 {
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
}
|
||||
Mode::Insert | Mode::Normal => {
|
||||
let start = selection.start;
|
||||
let mut end = start;
|
||||
@ -96,6 +103,11 @@ mod test {
|
||||
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
|
||||
cx.assert_shared_state("ˇABc\n").await;
|
||||
|
||||
// works in visual block mode
|
||||
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
|
||||
cx.assert_shared_state("ˇAa\nBb\ncc").await;
|
||||
|
||||
// works with multiple cursors (zed only)
|
||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||
cx.simulate_keystroke("~");
|
||||
|
@ -4,6 +4,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::WindowContext;
|
||||
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
@ -37,6 +38,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
}
|
||||
|
||||
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
@ -28,6 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
427
crates/vim/src/normal/repeat.rs
Normal file
427
crates/vim/src/normal/repeat.rs
Normal file
@ -0,0 +1,427 @@
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
|
||||
fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
// skip so that we don't leave the character palette open
|
||||
if editor::ShowCharacterPalette.id() == action.id() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.replaying = false;
|
||||
vim.update_active_editor(cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.pop_number_operator(cx);
|
||||
|
||||
vim.workspace_state.replaying = true;
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((actions, editor, selection))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { cols } => {
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::Visual { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
visual_motion(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualBlock { rows, cols } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
if cols > 1 {
|
||||
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||
}
|
||||
}
|
||||
RecordedSelection::VisualLine { rows } => {
|
||||
visual_motion(
|
||||
Motion::Down {
|
||||
display_lines: false,
|
||||
},
|
||||
Some(rows as usize),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RecordedSelection::None => {}
|
||||
}
|
||||
|
||||
let window = cx.window();
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
for action in actions {
|
||||
match action {
|
||||
ReplayableAction::Action(action) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ReplayableAction::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => editor.update(&mut cx, |editor, cx| {
|
||||
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
||||
}),
|
||||
}?
|
||||
}
|
||||
window
|
||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use gpui::{executor::Deterministic, View};
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// "o"
|
||||
cx.set_shared_state("ˇhello").await;
|
||||
cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state("hello\nworlˇd").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nworld\nworlˇd").await;
|
||||
|
||||
// "d"
|
||||
cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
|
||||
cx.simulate_shared_keystrokes(["g", "g", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("ˇ\nworld\nrld").await;
|
||||
|
||||
// "p" (note that it pastes the current clipboard)
|
||||
cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
|
||||
cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
|
||||
|
||||
// "~" (note that counts apply to the action taken, not . itself)
|
||||
cx.set_shared_state("ˇthe quick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["2", "~", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE ˇquick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["3", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUIˇck brown fox").await;
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("hˇllo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
|
||||
// simulate brazilian input for ä.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
|
||||
editor.replace_text_in_range(None, "ä", cx);
|
||||
});
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
cx.assert_state("hˇällo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hˇäällo", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_completion(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let mut cx = VimTestContext::new_with_lsp(cx, true);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
onˇe
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
|
||||
let position = params.text_document_position.position;
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "first".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "second".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "second".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.simulate_keystrokes(["a", "."]);
|
||||
request.next().await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.secondˇ!
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["j", "."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.second!
|
||||
two.secondˇ!
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// single-line (3 columns)
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇo quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "w", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ˇops over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["f", "r", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o quick brown
|
||||
fox ops oveˇothe lazy dog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// visual
|
||||
cx.set_shared_state(indoc! {
|
||||
"the ˇquick brown
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the ˇumps over
|
||||
fox jumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the ˇumps over
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["w", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps ˇumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"the umps umps over
|
||||
the ˇog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// block mode (3 rows)
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇothe quick brown
|
||||
ofox jumps over
|
||||
othe lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"othe quick brown
|
||||
ofoxˇo jumps over
|
||||
otheo lazy dog"
|
||||
})
|
||||
.await;
|
||||
|
||||
// line mode
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇthe quick brown
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {
|
||||
"ˇo
|
||||
fox jumps over
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state(indoc! {
|
||||
"o
|
||||
ˇo
|
||||
the lazy dog"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ actions!(vim, [Substitute, SubstituteLine]);
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = vim.pop_number_operator(cx);
|
||||
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
||||
})
|
||||
@ -17,6 +18,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
use gpui::keymap_matcher::KeymapContext;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{keymap_matcher::KeymapContext, Action};
|
||||
use language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
@ -48,10 +50,61 @@ pub struct EditorState {
|
||||
pub operator_stack: Vec<Operator>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub enum RecordedSelection {
|
||||
#[default]
|
||||
None,
|
||||
Visual {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
},
|
||||
SingleLine {
|
||||
cols: u32,
|
||||
},
|
||||
VisualBlock {
|
||||
rows: u32,
|
||||
cols: u32,
|
||||
},
|
||||
VisualLine {
|
||||
rows: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct WorkspaceState {
|
||||
pub search: SearchState,
|
||||
pub last_find: Option<Motion>,
|
||||
|
||||
pub recording: bool,
|
||||
pub stop_recording_after_next_action: bool,
|
||||
pub replaying: bool,
|
||||
pub recorded_count: Option<usize>,
|
||||
pub recorded_actions: Vec<ReplayableAction>,
|
||||
pub recorded_selection: RecordedSelection,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReplayableAction {
|
||||
Action(Box<dyn Action>),
|
||||
Insertion {
|
||||
text: Arc<str>,
|
||||
utf16_range_to_replace: Option<Range<isize>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for ReplayableAction {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Action(action) => Self::Action(action.boxed_clone()),
|
||||
Self::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => Self::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: utf16_range_to_replace.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -286,6 +286,55 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_join_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
oneˇ two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one two threeˇ four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one
|
||||
two three fourˇ five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
@ -449,6 +498,13 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
fourteen char
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "shift-f", "e", "f", "r"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
fourteen•
|
||||
fourteen chaˇr
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
|
||||
use editor::test::{
|
||||
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::Future;
|
||||
use gpui::ContextHandle;
|
||||
use lsp::request;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
@ -124,6 +126,19 @@ impl<'a> VimTestContext<'a> {
|
||||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
self.cx.handle_request::<T, F, Fut>(handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
|
@ -18,17 +18,19 @@ use gpui::{
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Selection, SelectionGoal};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
use settings::{Setting, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, WorkspaceState};
|
||||
use std::sync::Arc;
|
||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
struct VimModeSetting(bool);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@ -102,6 +104,19 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
return true;
|
||||
}
|
||||
if let Some(handled_by) = handled_by {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
||||
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
if handled_by.namespace() == "vim" {
|
||||
return true;
|
||||
@ -156,7 +171,12 @@ impl Vim {
|
||||
}
|
||||
Event::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
}
|
||||
Event::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
_ => {}
|
||||
}));
|
||||
|
||||
@ -176,6 +196,27 @@ impl Vim {
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn record_insertion(
|
||||
text: &Arc<str>,
|
||||
range_to_replace: Option<Range<isize>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
});
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_active_editor<S>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
@ -184,6 +225,71 @@ impl Vim {
|
||||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
// ~, shift-j, x, shift-x, p
|
||||
// shift-c, shift-d, shift-i, i, a, o, shift-o, s
|
||||
// c, d
|
||||
// r
|
||||
|
||||
// TODO: shift-j?
|
||||
//
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
self.workspace_state.recorded_actions = Default::default();
|
||||
self.workspace_state.recorded_count =
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
Some(number)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let selections = self
|
||||
.active_editor
|
||||
.and_then(|editor| editor.upgrade(cx))
|
||||
.map(|editor| {
|
||||
let editor = editor.read(cx);
|
||||
(
|
||||
editor.selections.oldest::<Point>(cx),
|
||||
editor.selections.newest::<Point>(cx),
|
||||
)
|
||||
});
|
||||
|
||||
if let Some((oldest, newest)) = selections {
|
||||
self.workspace_state.recorded_selection = match self.state().mode {
|
||||
Mode::Visual if newest.end.row == newest.start.row => {
|
||||
RecordedSelection::SingleLine {
|
||||
cols: newest.end.column - newest.start.column,
|
||||
}
|
||||
}
|
||||
Mode::Visual => RecordedSelection::Visual {
|
||||
rows: newest.end.row - newest.start.row,
|
||||
cols: newest.end.column,
|
||||
},
|
||||
Mode::VisualLine => RecordedSelection::VisualLine {
|
||||
rows: newest.end.row - newest.start.row,
|
||||
},
|
||||
Mode::VisualBlock => RecordedSelection::VisualBlock {
|
||||
rows: newest.end.row.abs_diff(oldest.start.row),
|
||||
cols: newest.end.column.abs_diff(oldest.start.column),
|
||||
},
|
||||
_ => RecordedSelection::None,
|
||||
}
|
||||
} else {
|
||||
self.workspace_state.recorded_selection = RecordedSelection::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
@ -247,6 +353,12 @@ impl Vim {
|
||||
}
|
||||
|
||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||
if matches!(
|
||||
operator,
|
||||
Operator::Change | Operator::Delete | Operator::Replace
|
||||
) {
|
||||
self.start_recording(cx)
|
||||
};
|
||||
self.update_state(|state| state.operator_stack.push(operator));
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
@ -272,6 +384,12 @@ impl Vim {
|
||||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
if let Some(number) = self.workspace_state.recorded_count {
|
||||
return Some(number);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
return Some(number);
|
||||
@ -295,14 +413,20 @@ impl Vim {
|
||||
|
||||
match Vim::read(cx).active_operator() {
|
||||
Some(Operator::FindForward { before }) => {
|
||||
let find = Motion::FindForward { before, text };
|
||||
let find = Motion::FindForward {
|
||||
before,
|
||||
char: text.chars().next().unwrap(),
|
||||
};
|
||||
Vim::update(cx, |vim, _| {
|
||||
vim.workspace_state.last_find = Some(find.clone())
|
||||
});
|
||||
motion::motion(find, cx)
|
||||
}
|
||||
Some(Operator::FindBackward { after }) => {
|
||||
let find = Motion::FindBackward { after, text };
|
||||
let find = Motion::FindBackward {
|
||||
after,
|
||||
char: text.chars().next().unwrap(),
|
||||
};
|
||||
Vim::update(cx, |vim, _| {
|
||||
vim.workspace_state.last_find = Some(find.clone())
|
||||
});
|
||||
|
@ -277,6 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
|
||||
|
||||
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = editor.selections.line_mode;
|
||||
@ -339,6 +340,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
|
||||
|
||||
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
@ -16,3 +16,8 @@
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
|
||||
|
38
crates/vim/test_data/test_dot_repeat.json
Normal file
38
crates/vim/test_data/test_dot_repeat.json
Normal file
@ -0,0 +1,38 @@
|
||||
{"Put":{"state":"ˇhello"}}
|
||||
{"Key":"o"}
|
||||
{"Key":"w"}
|
||||
{"Key":"o"}
|
||||
{"Key":"r"}
|
||||
{"Key":"l"}
|
||||
{"Key":"d"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"hello\nworlˇd","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}}
|
||||
{"Key":"^"}
|
||||
{"Key":"d"}
|
||||
{"Key":"f"}
|
||||
{"Key":"o"}
|
||||
{"Key":"g"}
|
||||
{"Key":"g"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"ˇ\nworld\nrld","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"y"}
|
||||
{"Key":"y"}
|
||||
{"Key":"p"}
|
||||
{"Key":"shift-g"}
|
||||
{"Key":"y"}
|
||||
{"Key":"y"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇthe quick brown fox"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"~"}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE ˇquick brown fox"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUIˇck brown fox"}}
|
||||
{"Key":"."}
|
||||
{"Put":{"state":"THE QUICK ˇbrown fox"}}
|
13
crates/vim/test_data/test_join_lines.json
Normal file
13
crates/vim/test_data/test_join_lines.json
Normal file
@ -0,0 +1,13 @@
|
||||
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
|
||||
{"Key":"shift-j"}
|
||||
{"Get":{"state":"oneˇ two\nthree\nfour\nfive\nsix\n","mode":"Normal"}}
|
||||
{"Key":"3"}
|
||||
{"Key":"shift-j"}
|
||||
{"Get":{"state":"one two threeˇ four\nfive\nsix\n","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"v"}
|
||||
{"Key":"3"}
|
||||
{"Key":"j"}
|
||||
{"Key":"shift-j"}
|
||||
{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}}
|
51
crates/vim/test_data/test_repeat_visual.json
Normal file
51
crates/vim/test_data/test_repeat_visual.json
Normal file
@ -0,0 +1,51 @@
|
||||
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"s"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"w"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"f"}
|
||||
{"Key":"r"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}}
|
||||
{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"x"}
|
||||
{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"w"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"j"}
|
||||
{"Key":"j"}
|
||||
{"Key":"shift-i"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"4"}
|
||||
{"Key":"l"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"shift-r"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"."}
|
||||
{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}}
|
@ -53,3 +53,9 @@
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}
|
||||
{"Key":"j"}
|
||||
{"Key":"shift-f"}
|
||||
{"Key":"e"}
|
||||
{"Key":"f"}
|
||||
{"Key":"r"}
|
||||
{"Get":{"state":"fourteen \nfourteen chaˇr\n","mode":"Normal"}}
|
||||
|
@ -171,6 +171,7 @@ pub trait Item: View {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
|
||||
[
|
||||
(string)
|
||||
(template_string)
|
||||
] @string
|
||||
|
||||
[
|
||||
(jsx_element)
|
||||
|
@ -1,6 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
@ -164,31 +163,16 @@ async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &dyn NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
Some(LanguageServerBinary {
|
||||
path: node.binary_path().await.log_err()?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
log::error!("missing executable in directory {:?}", server_path);
|
||||
None
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -262,6 +262,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
|
@ -1,5 +1,10 @@
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
|
||||
[
|
||||
(string)
|
||||
(template_string)
|
||||
] @string
|
||||
|
||||
[
|
||||
(jsx_element)
|
||||
(jsx_fragment)
|
||||
|
@ -12,9 +12,6 @@ export default function feedback(): any {
|
||||
background: background(theme.highest, "on"),
|
||||
corner_radius: 6,
|
||||
border: border(theme.highest, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
@ -41,9 +38,15 @@ export default function feedback(): any {
|
||||
},
|
||||
}),
|
||||
button_margin: 8,
|
||||
info_text_default: text(theme.highest, "sans", "default", {
|
||||
info_text_default: {
|
||||
padding: {
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
...text(theme.highest, "sans", "default", {
|
||||
size: "xs",
|
||||
}),
|
||||
})
|
||||
},
|
||||
link_text_default: text(theme.highest, "sans", "default", {
|
||||
size: "xs",
|
||||
underline: true,
|
||||
|
@ -2,14 +2,14 @@ import { useTheme } from "../common"
|
||||
import { toggleable_icon_button } from "../component/icon_button"
|
||||
import { interactive, toggleable } from "../element"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
import { text_button } from "../component";
|
||||
import { text_button } from "../component"
|
||||
|
||||
export const toolbar = () => {
|
||||
const theme = useTheme()
|
||||
|
||||
return {
|
||||
height: 42,
|
||||
padding: { left: 4, right: 4 },
|
||||
padding: { left: 8, right: 8 },
|
||||
background: background(theme.highest),
|
||||
border: border(theme.highest, { bottom: true }),
|
||||
item_spacing: 4,
|
||||
@ -24,9 +24,9 @@ export const toolbar = () => {
|
||||
...text(theme.highest, "sans", "variant"),
|
||||
corner_radius: 6,
|
||||
padding: {
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
left: 4,
|
||||
right: 4,
|
||||
}
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
|
Loading…
Reference in New Issue
Block a user