mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-31 06:50:10 +00:00
commit
d4436277ee
20 changed files with 847 additions and 177 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
@ -2527,6 +2527,15 @@ dependencies = [
|
||||||
"hashbrown 0.9.1",
|
"hashbrown 0.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e"
|
||||||
|
dependencies = [
|
||||||
|
"unindent",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "infer"
|
name = "infer"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
@ -5553,9 +5562,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unindent"
|
name = "unindent"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
|
checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "universal-hash"
|
name = "universal-hash"
|
||||||
|
@ -5673,6 +5682,21 @@ version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vim"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"collections",
|
||||||
|
"editor",
|
||||||
|
"gpui",
|
||||||
|
"indoc",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"project",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -6003,6 +6027,7 @@ dependencies = [
|
||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
|
"vim",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub struct DisplayMap {
|
||||||
wrap_map: ModelHandle<WrapMap>,
|
wrap_map: ModelHandle<WrapMap>,
|
||||||
block_map: BlockMap,
|
block_map: BlockMap,
|
||||||
text_highlights: TextHighlights,
|
text_highlights: TextHighlights,
|
||||||
|
pub clip_at_line_ends: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for DisplayMap {
|
impl Entity for DisplayMap {
|
||||||
|
@ -67,6 +68,7 @@ impl DisplayMap {
|
||||||
wrap_map,
|
wrap_map,
|
||||||
block_map,
|
block_map,
|
||||||
text_highlights: Default::default(),
|
text_highlights: Default::default(),
|
||||||
|
clip_at_line_ends: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +89,7 @@ impl DisplayMap {
|
||||||
wraps_snapshot,
|
wraps_snapshot,
|
||||||
blocks_snapshot,
|
blocks_snapshot,
|
||||||
text_highlights: self.text_highlights.clone(),
|
text_highlights: self.text_highlights.clone(),
|
||||||
|
clip_at_line_ends: self.clip_at_line_ends,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +208,7 @@ pub struct DisplaySnapshot {
|
||||||
wraps_snapshot: wrap_map::WrapSnapshot,
|
wraps_snapshot: wrap_map::WrapSnapshot,
|
||||||
blocks_snapshot: block_map::BlockSnapshot,
|
blocks_snapshot: block_map::BlockSnapshot,
|
||||||
text_highlights: TextHighlights,
|
text_highlights: TextHighlights,
|
||||||
|
clip_at_line_ends: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplaySnapshot {
|
impl DisplaySnapshot {
|
||||||
|
@ -332,7 +336,12 @@ impl DisplaySnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||||
DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias))
|
let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
|
||||||
|
if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) {
|
||||||
|
clipped.column = clipped.column.saturating_sub(1);
|
||||||
|
clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left);
|
||||||
|
}
|
||||||
|
DisplayPoint(clipped)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn folds_in_range<'a, T>(
|
pub fn folds_in_range<'a, T>(
|
||||||
|
@ -488,19 +497,16 @@ impl ToDisplayPoint for Anchor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{movement, test::marked_display_snapshot};
|
||||||
movement,
|
|
||||||
test::{marked_text_ranges},
|
|
||||||
};
|
|
||||||
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
|
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
|
||||||
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
|
||||||
use rand::{prelude::*, Rng};
|
use rand::{prelude::*, Rng};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{env, sync::Arc};
|
use std::{env, sync::Arc};
|
||||||
use theme::SyntaxTheme;
|
use theme::SyntaxTheme;
|
||||||
use util::test::sample_text;
|
use util::test::{marked_text_ranges, sample_text};
|
||||||
use Bias::*;
|
use Bias::*;
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
|
@ -1133,49 +1139,70 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
|
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
|
||||||
use Bias::{Left, Right};
|
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
|
||||||
|
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
|
||||||
|
|
||||||
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
|
match bias {
|
||||||
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
|
Bias::Left => {
|
||||||
let buffer = MultiBuffer::build_simple(text, cx);
|
if shift_right {
|
||||||
|
*markers[1].column_mut() += 1;
|
||||||
|
}
|
||||||
|
|
||||||
let tab_size = 4;
|
assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
|
||||||
let font_cache = cx.font_cache();
|
}
|
||||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
Bias::Right => {
|
||||||
let font_id = font_cache
|
if shift_right {
|
||||||
.select_font(family_id, &Default::default())
|
*markers[0].column_mut() += 1;
|
||||||
.unwrap();
|
}
|
||||||
let font_size = 14.0;
|
|
||||||
let map = cx.add_model(|cx| {
|
|
||||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx)
|
|
||||||
});
|
|
||||||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
|
||||||
|
|
||||||
assert_eq!(map.text(), display_text);
|
|
||||||
for (input_column, bias, output_column) in vec![
|
|
||||||
("'a', '".len(), Left, "'a', '".len()),
|
|
||||||
("'a', '".len() + 1, Left, "'a', '".len()),
|
|
||||||
("'a', '".len() + 1, Right, "'a', 'α".len()),
|
|
||||||
("'a', 'α', ".len(), Left, "'a', 'α',".len()),
|
|
||||||
("'a', 'α', ".len(), Right, "'a', 'α', ".len()),
|
|
||||||
("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()),
|
|
||||||
("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()),
|
|
||||||
("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()),
|
|
||||||
("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()),
|
|
||||||
(
|
|
||||||
"'a', 'α', '✋', ".len(),
|
|
||||||
Right,
|
|
||||||
"'a', 'α', '✋', ".len(),
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
|
unmarked_snapshot.clip_point(dbg!(markers[0]), bias),
|
||||||
DisplayPoint::new(1, output_column as u32),
|
markers[1]
|
||||||
"clip_point(({}, {}))",
|
)
|
||||||
1,
|
}
|
||||||
input_column,
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
use Bias::{Left, Right};
|
||||||
|
assert("||α", false, Left, cx);
|
||||||
|
assert("||α", true, Left, cx);
|
||||||
|
assert("||α", false, Right, cx);
|
||||||
|
assert("|α|", true, Right, cx);
|
||||||
|
assert("||✋", false, Left, cx);
|
||||||
|
assert("||✋", true, Left, cx);
|
||||||
|
assert("||✋", false, Right, cx);
|
||||||
|
assert("|✋|", true, Right, cx);
|
||||||
|
assert("||🍐", false, Left, cx);
|
||||||
|
assert("||🍐", true, Left, cx);
|
||||||
|
assert("||🍐", false, Right, cx);
|
||||||
|
assert("|🍐|", true, Right, cx);
|
||||||
|
assert("||\t", false, Left, cx);
|
||||||
|
assert("||\t", true, Left, cx);
|
||||||
|
assert("||\t", false, Right, cx);
|
||||||
|
assert("|\t|", true, Right, cx);
|
||||||
|
assert(" ||\t", false, Left, cx);
|
||||||
|
assert(" ||\t", true, Left, cx);
|
||||||
|
assert(" ||\t", false, Right, cx);
|
||||||
|
assert(" |\t|", true, Right, cx);
|
||||||
|
assert(" ||\t", false, Left, cx);
|
||||||
|
assert(" ||\t", false, Right, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
|
||||||
|
fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
|
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
|
||||||
|
unmarked_snapshot.clip_at_line_ends = true;
|
||||||
|
assert_eq!(
|
||||||
|
unmarked_snapshot.clip_point(markers[1], Bias::Left),
|
||||||
|
markers[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert("||", cx);
|
||||||
|
assert("|a|", cx);
|
||||||
|
assert("a|b|", cx);
|
||||||
|
assert("a|α|", cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -456,6 +456,8 @@ pub struct Editor {
|
||||||
pending_rename: Option<RenameState>,
|
pending_rename: Option<RenameState>,
|
||||||
searchable: bool,
|
searchable: bool,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
|
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
|
||||||
|
input_enabled: bool,
|
||||||
leader_replica_id: Option<u16>,
|
leader_replica_id: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -932,6 +934,8 @@ impl Editor {
|
||||||
searchable: true,
|
searchable: true,
|
||||||
override_text_style: None,
|
override_text_style: None,
|
||||||
cursor_shape: Default::default(),
|
cursor_shape: Default::default(),
|
||||||
|
keymap_context_layers: Default::default(),
|
||||||
|
input_enabled: true,
|
||||||
leader_replica_id: None,
|
leader_replica_id: None,
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
@ -1000,6 +1004,10 @@ impl Editor {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mode(&self) -> EditorMode {
|
||||||
|
self.mode
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_placeholder_text(
|
pub fn set_placeholder_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
placeholder_text: impl Into<Arc<str>>,
|
placeholder_text: impl Into<Arc<str>>,
|
||||||
|
@ -1063,6 +1071,24 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.display_map
|
||||||
|
.update(cx, |map, _| map.clip_at_line_ends = clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
|
||||||
|
self.keymap_context_layers
|
||||||
|
.insert(TypeId::of::<Tag>(), context);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self) {
|
||||||
|
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_input_enabled(&mut self, input_enabled: bool) {
|
||||||
|
self.input_enabled = input_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
|
compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
|
||||||
|
@ -1742,6 +1768,11 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
|
pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
|
||||||
|
if !self.input_enabled {
|
||||||
|
cx.propagate_action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let text = action.0.as_ref();
|
let text = action.0.as_ref();
|
||||||
if !self.skip_autoclose_end(text, cx) {
|
if !self.skip_autoclose_end(text, cx) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
|
@ -5741,26 +5772,31 @@ impl View for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut context = Self::default_keymap_context();
|
||||||
let mode = match self.mode {
|
let mode = match self.mode {
|
||||||
EditorMode::SingleLine => "single_line",
|
EditorMode::SingleLine => "single_line",
|
||||||
EditorMode::AutoHeight { .. } => "auto_height",
|
EditorMode::AutoHeight { .. } => "auto_height",
|
||||||
EditorMode::Full => "full",
|
EditorMode::Full => "full",
|
||||||
};
|
};
|
||||||
cx.map.insert("mode".into(), mode.into());
|
context.map.insert("mode".into(), mode.into());
|
||||||
if self.pending_rename.is_some() {
|
if self.pending_rename.is_some() {
|
||||||
cx.set.insert("renaming".into());
|
context.set.insert("renaming".into());
|
||||||
}
|
}
|
||||||
match self.context_menu.as_ref() {
|
match self.context_menu.as_ref() {
|
||||||
Some(ContextMenu::Completions(_)) => {
|
Some(ContextMenu::Completions(_)) => {
|
||||||
cx.set.insert("showing_completions".into());
|
context.set.insert("showing_completions".into());
|
||||||
}
|
}
|
||||||
Some(ContextMenu::CodeActions(_)) => {
|
Some(ContextMenu::CodeActions(_)) => {
|
||||||
cx.set.insert("showing_code_actions".into());
|
context.set.insert("showing_code_actions".into());
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
cx
|
|
||||||
|
for layer in self.keymap_context_layers.values() {
|
||||||
|
context.extend(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6139,7 +6175,6 @@ pub fn styled_runs_for_code_label<'a>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::test::marked_text_by;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -6153,7 +6188,7 @@ mod tests {
|
||||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||||
use text::Point;
|
use text::Point;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::test::sample_text;
|
use util::test::{marked_text_by, sample_text};
|
||||||
use workspace::FollowableItem;
|
use workspace::FollowableItem;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -1292,7 +1292,7 @@ impl PaintState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum CursorShape {
|
pub enum CursorShape {
|
||||||
Bar,
|
Bar,
|
||||||
Block,
|
Block,
|
||||||
|
|
|
@ -266,13 +266,13 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{test::marked_text, Buffer, DisplayMap, MultiBuffer};
|
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
|
||||||
use language::Point;
|
use language::Point;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
|
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
|
||||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
previous_word_start(&snapshot, display_points[1]),
|
previous_word_start(&snapshot, display_points[1]),
|
||||||
display_points[0]
|
display_points[0]
|
||||||
|
@ -298,7 +298,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
|
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
|
||||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
previous_subword_start(&snapshot, display_points[1]),
|
previous_subword_start(&snapshot, display_points[1]),
|
||||||
display_points[0]
|
display_points[0]
|
||||||
|
@ -335,7 +335,7 @@ mod tests {
|
||||||
cx: &mut gpui::MutableAppContext,
|
cx: &mut gpui::MutableAppContext,
|
||||||
is_boundary: impl FnMut(char, char) -> bool,
|
is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) {
|
) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
find_preceding_boundary(&snapshot, display_points[1], is_boundary),
|
find_preceding_boundary(&snapshot, display_points[1], is_boundary),
|
||||||
display_points[0]
|
display_points[0]
|
||||||
|
@ -362,7 +362,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
|
fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
|
||||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_word_end(&snapshot, display_points[0]),
|
next_word_end(&snapshot, display_points[0]),
|
||||||
display_points[1]
|
display_points[1]
|
||||||
|
@ -385,7 +385,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
|
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
|
||||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_subword_end(&snapshot, display_points[0]),
|
next_subword_end(&snapshot, display_points[0]),
|
||||||
display_points[1]
|
display_points[1]
|
||||||
|
@ -421,7 +421,7 @@ mod tests {
|
||||||
cx: &mut gpui::MutableAppContext,
|
cx: &mut gpui::MutableAppContext,
|
||||||
is_boundary: impl FnMut(char, char) -> bool,
|
is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) {
|
) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
find_boundary(&snapshot, display_points[0], is_boundary),
|
find_boundary(&snapshot, display_points[0], is_boundary),
|
||||||
display_points[1]
|
display_points[1]
|
||||||
|
@ -448,7 +448,7 @@ mod tests {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
|
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
|
||||||
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
|
||||||
let (snapshot, display_points) = marked_snapshot(marked_text, cx);
|
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
surrounding_word(&snapshot, display_points[1]),
|
surrounding_word(&snapshot, display_points[1]),
|
||||||
display_points[0]..display_points[2]
|
display_points[0]..display_points[2]
|
||||||
|
@ -532,31 +532,4 @@ mod tests {
|
||||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
|
|
||||||
fn marked_snapshot(
|
|
||||||
text: &str,
|
|
||||||
cx: &mut gpui::MutableAppContext,
|
|
||||||
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
|
||||||
let (unmarked_text, markers) = marked_text(text);
|
|
||||||
|
|
||||||
let tab_size = 4;
|
|
||||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
|
||||||
let font_id = cx
|
|
||||||
.font_cache()
|
|
||||||
.select_font(family_id, &Default::default())
|
|
||||||
.unwrap();
|
|
||||||
let font_size = 14.0;
|
|
||||||
|
|
||||||
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
|
||||||
let display_map = cx
|
|
||||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
|
||||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
|
||||||
let markers = markers
|
|
||||||
.into_iter()
|
|
||||||
.map(|offset| offset.to_display_point(&snapshot))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(snapshot, markers)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use std::ops::Range;
|
use util::test::marked_text;
|
||||||
|
|
||||||
use collections::HashMap;
|
use crate::{
|
||||||
|
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||||
|
DisplayPoint, MultiBuffer,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -10,47 +13,29 @@ fn init_logger() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn marked_text_by(
|
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
|
||||||
marked_text: &str,
|
pub fn marked_display_snapshot(
|
||||||
markers: Vec<char>,
|
text: &str,
|
||||||
) -> (String, HashMap<char, Vec<usize>>) {
|
cx: &mut gpui::MutableAppContext,
|
||||||
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
|
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
||||||
let mut unmarked_text = String::new();
|
let (unmarked_text, markers) = marked_text(text);
|
||||||
|
|
||||||
for char in marked_text.chars() {
|
let tab_size = 4;
|
||||||
if markers.contains(&char) {
|
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||||
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
|
let font_id = cx
|
||||||
char_offsets.push(unmarked_text.len());
|
.font_cache()
|
||||||
} else {
|
.select_font(family_id, &Default::default())
|
||||||
unmarked_text.push(char);
|
.unwrap();
|
||||||
}
|
let font_size = 14.0;
|
||||||
}
|
|
||||||
|
|
||||||
(unmarked_text, extracted_markers)
|
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||||
}
|
let display_map =
|
||||||
|
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
|
||||||
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
|
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
|
let markers = markers
|
||||||
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
|
.into_iter()
|
||||||
}
|
.map(|offset| offset.to_display_point(&snapshot))
|
||||||
|
|
||||||
pub fn marked_text_ranges(
|
|
||||||
marked_text: &str,
|
|
||||||
range_markers: Vec<(char, char)>,
|
|
||||||
) -> (String, Vec<Range<usize>>) {
|
|
||||||
let mut marker_chars = Vec::new();
|
|
||||||
for (start, end) in range_markers.iter() {
|
|
||||||
marker_chars.push(*start);
|
|
||||||
marker_chars.push(*end);
|
|
||||||
}
|
|
||||||
let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars);
|
|
||||||
let ranges = range_markers
|
|
||||||
.iter()
|
|
||||||
.map(|(start_marker, end_marker)| {
|
|
||||||
let start = markers.get(start_marker).unwrap()[0];
|
|
||||||
let end = markers.get(end_marker).unwrap()[0];
|
|
||||||
start..end
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
(unmarked_text, ranges)
|
|
||||||
|
(snapshot, markers)
|
||||||
}
|
}
|
||||||
|
|
|
@ -442,13 +442,32 @@ impl TestAppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dispatch_keystroke(
|
pub fn dispatch_keystroke(
|
||||||
&self,
|
&mut self,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
responder_chain: Vec<usize>,
|
keystroke: Keystroke,
|
||||||
keystroke: &Keystroke,
|
input: Option<String>,
|
||||||
) -> Result<bool> {
|
is_held: bool,
|
||||||
let mut state = self.cx.borrow_mut();
|
) {
|
||||||
state.dispatch_keystroke(window_id, responder_chain, keystroke)
|
self.cx.borrow_mut().update(|cx| {
|
||||||
|
let presenter = cx
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get(&window_id)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.clone();
|
||||||
|
let responder_chain = presenter.borrow().dispatch_path(cx.as_ref());
|
||||||
|
|
||||||
|
if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) {
|
||||||
|
presenter.borrow_mut().dispatch_event(
|
||||||
|
Event::KeyDown {
|
||||||
|
keystroke,
|
||||||
|
input,
|
||||||
|
is_held,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||||
|
@ -503,7 +522,7 @@ impl TestAppContext {
|
||||||
|
|
||||||
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
||||||
let mut state = self.cx.borrow_mut();
|
let mut state = self.cx.borrow_mut();
|
||||||
// Don't increment pending flushes in order to effects to be flushed before the callback
|
// Don't increment pending flushes in order for effects to be flushed before the callback
|
||||||
// completes, which is helpful in tests.
|
// completes, which is helpful in tests.
|
||||||
let result = callback(&mut *state);
|
let result = callback(&mut *state);
|
||||||
// Flush effects after the callback just in case there are any. This can happen in edge
|
// Flush effects after the callback just in case there are any. This can happen in edge
|
||||||
|
@ -1250,9 +1269,9 @@ impl MutableAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn defer(&mut self, callback: Box<dyn FnOnce(&mut MutableAppContext)>) {
|
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
||||||
self.pending_effects.push_back(Effect::Deferred {
|
self.pending_effects.push_back(Effect::Deferred {
|
||||||
callback,
|
callback: Box::new(callback),
|
||||||
after_window_update: false,
|
after_window_update: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1379,17 +1398,15 @@ impl MutableAppContext {
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
responder_chain: Vec<usize>,
|
responder_chain: Vec<usize>,
|
||||||
keystroke: &Keystroke,
|
keystroke: &Keystroke,
|
||||||
) -> Result<bool> {
|
) -> bool {
|
||||||
let mut context_chain = Vec::new();
|
let mut context_chain = Vec::new();
|
||||||
for view_id in &responder_chain {
|
for view_id in &responder_chain {
|
||||||
if let Some(view) = self.cx.views.get(&(window_id, *view_id)) {
|
let view = self
|
||||||
|
.cx
|
||||||
|
.views
|
||||||
|
.get(&(window_id, *view_id))
|
||||||
|
.expect("view in responder chain does not exist");
|
||||||
context_chain.push(view.keymap_context(self.as_ref()));
|
context_chain.push(view.keymap_context(self.as_ref()));
|
||||||
} else {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"View {} in responder chain does not exist",
|
|
||||||
view_id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pending = false;
|
let mut pending = false;
|
||||||
|
@ -1404,13 +1421,13 @@ impl MutableAppContext {
|
||||||
if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
|
if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref())
|
||||||
{
|
{
|
||||||
self.keystroke_matcher.clear_pending();
|
self.keystroke_matcher.clear_pending();
|
||||||
return Ok(true);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(pending)
|
pending
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
|
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
|
||||||
|
@ -1540,14 +1557,11 @@ impl MutableAppContext {
|
||||||
window.on_event(Box::new(move |event| {
|
window.on_event(Box::new(move |event| {
|
||||||
app.update(|cx| {
|
app.update(|cx| {
|
||||||
if let Event::KeyDown { keystroke, .. } = &event {
|
if let Event::KeyDown { keystroke, .. } = &event {
|
||||||
if cx
|
if cx.dispatch_keystroke(
|
||||||
.dispatch_keystroke(
|
|
||||||
window_id,
|
window_id,
|
||||||
presenter.borrow().dispatch_path(cx.as_ref()),
|
presenter.borrow().dispatch_path(cx.as_ref()),
|
||||||
keystroke,
|
keystroke,
|
||||||
)
|
) {
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2711,11 +2725,11 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||||
|
|
||||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext<T>)) {
|
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext<T>)) {
|
||||||
let handle = self.handle();
|
let handle = self.handle();
|
||||||
self.app.defer(Box::new(move |cx| {
|
self.app.defer(move |cx| {
|
||||||
handle.update(cx, |model, cx| {
|
handle.update(cx, |model, cx| {
|
||||||
callback(model, cx);
|
callback(model, cx);
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(&mut self, payload: T::Event) {
|
pub fn emit(&mut self, payload: T::Event) {
|
||||||
|
@ -3064,11 +3078,11 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||||
|
|
||||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>)) {
|
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>)) {
|
||||||
let handle = self.handle();
|
let handle = self.handle();
|
||||||
self.app.defer(Box::new(move |cx| {
|
self.app.defer(move |cx| {
|
||||||
handle.update(cx, |view, cx| {
|
handle.update(cx, |view, cx| {
|
||||||
callback(view, cx);
|
callback(view, cx);
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn after_window_update(
|
pub fn after_window_update(
|
||||||
|
@ -3678,9 +3692,9 @@ impl<T: View> ViewHandle<T> {
|
||||||
F: 'static + FnOnce(&mut T, &mut ViewContext<T>),
|
F: 'static + FnOnce(&mut T, &mut ViewContext<T>),
|
||||||
{
|
{
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
cx.as_mut().defer(Box::new(move |cx| {
|
cx.as_mut().defer(move |cx| {
|
||||||
this.update(cx, |view, cx| update(view, cx));
|
this.update(cx, |view, cx| update(view, cx));
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_focused(&self, cx: &AppContext) -> bool {
|
pub fn is_focused(&self, cx: &AppContext) -> bool {
|
||||||
|
@ -5921,8 +5935,7 @@ mod tests {
|
||||||
window_id,
|
window_id,
|
||||||
vec![view_1.id(), view_2.id(), view_3.id()],
|
vec![view_1.id(), view_2.id(), view_3.id()],
|
||||||
&Keystroke::parse("a").unwrap(),
|
&Keystroke::parse("a").unwrap(),
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(&*actions.borrow(), &["2 a"]);
|
assert_eq!(&*actions.borrow(), &["2 a"]);
|
||||||
|
|
||||||
|
@ -5931,8 +5944,7 @@ mod tests {
|
||||||
window_id,
|
window_id,
|
||||||
vec![view_1.id(), view_2.id(), view_3.id()],
|
vec![view_1.id(), view_2.id(), view_3.id()],
|
||||||
&Keystroke::parse("b").unwrap(),
|
&Keystroke::parse("b").unwrap(),
|
||||||
)
|
);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]);
|
assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,15 +224,19 @@ impl Keystroke {
|
||||||
key: key.unwrap(),
|
key: key.unwrap(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn modified(&self) -> bool {
|
||||||
|
self.ctrl || self.alt || self.shift || self.cmd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub fn extend(&mut self, other: Context) {
|
pub fn extend(&mut self, other: &Context) {
|
||||||
for v in other.set {
|
for v in &other.set {
|
||||||
self.set.insert(v);
|
self.set.insert(v.clone());
|
||||||
}
|
}
|
||||||
for (k, v) in other.map {
|
for (k, v) in &other.map {
|
||||||
self.map.insert(k, v);
|
self.map.insert(k.clone(), v.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,19 @@ impl<T: Clone> Selection<T> {
|
||||||
self.start.clone()
|
self.start.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map<F, S>(&self, f: F) -> Selection<S>
|
||||||
|
where
|
||||||
|
F: Fn(T) -> S,
|
||||||
|
{
|
||||||
|
Selection::<S> {
|
||||||
|
id: self.id,
|
||||||
|
start: f(self.start.clone()),
|
||||||
|
end: f(self.end.clone()),
|
||||||
|
reversed: self.reversed,
|
||||||
|
goal: self.goal,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Copy + Ord> Selection<T> {
|
impl<T: Copy + Ord> Selection<T> {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
ops::Range,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
|
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
|
||||||
|
@ -48,3 +52,48 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
|
||||||
}
|
}
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn marked_text_by(
|
||||||
|
marked_text: &str,
|
||||||
|
markers: Vec<char>,
|
||||||
|
) -> (String, HashMap<char, Vec<usize>>) {
|
||||||
|
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
|
||||||
|
let mut unmarked_text = String::new();
|
||||||
|
|
||||||
|
for char in marked_text.chars() {
|
||||||
|
if markers.contains(&char) {
|
||||||
|
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
|
||||||
|
char_offsets.push(unmarked_text.len());
|
||||||
|
} else {
|
||||||
|
unmarked_text.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(unmarked_text, extracted_markers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
|
||||||
|
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
|
||||||
|
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marked_text_ranges(
|
||||||
|
marked_text: &str,
|
||||||
|
range_markers: Vec<(char, char)>,
|
||||||
|
) -> (String, Vec<Range<usize>>) {
|
||||||
|
let mut marker_chars = Vec::new();
|
||||||
|
for (start, end) in range_markers.iter() {
|
||||||
|
marker_chars.push(*start);
|
||||||
|
marker_chars.push(*end);
|
||||||
|
}
|
||||||
|
let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars);
|
||||||
|
let ranges = range_markers
|
||||||
|
.iter()
|
||||||
|
.map(|(start_marker, end_marker)| {
|
||||||
|
let start = markers.get(start_marker).unwrap()[0];
|
||||||
|
let end = markers.get(end_marker).unwrap()[0];
|
||||||
|
start..end
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(unmarked_text, ranges)
|
||||||
|
}
|
||||||
|
|
25
crates/vim/Cargo.toml
Normal file
25
crates/vim/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "vim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/vim.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
indoc = "1.0.4"
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
project = { path = "../project", features = ["test-support"] }
|
||||||
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
util = { path = "../util", features = ["test-support"] }
|
||||||
|
workspace = { path = "../workspace", features = ["test-support"] }
|
53
crates/vim/src/editor_events.rs
Normal file
53
crates/vim/src/editor_events.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased};
|
||||||
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
|
use crate::{mode::Mode, SwitchMode, VimState};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.subscribe_global(editor_created).detach();
|
||||||
|
cx.subscribe_global(editor_focused).detach();
|
||||||
|
cx.subscribe_global(editor_blurred).detach();
|
||||||
|
cx.subscribe_global(editor_released).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) {
|
||||||
|
cx.update_default_global(|vim_state: &mut VimState, cx| {
|
||||||
|
vim_state.editors.insert(editor.id(), editor.downgrade());
|
||||||
|
VimState::sync_editor_options(cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) {
|
||||||
|
let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) {
|
||||||
|
Mode::Insert
|
||||||
|
} else {
|
||||||
|
Mode::Normal
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.update_default_global(|vim_state: &mut VimState, _| {
|
||||||
|
vim_state.active_editor = Some(editor.downgrade());
|
||||||
|
});
|
||||||
|
VimState::switch_mode(&SwitchMode(mode), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) {
|
||||||
|
cx.update_default_global(|vim_state: &mut VimState, _| {
|
||||||
|
if let Some(previous_editor) = vim_state.active_editor.clone() {
|
||||||
|
if previous_editor == editor.clone() {
|
||||||
|
vim_state.active_editor = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
VimState::sync_editor_options(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) {
|
||||||
|
cx.update_default_global(|vim_state: &mut VimState, _| {
|
||||||
|
vim_state.editors.remove(&editor.id());
|
||||||
|
if let Some(previous_editor) = vim_state.active_editor.clone() {
|
||||||
|
if previous_editor == editor.clone() {
|
||||||
|
vim_state.active_editor = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
28
crates/vim/src/insert.rs
Normal file
28
crates/vim/src/insert.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use editor::Bias;
|
||||||
|
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
|
||||||
|
use language::SelectionGoal;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{mode::Mode, SwitchMode, VimState};
|
||||||
|
|
||||||
|
action!(NormalBefore);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
let context = Some("Editor && vim_mode == insert");
|
||||||
|
cx.add_bindings(vec![
|
||||||
|
Binding::new("escape", NormalBefore, context),
|
||||||
|
Binding::new("ctrl-c", NormalBefore, context),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cx.add_action(normal_before);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||||
|
VimState::update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||||
|
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||||
|
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
VimState::switch_mode(&SwitchMode(Mode::Normal), cx);
|
||||||
|
}
|
36
crates/vim/src/mode.rs
Normal file
36
crates/vim/src/mode.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use editor::CursorShape;
|
||||||
|
use gpui::keymap::Context;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Mode {
|
||||||
|
Normal,
|
||||||
|
Insert,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
pub fn cursor_shape(&self) -> CursorShape {
|
||||||
|
match self {
|
||||||
|
Mode::Normal => CursorShape::Block,
|
||||||
|
Mode::Insert => CursorShape::Bar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keymap_context_layer(&self) -> Context {
|
||||||
|
let mut context = Context::default();
|
||||||
|
context.map.insert(
|
||||||
|
"vim_mode".to_string(),
|
||||||
|
match self {
|
||||||
|
Self::Normal => "normal",
|
||||||
|
Self::Insert => "insert",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Normal
|
||||||
|
}
|
||||||
|
}
|
58
crates/vim/src/normal.rs
Normal file
58
crates/vim/src/normal.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use editor::{movement, Bias};
|
||||||
|
use gpui::{action, keymap::Binding, MutableAppContext, ViewContext};
|
||||||
|
use language::SelectionGoal;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{Mode, SwitchMode, VimState};
|
||||||
|
|
||||||
|
action!(InsertBefore);
|
||||||
|
action!(MoveLeft);
|
||||||
|
action!(MoveDown);
|
||||||
|
action!(MoveUp);
|
||||||
|
action!(MoveRight);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
let context = Some("Editor && vim_mode == normal");
|
||||||
|
cx.add_bindings(vec![
|
||||||
|
Binding::new("i", SwitchMode(Mode::Insert), context),
|
||||||
|
Binding::new("h", MoveLeft, context),
|
||||||
|
Binding::new("j", MoveDown, context),
|
||||||
|
Binding::new("k", MoveUp, context),
|
||||||
|
Binding::new("l", MoveRight, context),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cx.add_action(move_left);
|
||||||
|
cx.add_action(move_down);
|
||||||
|
cx.add_action(move_up);
|
||||||
|
cx.add_action(move_right);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
|
||||||
|
VimState::update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||||
|
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||||
|
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
|
||||||
|
VimState::update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.move_cursors(cx, movement::down);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
|
||||||
|
VimState::update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.move_cursors(cx, movement::up);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
|
||||||
|
VimState::update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.move_cursors(cx, |map, mut cursor, _| {
|
||||||
|
*cursor.column_mut() += 1;
|
||||||
|
(map.clip_point(cursor, Bias::Right), SelectionGoal::None)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
96
crates/vim/src/vim.rs
Normal file
96
crates/vim/src/vim.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
mod editor_events;
|
||||||
|
mod insert;
|
||||||
|
mod mode;
|
||||||
|
mod normal;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vim_tests;
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::{CursorShape, Editor};
|
||||||
|
use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
|
||||||
|
|
||||||
|
use mode::Mode;
|
||||||
|
use workspace::{self, Settings, Workspace};
|
||||||
|
|
||||||
|
action!(SwitchMode, Mode);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
editor_events::init(cx);
|
||||||
|
insert::init(cx);
|
||||||
|
normal::init(cx);
|
||||||
|
|
||||||
|
cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| VimState::switch_mode(action, cx));
|
||||||
|
|
||||||
|
cx.observe_global::<Settings, _>(VimState::settings_changed)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct VimState {
|
||||||
|
editors: HashMap<usize, WeakViewHandle<Editor>>,
|
||||||
|
active_editor: Option<WeakViewHandle<Editor>>,
|
||||||
|
|
||||||
|
enabled: bool,
|
||||||
|
mode: Mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VimState {
|
||||||
|
fn update_active_editor<S>(
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
update: impl FnOnce(&mut Editor, &mut ViewContext<Editor>) -> S,
|
||||||
|
) -> Option<S> {
|
||||||
|
cx.global::<Self>()
|
||||||
|
.active_editor
|
||||||
|
.clone()
|
||||||
|
.and_then(|ae| ae.upgrade(cx))
|
||||||
|
.map(|ae| ae.update(cx, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_mode(SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) {
|
||||||
|
cx.update_default_global(|this: &mut Self, _| {
|
||||||
|
this.mode = *mode;
|
||||||
|
});
|
||||||
|
|
||||||
|
VimState::sync_editor_options(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings_changed(cx: &mut MutableAppContext) {
|
||||||
|
cx.update_default_global(|this: &mut Self, cx| {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
if this.enabled != settings.vim_mode {
|
||||||
|
this.enabled = settings.vim_mode;
|
||||||
|
this.mode = if settings.vim_mode {
|
||||||
|
Mode::Normal
|
||||||
|
} else {
|
||||||
|
Mode::Insert
|
||||||
|
};
|
||||||
|
Self::sync_editor_options(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_editor_options(cx: &mut MutableAppContext) {
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
cx.update_default_global(|this: &mut VimState, cx| {
|
||||||
|
let mode = this.mode;
|
||||||
|
let cursor_shape = mode.cursor_shape();
|
||||||
|
let keymap_layer_active = this.enabled;
|
||||||
|
for editor in this.editors.values() {
|
||||||
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_cursor_shape(cursor_shape, cx);
|
||||||
|
editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx);
|
||||||
|
editor.set_input_enabled(mode == Mode::Insert);
|
||||||
|
if keymap_layer_active {
|
||||||
|
let context_layer = mode.keymap_context_layer();
|
||||||
|
editor.set_keymap_context_layer::<Self>(context_layer);
|
||||||
|
} else {
|
||||||
|
editor.remove_keymap_context_layer::<Self>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
243
crates/vim/src/vim_tests.rs
Normal file
243
crates/vim/src/vim_tests.rs
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
use indoc::indoc;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use editor::{display_map::ToDisplayPoint, DisplayPoint};
|
||||||
|
use gpui::{json::json, keymap::Keystroke, ViewHandle};
|
||||||
|
use language::{Point, Selection};
|
||||||
|
use util::test::marked_text;
|
||||||
|
use workspace::{WorkspaceHandle, WorkspaceParams};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestAppContext::new(cx, "").await;
|
||||||
|
cx.simulate_keystroke("i");
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.simulate_keystrokes(&["T", "e", "s", "t"]);
|
||||||
|
cx.assert_newest_selection_head("Test|");
|
||||||
|
cx.simulate_keystroke("escape");
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
cx.assert_newest_selection_head("Tes|t");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestAppContext::new(cx, "Test\nTestTest\nTest").await;
|
||||||
|
cx.simulate_keystroke("l");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
T|est
|
||||||
|
TestTest
|
||||||
|
Test"});
|
||||||
|
cx.simulate_keystroke("h");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
|Test
|
||||||
|
TestTest
|
||||||
|
Test"});
|
||||||
|
cx.simulate_keystroke("j");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Test
|
||||||
|
|TestTest
|
||||||
|
Test"});
|
||||||
|
cx.simulate_keystroke("k");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
|Test
|
||||||
|
TestTest
|
||||||
|
Test"});
|
||||||
|
cx.simulate_keystroke("j");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Test
|
||||||
|
|TestTest
|
||||||
|
Test"});
|
||||||
|
|
||||||
|
// When moving left, cursor does not wrap to the previous line
|
||||||
|
cx.simulate_keystroke("h");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Test
|
||||||
|
|TestTest
|
||||||
|
Test"});
|
||||||
|
|
||||||
|
// When moving right, cursor does not reach the line end or wrap to the next line
|
||||||
|
for _ in 0..9 {
|
||||||
|
cx.simulate_keystroke("l");
|
||||||
|
}
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Test
|
||||||
|
TestTes|t
|
||||||
|
Test"});
|
||||||
|
|
||||||
|
// Goal column respects the inability to reach the end of the line
|
||||||
|
cx.simulate_keystroke("k");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Tes|t
|
||||||
|
TestTest
|
||||||
|
Test"});
|
||||||
|
cx.simulate_keystroke("j");
|
||||||
|
cx.assert_newest_selection_head(indoc! {"
|
||||||
|
Test
|
||||||
|
TestTes|t
|
||||||
|
Test"});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestAppContext::new(cx, "").await;
|
||||||
|
|
||||||
|
// Editor acts as though vim is disabled
|
||||||
|
cx.disable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.simulate_keystrokes(&["h", "j", "k", "l"]);
|
||||||
|
cx.assert_newest_selection_head("hjkl|");
|
||||||
|
|
||||||
|
// Enabling dynamically sets vim mode again
|
||||||
|
cx.enable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
cx.simulate_keystrokes(&["h", "h", "h", "l"]);
|
||||||
|
assert_eq!(cx.editor_text(), "hjkl".to_owned());
|
||||||
|
cx.assert_newest_selection_head("hj|kl");
|
||||||
|
cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]);
|
||||||
|
cx.assert_newest_selection_head("hjTest|kl");
|
||||||
|
|
||||||
|
// Disabling and enabling resets to normal mode
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.disable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.enable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VimTestAppContext<'a> {
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
window_id: usize,
|
||||||
|
editor: ViewHandle<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> VimTestAppContext<'a> {
|
||||||
|
async fn new(
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
initial_editor_text: &str,
|
||||||
|
) -> VimTestAppContext<'a> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
editor::init(cx);
|
||||||
|
crate::init(cx);
|
||||||
|
});
|
||||||
|
let params = cx.update(WorkspaceParams::test);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.vim_mode = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
params
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({ "dir": { "test.txt": initial_editor_text } }),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||||
|
params
|
||||||
|
.project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||||
|
let item = workspace
|
||||||
|
.update(cx, |workspace, cx| workspace.open_path(file, cx))
|
||||||
|
.await
|
||||||
|
.expect("Could not open test file");
|
||||||
|
|
||||||
|
let editor = cx.update(|cx| {
|
||||||
|
item.act_as::<Editor>(cx)
|
||||||
|
.expect("Opened test file wasn't an editor")
|
||||||
|
});
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable_vim(&mut self) {
|
||||||
|
self.cx.update(|cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.vim_mode = true;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disable_vim(&mut self) {
|
||||||
|
self.cx.update(|cx| {
|
||||||
|
cx.update_global(|settings: &mut Settings, _| {
|
||||||
|
settings.vim_mode = false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newest_selection(&mut self) -> Selection<DisplayPoint> {
|
||||||
|
self.editor.update(self.cx, |editor, cx| {
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
editor
|
||||||
|
.newest_selection::<Point>(cx)
|
||||||
|
.map(|point| point.to_display_point(&snapshot.display_snapshot))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mode(&mut self) -> Mode {
|
||||||
|
self.cx.update(|cx| cx.global::<VimState>().mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editor_text(&mut self) -> String {
|
||||||
|
self.editor
|
||||||
|
.update(self.cx, |editor, cx| editor.snapshot(cx).text())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||||
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
|
let input = if keystroke.modified() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(keystroke.key.clone())
|
||||||
|
};
|
||||||
|
self.cx
|
||||||
|
.dispatch_keystroke(self.window_id, keystroke, input, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) {
|
||||||
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
|
self.simulate_keystroke(keystroke_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_newest_selection_head(&mut self, text: &str) {
|
||||||
|
let (unmarked_text, markers) = marked_text(&text);
|
||||||
|
assert_eq!(
|
||||||
|
self.editor_text(),
|
||||||
|
unmarked_text,
|
||||||
|
"Unmarked text doesn't match editor text"
|
||||||
|
);
|
||||||
|
let newest_selection = self.newest_selection();
|
||||||
|
let expected_head = self.editor.update(self.cx, |editor, cx| {
|
||||||
|
markers[0].to_display_point(&editor.snapshot(cx))
|
||||||
|
});
|
||||||
|
assert_eq!(newest_selection.head(), expected_head)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for VimTestAppContext<'a> {
|
||||||
|
type Target = gpui::TestAppContext;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.cx
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ use util::ResultExt;
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub buffer_font_family: FamilyId,
|
pub buffer_font_family: FamilyId,
|
||||||
pub buffer_font_size: f32,
|
pub buffer_font_size: f32,
|
||||||
|
pub vim_mode: bool,
|
||||||
pub tab_size: usize,
|
pub tab_size: usize,
|
||||||
pub soft_wrap: SoftWrap,
|
pub soft_wrap: SoftWrap,
|
||||||
pub preferred_line_length: u32,
|
pub preferred_line_length: u32,
|
||||||
|
@ -48,6 +49,8 @@ struct SettingsFileContent {
|
||||||
buffer_font_family: Option<String>,
|
buffer_font_family: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
buffer_font_size: Option<f32>,
|
buffer_font_size: Option<f32>,
|
||||||
|
#[serde(default)]
|
||||||
|
vim_mode: Option<bool>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
editor: LanguageOverride,
|
editor: LanguageOverride,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -130,6 +133,7 @@ impl Settings {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
|
||||||
buffer_font_size: 15.,
|
buffer_font_size: 15.,
|
||||||
|
vim_mode: false,
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
soft_wrap: SoftWrap::None,
|
soft_wrap: SoftWrap::None,
|
||||||
preferred_line_length: 80,
|
preferred_line_length: 80,
|
||||||
|
@ -174,6 +178,7 @@ impl Settings {
|
||||||
Settings {
|
Settings {
|
||||||
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
|
||||||
buffer_font_size: 14.,
|
buffer_font_size: 14.,
|
||||||
|
vim_mode: false,
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
soft_wrap: SoftWrap::None,
|
soft_wrap: SoftWrap::None,
|
||||||
preferred_line_length: 80,
|
preferred_line_length: 80,
|
||||||
|
@ -200,6 +205,7 @@ impl Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
merge(&mut self.buffer_font_size, data.buffer_font_size);
|
||||||
|
merge(&mut self.vim_mode, data.vim_mode);
|
||||||
merge(&mut self.soft_wrap, data.editor.soft_wrap);
|
merge(&mut self.soft_wrap, data.editor.soft_wrap);
|
||||||
merge(&mut self.tab_size, data.editor.tab_size);
|
merge(&mut self.tab_size, data.editor.tab_size);
|
||||||
merge(
|
merge(
|
||||||
|
|
|
@ -55,6 +55,7 @@ text = { path = "../text" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme_selector = { path = "../theme_selector" }
|
theme_selector = { path = "../theme_selector" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
vim = { path = "../vim" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
anyhow = "1.0.38"
|
anyhow = "1.0.38"
|
||||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||||
|
|
|
@ -78,6 +78,7 @@ fn main() {
|
||||||
project_panel::init(cx);
|
project_panel::init(cx);
|
||||||
diagnostics::init(cx);
|
diagnostics::init(cx);
|
||||||
search::init(cx);
|
search::init(cx);
|
||||||
|
vim::init(cx);
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|cx| async move {
|
|cx| async move {
|
||||||
|
|
Loading…
Reference in a new issue