Add textobjects queries (#20924)
Co-Authored-By: Max <max@zed.dev> Release Notes: - vim: Added motions `[[`, `[]`, `]]`, `][` for navigating by section, `[m`, `]m`, `[M`, `]M` for navigating by method, and `[*`, `]*`, `[/`, `]/` for comments. These currently only work for languages built in to Zed, as they are powered by new tree-sitter queries. - vim: Added new text objects: `ic`, `ac` for inside/around classes, `if`,`af` for functions/methods, and `g c` for comments. These currently only work for languages built in to Zed, as they are powered by new tree-sitter queries. --------- Co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
c443307c19
commit
75c9dc179b
28 changed files with 1205 additions and 26 deletions
|
@ -14,7 +14,8 @@ use crate::{
|
|||
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
|
||||
},
|
||||
task_context::RunnableRange,
|
||||
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
|
||||
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
|
||||
TreeSitterOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_watch as watch;
|
||||
|
@ -3412,6 +3413,72 @@ impl BufferSnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn text_object_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
options: TreeSitterOptions,
|
||||
) -> impl Iterator<Item = (Range<usize>, TextObject)> + '_ {
|
||||
let range = range.start.to_offset(self).saturating_sub(1)
|
||||
..self.len().min(range.end.to_offset(self) + 1);
|
||||
|
||||
let mut matches =
|
||||
self.syntax
|
||||
.matches_with_options(range.clone(), &self.text, options, |grammar| {
|
||||
grammar.text_object_config.as_ref().map(|c| &c.query)
|
||||
});
|
||||
|
||||
let configs = matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|grammar| grammar.text_object_config.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut captures = Vec::<(Range<usize>, TextObject)>::new();
|
||||
|
||||
iter::from_fn(move || loop {
|
||||
while let Some(capture) = captures.pop() {
|
||||
if capture.0.overlaps(&range) {
|
||||
return Some(capture);
|
||||
}
|
||||
}
|
||||
|
||||
let mat = matches.peek()?;
|
||||
|
||||
let Some(config) = configs[mat.grammar_index].as_ref() else {
|
||||
matches.advance();
|
||||
continue;
|
||||
};
|
||||
|
||||
for capture in mat.captures {
|
||||
let Some(ix) = config
|
||||
.text_objects_by_capture_ix
|
||||
.binary_search_by_key(&capture.index, |e| e.0)
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let text_object = config.text_objects_by_capture_ix[ix].1;
|
||||
let byte_range = capture.node.byte_range();
|
||||
|
||||
let mut found = false;
|
||||
for (range, existing) in captures.iter_mut() {
|
||||
if existing == &text_object {
|
||||
range.start = range.start.min(byte_range.start);
|
||||
range.end = range.end.max(byte_range.end);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
captures.push((byte_range, text_object));
|
||||
}
|
||||
}
|
||||
|
||||
matches.advance();
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns enclosing bracket ranges containing the given range
|
||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
|
|
|
@ -20,6 +20,7 @@ use std::{
|
|||
sync::LazyLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use syntax_map::TreeSitterOptions;
|
||||
use text::network::Network;
|
||||
use text::{BufferId, LineEnding, LineIndent};
|
||||
use text::{Point, ToPoint};
|
||||
|
@ -915,6 +916,39 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_text_objects(cx: &mut AppContext) {
|
||||
let (text, ranges) = marked_text_ranges(
|
||||
indoc! {r#"
|
||||
impl Hello {
|
||||
fn say() -> u8 { return /* ˇhi */ 1 }
|
||||
}"#
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let matches = snapshot
|
||||
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
|
||||
.map(|(range, text_object)| (&text[range], text_object))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
("/* hi */", TextObject::AroundComment),
|
||||
("return /* hi */ 1", TextObject::InsideFunction),
|
||||
(
|
||||
"fn say() -> u8 { return /* hi */ 1 }",
|
||||
TextObject::AroundFunction
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
|
||||
let mut assert = |selection_text, range_markers| {
|
||||
|
@ -3182,6 +3216,20 @@ fn rust_lang() -> Language {
|
|||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_text_object_query(
|
||||
r#"
|
||||
(function_item
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" )) @function.around
|
||||
|
||||
(line_comment)+ @comment.around
|
||||
|
||||
(block_comment) @comment.around
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(line_comment) @annotation
|
||||
|
|
|
@ -78,7 +78,7 @@ pub use language_registry::{
|
|||
};
|
||||
pub use lsp::LanguageServerId;
|
||||
pub use outline::*;
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
|
||||
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
|
||||
pub use text::{AnchorRangeExt, LineEnding};
|
||||
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
|
||||
|
||||
|
@ -848,6 +848,7 @@ pub struct Grammar {
|
|||
pub(crate) runnable_config: Option<RunnableConfig>,
|
||||
pub(crate) indents_config: Option<IndentConfig>,
|
||||
pub outline_config: Option<OutlineConfig>,
|
||||
pub text_object_config: Option<TextObjectConfig>,
|
||||
pub embedding_config: Option<EmbeddingConfig>,
|
||||
pub(crate) injection_config: Option<InjectionConfig>,
|
||||
pub(crate) override_config: Option<OverrideConfig>,
|
||||
|
@ -873,6 +874,44 @@ pub struct OutlineConfig {
|
|||
pub annotation_capture_ix: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum TextObject {
|
||||
InsideFunction,
|
||||
AroundFunction,
|
||||
InsideClass,
|
||||
AroundClass,
|
||||
InsideComment,
|
||||
AroundComment,
|
||||
}
|
||||
|
||||
impl TextObject {
|
||||
pub fn from_capture_name(name: &str) -> Option<TextObject> {
|
||||
match name {
|
||||
"function.inside" => Some(TextObject::InsideFunction),
|
||||
"function.around" => Some(TextObject::AroundFunction),
|
||||
"class.inside" => Some(TextObject::InsideClass),
|
||||
"class.around" => Some(TextObject::AroundClass),
|
||||
"comment.inside" => Some(TextObject::InsideComment),
|
||||
"comment.around" => Some(TextObject::AroundComment),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn around(&self) -> Option<Self> {
|
||||
match self {
|
||||
TextObject::InsideFunction => Some(TextObject::AroundFunction),
|
||||
TextObject::InsideClass => Some(TextObject::AroundClass),
|
||||
TextObject::InsideComment => Some(TextObject::AroundComment),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextObjectConfig {
|
||||
pub query: Query,
|
||||
pub text_objects_by_capture_ix: Vec<(u32, TextObject)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmbeddingConfig {
|
||||
pub query: Query,
|
||||
|
@ -950,6 +989,7 @@ impl Language {
|
|||
highlights_query: None,
|
||||
brackets_config: None,
|
||||
outline_config: None,
|
||||
text_object_config: None,
|
||||
embedding_config: None,
|
||||
indents_config: None,
|
||||
injection_config: None,
|
||||
|
@ -1020,7 +1060,12 @@ impl Language {
|
|||
if let Some(query) = queries.runnables {
|
||||
self = self
|
||||
.with_runnable_query(query.as_ref())
|
||||
.context("Error loading tests query")?;
|
||||
.context("Error loading runnables query")?;
|
||||
}
|
||||
if let Some(query) = queries.text_objects {
|
||||
self = self
|
||||
.with_text_object_query(query.as_ref())
|
||||
.context("Error loading textobject query")?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
@ -1097,6 +1142,26 @@ impl Language {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_text_object_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar_mut()
|
||||
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
|
||||
let query = Query::new(&grammar.ts_language, source)?;
|
||||
|
||||
let mut text_objects_by_capture_ix = Vec::new();
|
||||
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||
if let Some(text_object) = TextObject::from_capture_name(name) {
|
||||
text_objects_by_capture_ix.push((ix as u32, text_object));
|
||||
}
|
||||
}
|
||||
|
||||
grammar.text_object_config = Some(TextObjectConfig {
|
||||
query,
|
||||
text_objects_by_capture_ix,
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar_mut()
|
||||
|
|
|
@ -181,6 +181,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
|
|||
("overrides", |q| &mut q.overrides),
|
||||
("redactions", |q| &mut q.redactions),
|
||||
("runnables", |q| &mut q.runnables),
|
||||
("textobjects", |q| &mut q.text_objects),
|
||||
];
|
||||
|
||||
/// Tree-sitter language queries for a given language.
|
||||
|
@ -195,6 +196,7 @@ pub struct LanguageQueries {
|
|||
pub overrides: Option<Cow<'static, str>>,
|
||||
pub redactions: Option<Cow<'static, str>>,
|
||||
pub runnables: Option<Cow<'static, str>>,
|
||||
pub text_objects: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
|
|
@ -814,6 +814,23 @@ impl SyntaxSnapshot {
|
|||
buffer.as_rope(),
|
||||
self.layers_for_range(range, buffer, true),
|
||||
query,
|
||||
TreeSitterOptions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn matches_with_options<'a>(
|
||||
&'a self,
|
||||
range: Range<usize>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
options: TreeSitterOptions,
|
||||
query: fn(&Grammar) -> Option<&Query>,
|
||||
) -> SyntaxMapMatches<'a> {
|
||||
SyntaxMapMatches::new(
|
||||
range.clone(),
|
||||
buffer.as_rope(),
|
||||
self.layers_for_range(range, buffer, true),
|
||||
query,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1001,12 +1018,25 @@ impl<'a> SyntaxMapCaptures<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TreeSitterOptions {
|
||||
max_start_depth: Option<u32>,
|
||||
}
|
||||
impl TreeSitterOptions {
|
||||
pub fn max_start_depth(max_start_depth: u32) -> Self {
|
||||
Self {
|
||||
max_start_depth: Some(max_start_depth),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SyntaxMapMatches<'a> {
|
||||
fn new(
|
||||
range: Range<usize>,
|
||||
text: &'a Rope,
|
||||
layers: impl Iterator<Item = SyntaxLayer<'a>>,
|
||||
query: fn(&Grammar) -> Option<&Query>,
|
||||
options: TreeSitterOptions,
|
||||
) -> Self {
|
||||
let mut result = Self::default();
|
||||
for layer in layers {
|
||||
|
@ -1027,6 +1057,7 @@ impl<'a> SyntaxMapMatches<'a> {
|
|||
query_cursor.deref_mut(),
|
||||
)
|
||||
};
|
||||
cursor.set_max_start_depth(options.max_start_depth);
|
||||
|
||||
cursor.set_byte_range(range.clone());
|
||||
let matches = cursor.matches(query, layer.node(), TextProvider(text));
|
||||
|
|
7
crates/languages/src/bash/textobjects.scm
Normal file
7
crates/languages/src/bash/textobjects.scm
Normal file
|
@ -0,0 +1,7 @@
|
|||
(function_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" )) @function.around
|
||||
|
||||
(comment) @comment.around
|
25
crates/languages/src/c/textobjects.scm
Normal file
25
crates/languages/src/c/textobjects.scm
Normal file
|
@ -0,0 +1,25 @@
|
|||
(declaration
|
||||
declarator: (function_declarator)) @function.around
|
||||
|
||||
(function_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" )) @function.around
|
||||
|
||||
(preproc_function_def
|
||||
value: (_) @function.inside) @function.around
|
||||
|
||||
(comment) @comment.around
|
||||
|
||||
(struct_specifier
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(enum_specifier
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}")) @class.around
|
31
crates/languages/src/cpp/textobjects.scm
Normal file
31
crates/languages/src/cpp/textobjects.scm
Normal file
|
@ -0,0 +1,31 @@
|
|||
(declaration
|
||||
declarator: (function_declarator)) @function.around
|
||||
|
||||
(function_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" )) @function.around
|
||||
|
||||
(preproc_function_def
|
||||
value: (_) @function.inside) @function.around
|
||||
|
||||
(comment) @comment.around
|
||||
|
||||
(struct_specifier
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(enum_specifier
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}")) @class.around
|
||||
|
||||
(class_specifier
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ":"? ";"?]* @class.inside
|
||||
"}"?)) @class.around
|
30
crates/languages/src/css/textobjects.scm
Normal file
30
crates/languages/src/css/textobjects.scm
Normal file
|
@ -0,0 +1,30 @@
|
|||
(comment) @comment.around
|
||||
|
||||
(rule_set
|
||||
(block (
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" ))) @function.around
|
||||
(keyframe_block
|
||||
(block (
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" ))) @function.around
|
||||
|
||||
(media_statement
|
||||
(block (
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}" ))) @class.around
|
||||
|
||||
(supports_statement
|
||||
(block (
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}" ))) @class.around
|
||||
|
||||
(keyframes_statement
|
||||
(keyframe_block_list (
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}" ))) @class.around
|
25
crates/languages/src/go/textobjects.scm
Normal file
25
crates/languages/src/go/textobjects.scm
Normal file
|
@ -0,0 +1,25 @@
|
|||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(type_declaration
|
||||
(type_spec (struct_type (field_declaration_list (
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}")?)))) @class.around
|
||||
|
||||
(type_declaration
|
||||
(type_spec (interface_type
|
||||
(_)* @class.inside))) @class.around
|
||||
|
||||
(type_declaration) @class.around
|
||||
|
||||
(comment)+ @comment.around
|
51
crates/languages/src/javascript/textobjects.scm
Normal file
51
crates/languages/src/javascript/textobjects.scm
Normal file
|
@ -0,0 +1,51 @@
|
|||
(comment)+ @comment.around
|
||||
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(function_expression
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function) @function.around
|
||||
|
||||
(generator_function
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(generator_function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(class_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(class
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
1
crates/languages/src/json/textobjects.scm
Normal file
1
crates/languages/src/json/textobjects.scm
Normal file
|
@ -0,0 +1 @@
|
|||
(comment)+ @comment.around
|
1
crates/languages/src/jsonc/textobjects.scm
Normal file
1
crates/languages/src/jsonc/textobjects.scm
Normal file
|
@ -0,0 +1 @@
|
|||
(comment)+ @comment.around
|
3
crates/languages/src/markdown/textobjects.scm
Normal file
3
crates/languages/src/markdown/textobjects.scm
Normal file
|
@ -0,0 +1,3 @@
|
|||
(section
|
||||
(atx_heading)
|
||||
(_)* @class.inside) @class.around
|
7
crates/languages/src/python/textobjects.scm
Normal file
7
crates/languages/src/python/textobjects.scm
Normal file
|
@ -0,0 +1,7 @@
|
|||
(comment)+ @comment.around
|
||||
|
||||
(function_definition
|
||||
body: (_) @function.inside) @function.around
|
||||
|
||||
(class_definition
|
||||
body: (_) @class.inside) @class.around
|
|
@ -15,11 +15,7 @@
|
|||
(visibility_modifier)? @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_)? @name
|
||||
"for"? @context
|
||||
type: (_) @name
|
||||
(function_item
|
||||
body: (_ "{" @open (_)* "}" @close)) @item
|
||||
|
||||
(trait_item
|
||||
|
|
51
crates/languages/src/rust/textobjects.scm
Normal file
51
crates/languages/src/rust/textobjects.scm
Normal file
|
@ -0,0 +1,51 @@
|
|||
; functions
|
||||
(function_signature_item) @function.around
|
||||
|
||||
(function_item
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}" )) @function.around
|
||||
|
||||
; classes
|
||||
(struct_item
|
||||
body: (_
|
||||
["{" "("]?
|
||||
[(_) ","?]* @class.inside
|
||||
["}" ")"]? )) @class.around
|
||||
|
||||
(enum_item
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(union_item
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(trait_item
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(impl_item
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(mod_item
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
; comments
|
||||
|
||||
(line_comment)+ @comment.around
|
||||
|
||||
(block_comment) @comment.around
|
79
crates/languages/src/tsx/textobjects.scm
Normal file
79
crates/languages/src/tsx/textobjects.scm
Normal file
|
@ -0,0 +1,79 @@
|
|||
(comment)+ @comment.around
|
||||
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(function_expression
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function) @function.around
|
||||
(function_signature) @function.around
|
||||
|
||||
(generator_function
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(generator_function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(class_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(class
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(interface_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(enum_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(ambient_declaration
|
||||
(module
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" ))) @class.around
|
||||
|
||||
(internal_module
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(type_alias_declaration) @class.around
|
79
crates/languages/src/typescript/textobjects.scm
Normal file
79
crates/languages/src/typescript/textobjects.scm
Normal file
|
@ -0,0 +1,79 @@
|
|||
(comment)+ @comment.around
|
||||
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(function_expression
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(arrow_function) @function.around
|
||||
(function_signature) @function.around
|
||||
|
||||
(generator_function
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(generator_function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(class_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(class
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(interface_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(enum_declaration
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ","?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(ambient_declaration
|
||||
(module
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" ))) @class.around
|
||||
|
||||
(internal_module
|
||||
body: (_
|
||||
"{"
|
||||
[(_) ";"?]* @class.inside
|
||||
"}" )) @class.around
|
||||
|
||||
(type_alias_declaration) @class.around
|
1
crates/languages/src/yaml/textobjects.scm
Normal file
1
crates/languages/src/yaml/textobjects.scm
Normal file
|
@ -0,0 +1 @@
|
|||
(comment)+ @comment
|
|
@ -3441,6 +3441,30 @@ impl MultiBufferSnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn excerpt_before(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
|
||||
let start_locator = self.excerpt_locator_for_id(id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
cursor.seek(&Some(start_locator), Bias::Left, &());
|
||||
cursor.prev(&());
|
||||
let excerpt = cursor.item()?;
|
||||
Some(MultiBufferExcerpt {
|
||||
excerpt,
|
||||
excerpt_offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn excerpt_after(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
|
||||
let start_locator = self.excerpt_locator_for_id(id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
cursor.seek(&Some(start_locator), Bias::Left, &());
|
||||
cursor.next(&());
|
||||
let excerpt = cursor.item()?;
|
||||
Some(MultiBufferExcerpt {
|
||||
excerpt,
|
||||
excerpt_offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn excerpt_boundaries_in_range<R, T>(
|
||||
&self,
|
||||
range: R,
|
||||
|
@ -4689,6 +4713,26 @@ impl<'a> MultiBufferExcerpt<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ExcerptId {
|
||||
self.excerpt.id
|
||||
}
|
||||
|
||||
pub fn start_anchor(&self) -> Anchor {
|
||||
Anchor {
|
||||
buffer_id: Some(self.excerpt.buffer_id),
|
||||
excerpt_id: self.excerpt.id,
|
||||
text_anchor: self.excerpt.range.context.start,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_anchor(&self) -> Anchor {
|
||||
Anchor {
|
||||
buffer_id: Some(self.excerpt.buffer_id),
|
||||
excerpt_id: self.excerpt.id,
|
||||
text_anchor: self.excerpt.range.context.end,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &'a BufferSnapshot {
|
||||
&self.excerpt.buffer
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use language::{CharKind, Point, Selection, SelectionGoal};
|
|||
use multi_buffer::MultiBufferRow;
|
||||
use serde::Deserialize;
|
||||
use std::ops::Range;
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
use crate::{
|
||||
normal::mark,
|
||||
|
@ -104,6 +105,16 @@ pub enum Motion {
|
|||
WindowTop,
|
||||
WindowMiddle,
|
||||
WindowBottom,
|
||||
NextSectionStart,
|
||||
NextSectionEnd,
|
||||
PreviousSectionStart,
|
||||
PreviousSectionEnd,
|
||||
NextMethodStart,
|
||||
NextMethodEnd,
|
||||
PreviousMethodStart,
|
||||
PreviousMethodEnd,
|
||||
NextComment,
|
||||
PreviousComment,
|
||||
|
||||
// we don't have a good way to run a search synchronously, so
|
||||
// we handle search motions by running the search async and then
|
||||
|
@ -269,6 +280,16 @@ actions!(
|
|||
WindowTop,
|
||||
WindowMiddle,
|
||||
WindowBottom,
|
||||
NextSectionStart,
|
||||
NextSectionEnd,
|
||||
PreviousSectionStart,
|
||||
PreviousSectionEnd,
|
||||
NextMethodStart,
|
||||
NextMethodEnd,
|
||||
PreviousMethodStart,
|
||||
PreviousMethodEnd,
|
||||
NextComment,
|
||||
PreviousComment,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -454,6 +475,37 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|||
Vim::action(editor, cx, |vim, &WindowBottom, cx| {
|
||||
vim.motion(Motion::WindowBottom, cx)
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| {
|
||||
vim.motion(Motion::PreviousSectionStart, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &NextSectionStart, cx| {
|
||||
vim.motion(Motion::NextSectionStart, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| {
|
||||
vim.motion(Motion::PreviousSectionEnd, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &NextSectionEnd, cx| {
|
||||
vim.motion(Motion::NextSectionEnd, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| {
|
||||
vim.motion(Motion::PreviousMethodStart, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &NextMethodStart, cx| {
|
||||
vim.motion(Motion::NextMethodStart, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| {
|
||||
vim.motion(Motion::PreviousMethodEnd, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &NextMethodEnd, cx| {
|
||||
vim.motion(Motion::NextMethodEnd, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &NextComment, cx| {
|
||||
vim.motion(Motion::NextComment, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, &PreviousComment, cx| {
|
||||
vim.motion(Motion::PreviousComment, cx)
|
||||
});
|
||||
}
|
||||
|
||||
impl Vim {
|
||||
|
@ -536,6 +588,16 @@ impl Motion {
|
|||
| WindowTop
|
||||
| WindowMiddle
|
||||
| WindowBottom
|
||||
| NextSectionStart
|
||||
| NextSectionEnd
|
||||
| PreviousSectionStart
|
||||
| PreviousSectionEnd
|
||||
| NextMethodStart
|
||||
| NextMethodEnd
|
||||
| PreviousMethodStart
|
||||
| PreviousMethodEnd
|
||||
| NextComment
|
||||
| PreviousComment
|
||||
| Jump { line: true, .. } => true,
|
||||
EndOfLine { .. }
|
||||
| Matching
|
||||
|
@ -607,6 +669,16 @@ impl Motion {
|
|||
| NextLineStart
|
||||
| PreviousLineStart
|
||||
| ZedSearchResult { .. }
|
||||
| NextSectionStart
|
||||
| NextSectionEnd
|
||||
| PreviousSectionStart
|
||||
| PreviousSectionEnd
|
||||
| NextMethodStart
|
||||
| NextMethodEnd
|
||||
| PreviousMethodStart
|
||||
| PreviousMethodEnd
|
||||
| NextComment
|
||||
| PreviousComment
|
||||
| Jump { .. } => false,
|
||||
}
|
||||
}
|
||||
|
@ -652,6 +724,16 @@ impl Motion {
|
|||
| FirstNonWhitespace { .. }
|
||||
| FindBackward { .. }
|
||||
| Jump { .. }
|
||||
| NextSectionStart
|
||||
| NextSectionEnd
|
||||
| PreviousSectionStart
|
||||
| PreviousSectionEnd
|
||||
| NextMethodStart
|
||||
| NextMethodEnd
|
||||
| PreviousMethodStart
|
||||
| PreviousMethodEnd
|
||||
| NextComment
|
||||
| PreviousComment
|
||||
| ZedSearchResult { .. } => false,
|
||||
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
|
||||
motion.inclusive()
|
||||
|
@ -867,6 +949,47 @@ impl Motion {
|
|||
return None;
|
||||
}
|
||||
}
|
||||
NextSectionStart => (
|
||||
section_motion(map, point, times, Direction::Next, true),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextSectionEnd => (
|
||||
section_motion(map, point, times, Direction::Next, false),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousSectionStart => (
|
||||
section_motion(map, point, times, Direction::Prev, true),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousSectionEnd => (
|
||||
section_motion(map, point, times, Direction::Prev, false),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
|
||||
NextMethodStart => (
|
||||
method_motion(map, point, times, Direction::Next, true),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextMethodEnd => (
|
||||
method_motion(map, point, times, Direction::Next, false),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousMethodStart => (
|
||||
method_motion(map, point, times, Direction::Prev, true),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousMethodEnd => (
|
||||
method_motion(map, point, times, Direction::Prev, false),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextComment => (
|
||||
comment_motion(map, point, times, Direction::Next),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
PreviousComment => (
|
||||
comment_motion(map, point, times, Direction::Prev),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
};
|
||||
|
||||
(new_point != point || infallible).then_some((new_point, goal))
|
||||
|
@ -2129,6 +2252,231 @@ fn window_bottom(
|
|||
}
|
||||
}
|
||||
|
||||
fn method_motion(
|
||||
map: &DisplaySnapshot,
|
||||
mut display_point: DisplayPoint,
|
||||
times: usize,
|
||||
direction: Direction,
|
||||
is_start: bool,
|
||||
) -> DisplayPoint {
|
||||
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
|
||||
return display_point;
|
||||
};
|
||||
|
||||
for _ in 0..times {
|
||||
let point = map.display_point_to_point(display_point, Bias::Left);
|
||||
let offset = point.to_offset(&map.buffer_snapshot);
|
||||
let range = if direction == Direction::Prev {
|
||||
0..offset
|
||||
} else {
|
||||
offset..buffer.len()
|
||||
};
|
||||
|
||||
let possibilities = buffer
|
||||
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
|
||||
.filter_map(|(range, object)| {
|
||||
if !matches!(object, language::TextObject::AroundFunction) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let relevant = if is_start { range.start } else { range.end };
|
||||
if direction == Direction::Prev && relevant < offset {
|
||||
Some(relevant)
|
||||
} else if direction == Direction::Next && relevant > offset + 1 {
|
||||
Some(relevant)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let dest = if direction == Direction::Prev {
|
||||
possibilities.max().unwrap_or(offset)
|
||||
} else {
|
||||
possibilities.min().unwrap_or(offset)
|
||||
};
|
||||
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
|
||||
if new_point == display_point {
|
||||
break;
|
||||
}
|
||||
display_point = new_point;
|
||||
}
|
||||
display_point
|
||||
}
|
||||
|
||||
fn comment_motion(
|
||||
map: &DisplaySnapshot,
|
||||
mut display_point: DisplayPoint,
|
||||
times: usize,
|
||||
direction: Direction,
|
||||
) -> DisplayPoint {
|
||||
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
|
||||
return display_point;
|
||||
};
|
||||
|
||||
for _ in 0..times {
|
||||
let point = map.display_point_to_point(display_point, Bias::Left);
|
||||
let offset = point.to_offset(&map.buffer_snapshot);
|
||||
let range = if direction == Direction::Prev {
|
||||
0..offset
|
||||
} else {
|
||||
offset..buffer.len()
|
||||
};
|
||||
|
||||
let possibilities = buffer
|
||||
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
|
||||
.filter_map(|(range, object)| {
|
||||
if !matches!(object, language::TextObject::AroundComment) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let relevant = if direction == Direction::Prev {
|
||||
range.start
|
||||
} else {
|
||||
range.end
|
||||
};
|
||||
if direction == Direction::Prev && relevant < offset {
|
||||
Some(relevant)
|
||||
} else if direction == Direction::Next && relevant > offset + 1 {
|
||||
Some(relevant)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let dest = if direction == Direction::Prev {
|
||||
possibilities.max().unwrap_or(offset)
|
||||
} else {
|
||||
possibilities.min().unwrap_or(offset)
|
||||
};
|
||||
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
|
||||
if new_point == display_point {
|
||||
break;
|
||||
}
|
||||
display_point = new_point;
|
||||
}
|
||||
|
||||
display_point
|
||||
}
|
||||
|
||||
fn section_motion(
|
||||
map: &DisplaySnapshot,
|
||||
mut display_point: DisplayPoint,
|
||||
times: usize,
|
||||
direction: Direction,
|
||||
is_start: bool,
|
||||
) -> DisplayPoint {
|
||||
if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
|
||||
for _ in 0..times {
|
||||
let offset = map
|
||||
.display_point_to_point(display_point, Bias::Left)
|
||||
.to_offset(&map.buffer_snapshot);
|
||||
let range = if direction == Direction::Prev {
|
||||
0..offset
|
||||
} else {
|
||||
offset..buffer.len()
|
||||
};
|
||||
|
||||
// we set a max start depth here because we want a section to only be "top level"
|
||||
// similar to vim's default of '{' in the first column.
|
||||
// (and without it, ]] at the start of editor.rs is -very- slow)
|
||||
let mut possibilities = buffer
|
||||
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
|
||||
.filter(|(_, object)| {
|
||||
matches!(
|
||||
object,
|
||||
language::TextObject::AroundClass | language::TextObject::AroundFunction
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
possibilities.sort_by_key(|(range_a, _)| range_a.start);
|
||||
let mut prev_end = None;
|
||||
let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
|
||||
if t == language::TextObject::AroundFunction
|
||||
&& prev_end.is_some_and(|prev_end| prev_end > range.start)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
prev_end = Some(range.end);
|
||||
|
||||
let relevant = if is_start { range.start } else { range.end };
|
||||
if direction == Direction::Prev && relevant < offset {
|
||||
Some(relevant)
|
||||
} else if direction == Direction::Next && relevant > offset + 1 {
|
||||
Some(relevant)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let offset = if direction == Direction::Prev {
|
||||
possibilities.max().unwrap_or(0)
|
||||
} else {
|
||||
possibilities.min().unwrap_or(buffer.len())
|
||||
};
|
||||
|
||||
let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
|
||||
if new_point == display_point {
|
||||
break;
|
||||
}
|
||||
display_point = new_point;
|
||||
}
|
||||
return display_point;
|
||||
};
|
||||
|
||||
for _ in 0..times {
|
||||
let point = map.display_point_to_point(display_point, Bias::Left);
|
||||
let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
|
||||
return display_point;
|
||||
};
|
||||
let next_point = match (direction, is_start) {
|
||||
(Direction::Prev, true) => {
|
||||
let mut start = excerpt.start_anchor().to_display_point(&map);
|
||||
if start >= display_point && start.row() > DisplayRow(0) {
|
||||
let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
|
||||
return display_point;
|
||||
};
|
||||
start = excerpt.start_anchor().to_display_point(&map);
|
||||
}
|
||||
start
|
||||
}
|
||||
(Direction::Prev, false) => {
|
||||
let mut start = excerpt.start_anchor().to_display_point(&map);
|
||||
if start.row() > DisplayRow(0) {
|
||||
*start.row_mut() -= 1;
|
||||
}
|
||||
map.clip_point(start, Bias::Left)
|
||||
}
|
||||
(Direction::Next, true) => {
|
||||
let mut end = excerpt.end_anchor().to_display_point(&map);
|
||||
*end.row_mut() += 1;
|
||||
map.clip_point(end, Bias::Right)
|
||||
}
|
||||
(Direction::Next, false) => {
|
||||
let mut end = excerpt.end_anchor().to_display_point(&map);
|
||||
*end.column_mut() = 0;
|
||||
if end <= display_point {
|
||||
*end.row_mut() += 1;
|
||||
let point_end = map.display_point_to_point(end, Bias::Right);
|
||||
let Some(excerpt) =
|
||||
map.buffer_snapshot.excerpt_containing(point_end..point_end)
|
||||
else {
|
||||
return display_point;
|
||||
};
|
||||
end = excerpt.end_anchor().to_display_point(&map);
|
||||
*end.column_mut() = 0;
|
||||
}
|
||||
end
|
||||
}
|
||||
};
|
||||
if next_point == display_point {
|
||||
break;
|
||||
}
|
||||
display_point = next_point;
|
||||
}
|
||||
|
||||
display_point
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crate::{motion::right, state::Mode, Vim};
|
||||
use crate::{
|
||||
motion::right,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
};
|
||||
use editor::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement::{self, FindRange},
|
||||
|
@ -10,7 +14,7 @@ use editor::{
|
|||
use itertools::Itertools;
|
||||
|
||||
use gpui::{actions, impl_actions, ViewContext};
|
||||
use language::{BufferSnapshot, CharKind, Point, Selection};
|
||||
use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -30,6 +34,9 @@ pub enum Object {
|
|||
Argument,
|
||||
IndentObj { include_below: bool },
|
||||
Tag,
|
||||
Method,
|
||||
Class,
|
||||
Comment,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
|
@ -61,7 +68,10 @@ actions!(
|
|||
CurlyBrackets,
|
||||
AngleBrackets,
|
||||
Argument,
|
||||
Tag
|
||||
Tag,
|
||||
Method,
|
||||
Class,
|
||||
Comment
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -107,6 +117,18 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
|
|||
Vim::action(editor, cx, |vim, _: &Argument, cx| {
|
||||
vim.object(Object::Argument, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &Method, cx| {
|
||||
vim.object(Object::Method, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &Class, cx| {
|
||||
vim.object(Object::Class, cx)
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &Comment, cx| {
|
||||
if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
|
||||
vim.push_operator(Operator::Object { around: true }, cx);
|
||||
}
|
||||
vim.object(Object::Comment, cx)
|
||||
});
|
||||
Vim::action(
|
||||
editor,
|
||||
cx,
|
||||
|
@ -144,6 +166,9 @@ impl Object {
|
|||
| Object::CurlyBrackets
|
||||
| Object::SquareBrackets
|
||||
| Object::Argument
|
||||
| Object::Method
|
||||
| Object::Class
|
||||
| Object::Comment
|
||||
| Object::IndentObj { .. } => true,
|
||||
}
|
||||
}
|
||||
|
@ -162,12 +187,15 @@ impl Object {
|
|||
| Object::Parentheses
|
||||
| Object::SquareBrackets
|
||||
| Object::Tag
|
||||
| Object::Method
|
||||
| Object::Class
|
||||
| Object::Comment
|
||||
| Object::CurlyBrackets
|
||||
| Object::AngleBrackets => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
|
||||
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
|
||||
match self {
|
||||
Object::Word { .. }
|
||||
| Object::Sentence
|
||||
|
@ -186,8 +214,16 @@ impl Object {
|
|||
| Object::AngleBrackets
|
||||
| Object::VerticalBars
|
||||
| Object::Tag
|
||||
| Object::Comment
|
||||
| Object::Argument
|
||||
| Object::IndentObj { .. } => Mode::Visual,
|
||||
Object::Method | Object::Class => {
|
||||
if around {
|
||||
Mode::VisualLine
|
||||
} else {
|
||||
Mode::Visual
|
||||
}
|
||||
}
|
||||
Object::Paragraph => Mode::VisualLine,
|
||||
}
|
||||
}
|
||||
|
@ -238,6 +274,33 @@ impl Object {
|
|||
Object::AngleBrackets => {
|
||||
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
|
||||
}
|
||||
Object::Method => text_object(
|
||||
map,
|
||||
relative_to,
|
||||
if around {
|
||||
TextObject::AroundFunction
|
||||
} else {
|
||||
TextObject::InsideFunction
|
||||
},
|
||||
),
|
||||
Object::Comment => text_object(
|
||||
map,
|
||||
relative_to,
|
||||
if around {
|
||||
TextObject::AroundComment
|
||||
} else {
|
||||
TextObject::InsideComment
|
||||
},
|
||||
),
|
||||
Object::Class => text_object(
|
||||
map,
|
||||
relative_to,
|
||||
if around {
|
||||
TextObject::AroundClass
|
||||
} else {
|
||||
TextObject::InsideClass
|
||||
},
|
||||
),
|
||||
Object::Argument => argument(map, relative_to, around),
|
||||
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
|
||||
}
|
||||
|
@ -441,6 +504,47 @@ fn around_next_word(
|
|||
Some(start..end)
|
||||
}
|
||||
|
||||
fn text_object(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
target: TextObject,
|
||||
) -> Option<Range<DisplayPoint>> {
|
||||
let snapshot = &map.buffer_snapshot;
|
||||
let offset = relative_to.to_offset(map, Bias::Left);
|
||||
|
||||
let excerpt = snapshot.excerpt_containing(offset..offset)?;
|
||||
let buffer = excerpt.buffer();
|
||||
|
||||
let mut matches: Vec<Range<usize>> = buffer
|
||||
.text_object_ranges(offset..offset, TreeSitterOptions::default())
|
||||
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
|
||||
.collect();
|
||||
matches.sort_by_key(|r| (r.end - r.start));
|
||||
if let Some(range) = matches.first() {
|
||||
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
|
||||
}
|
||||
|
||||
let around = target.around()?;
|
||||
let mut matches: Vec<Range<usize>> = buffer
|
||||
.text_object_ranges(offset..offset, TreeSitterOptions::default())
|
||||
.filter_map(|(r, m)| if m == around { Some(r) } else { None })
|
||||
.collect();
|
||||
matches.sort_by_key(|r| (r.end - r.start));
|
||||
let around_range = matches.first()?;
|
||||
|
||||
let mut matches: Vec<Range<usize>> = buffer
|
||||
.text_object_ranges(around_range.clone(), TreeSitterOptions::default())
|
||||
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
|
||||
.collect();
|
||||
matches.sort_by_key(|r| r.start);
|
||||
if let Some(range) = matches.first() {
|
||||
if !range.is_empty() {
|
||||
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
|
||||
}
|
||||
}
|
||||
return Some(around_range.start.to_display_point(map)..around_range.end.to_display_point(map));
|
||||
}
|
||||
|
||||
fn argument(
|
||||
map: &DisplaySnapshot,
|
||||
relative_to: DisplayPoint,
|
||||
|
|
|
@ -308,7 +308,7 @@ impl Vim {
|
|||
if let Some(Operator::Object { around }) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
let current_mode = self.mode;
|
||||
let target_mode = object.target_visual_mode(current_mode);
|
||||
let target_mode = object.target_visual_mode(current_mode, around);
|
||||
if target_mode != current_mode {
|
||||
self.switch_mode(target_mode, true, cx);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue