Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Commit

Permalink
First full and working implemantation of text-object. #27
Browse files Browse the repository at this point in the history
Object-mode requires a bit more work, because we can’t just use use the before/after node; we need to find the
enclosing node, which will come in the next commit.
  • Loading branch information
hadronized committed Feb 27, 2024
1 parent 5a0a3f6 commit 42935c9
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 58 deletions.
16 changes: 14 additions & 2 deletions kak-tree-sitter/rc/static.kak
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
declare-option str kts_cmd_fifo_path /dev/null

# FIFO buffer path; this is used by Kakoune to write the content of buffers to be highlighted / analyzed by KTS for the
# current session.
# current session.
declare-option str kts_buf_fifo_path /dev/null

# Highlight ranges used when highlighting buffers.
Expand Down Expand Up @@ -95,15 +95,27 @@ define-command kak-tree-sitter-req-highlight-buffer -docstring 'Highlight the cu
}

# Send a single request to modify selections with text-objects.
#
# The pattern must be full; e.g. 'function.inside'.
define-command kak-tree-sitter-req-text-objects -params 2 %{
evaluate-commands -no-hooks %{
echo -to-file %opt{kts_cmd_fifo_path} -- "{ ""type"": ""text_objects"", ""client"": ""%val{client}"", ""buffer"": ""%val{bufname}"", ""lang"": ""%opt{kts_lang}"", ""pattern"": ""%arg{1}"", ""selections"": ""%val{selections_desc}"", ""mode"": ""%arg{2}"" }"
write %opt{kts_buf_fifo_path}
}
}

# Send a single request to modify selections with text-objects in object-mode.
#
# The pattern must be expressed without the level — e.g. 'function' — as the level is deduced from %val{object_flags}.
define-command kak-tree-sitter-req-object-text-objects -params 1 %{
evaluate-commands -no-hooks %{
echo -to-file %opt{kts_cmd_fifo_path} -- "{ ""type"": ""text_objects"", ""client"": ""%val{client}"", ""buffer"": ""%val{bufname}"", ""lang"": ""%opt{kts_lang}"", ""pattern"": ""%arg{1}"", ""selections"": ""%val{selections_desc}"", ""mode"": { ""object"": { ""mode"": ""%val{select_mode}"", ""flags"": ""%val{object_flags}"" } } }"
write %opt{kts_buf_fifo_path}
}
}

# Enable highlighting for the current buffer.
#
#
# This command does a couple of things, among removing the « default » highlighting (Kakoune based) of the buffer and
# installing some hooks to automatically highlight the buffer.
define-command -hidden kak-tree-sitter-highlight-enable -docstring 'Enable tree-sitter highlighting for this buffer' %{
Expand Down
5 changes: 5 additions & 0 deletions kak-tree-sitter/rc/text-objects.kak
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ map global tree-sitter-find-extend-rev f ':kak-tree-sitter-req-text-objects func
map global tree-sitter-find-extend-rev a ':kak-tree-sitter-req-text-objects parameter.around extend_prev<ret>' -docstring 'parameter'
map global tree-sitter-find-extend-rev t ':kak-tree-sitter-req-text-objects class.around extend_prev<ret>' -docstring 'class'
map global tree-sitter-find-extend-rev T ':kak-tree-sitter-req-text-objects test.around extend_prev<ret>' -docstring 'test'

map global object f '<a-;>kak-tree-sitter-req-object-text-objects function<ret>' -docstring 'function (tree-sitter)'
map global object t '<a-;>kak-tree-sitter-req-object-text-objects class<ret>' -docstring 'type (tree-sitter)'
map global object a '<a-;>kak-tree-sitter-req-object-text-objects parameter<ret>' -docstring 'argument (tree-sitter)'
map global object T '<a-;>kak-tree-sitter-req-object-text-objects test<ret>' -docstring 'test (tree-sitter)'
8 changes: 8 additions & 0 deletions kak-tree-sitter/src/selection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Selections as recognized by Kakoune, as well as associated types and functions.

use serde::{Deserialize, Serialize};
use tree_sitter::Point;

/// A single position in a buffer.
Expand Down Expand Up @@ -125,6 +126,13 @@ impl ObjectFlags {
}
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SelectMode {
Replace,
Extend,
}

#[cfg(test)]
mod tests {
use super::{Pos, Sel};
Expand Down
2 changes: 1 addition & 1 deletion kak-tree-sitter/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ impl FifoHandler {
lang: lang.clone(),
pattern: pattern.clone(),
selections,
mode: *mode,
mode: mode.clone(),
};

Ok(None)
Expand Down
62 changes: 61 additions & 1 deletion kak-tree-sitter/src/text_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

use serde::{Deserialize, Serialize};

use crate::selection::SelectMode;

/// Text-objects can be manipulated in two different ways:
///
/// - In object mode, to expand selections or replace them.
/// - To shrink selections via selecting or splitting, as in `s`, `S`, etc.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationMode {
/// Search for the next text-object.
Expand Down Expand Up @@ -54,4 +56,62 @@ pub enum OperationMode {
///
/// Similar to `<a-F>`.
ExtendPrev,
/// Object mode.
///
/// This combines select mode with object flags.
Object { mode: SelectMode, flags: String },
}

#[cfg(test)]
mod tests {
use crate::selection::SelectMode;

use super::OperationMode;

#[test]
fn deser() {
assert_eq!(
serde_json::from_str::<OperationMode>("\"search_next\"").unwrap(),
OperationMode::SearchNext
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"search_prev\"").unwrap(),
OperationMode::SearchPrev
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"search_extend_next\"").unwrap(),
OperationMode::SearchExtendNext
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"search_extend_prev\"").unwrap(),
OperationMode::SearchExtendPrev
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"find_next\"").unwrap(),
OperationMode::FindNext
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"find_prev\"").unwrap(),
OperationMode::FindPrev
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"extend_next\"").unwrap(),
OperationMode::ExtendNext
);
assert_eq!(
serde_json::from_str::<OperationMode>("\"extend_prev\"").unwrap(),
OperationMode::ExtendPrev
);

assert_eq!(
serde_json::from_str::<OperationMode>(
r#"{ "object": { "mode": "replace", "flags": "to_begin|to_end|inner" }}"#
)
.unwrap(),
OperationMode::Object {
mode: SelectMode::Replace,
flags: "to_begin|to_end|inner".to_owned()
}
);
}
}
205 changes: 151 additions & 54 deletions kak-tree-sitter/src/tree_sitter_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
error::OhNo,
highlighting::KakHighlightRange,
languages::Language,
selection::{Pos, Sel},
selection::{ObjectFlags, Pos, Sel, SelectMode},
text_objects,
};

Expand Down Expand Up @@ -80,61 +80,104 @@ impl TreeState {
.textobject_query
.as_ref()
.ok_or_else(|| OhNo::UnsupportedTextObjects)?;
let capture_index =
query
.capture_index_for_name(pattern)
.ok_or(OhNo::UnknownTextObjectQuery {
pattern: pattern.to_owned(),
})?;

// run the query via a query cursor
let mut cursor = QueryCursor::new();
let captures: Vec<_> = cursor
.captures(query, self.tree.root_node(), buf.as_bytes())
.flat_map(|(cm, _)| cm.captures.iter().cloned())
.filter(|cq| cq.index == capture_index)
.collect();

// get captures for the given pattern; this is a function because the pattern might be dynamically recomputed (e.g.
// object mode)
let get_captures = |pattern| {
let capture_index =
query
.capture_index_for_name(pattern)
.ok_or(OhNo::UnknownTextObjectQuery {
pattern: pattern.to_owned(),
})?;
let mut cursor = QueryCursor::new();
let captures: Vec<_> = cursor
.captures(query, self.tree.root_node(), buf.as_bytes())
.flat_map(|(cm, _)| cm.captures.iter().cloned())
.filter(|cq| cq.index == capture_index)
.collect();
<Result<_, OhNo>>::Ok(captures)
};

let sels = match mode {
text_objects::OperationMode::SearchNext => selections
.iter()
.flat_map(|sel| Self::search_next_text_object(sel, &captures[..]))
.collect(),

text_objects::OperationMode::SearchPrev => selections
.iter()
.flat_map(|sel| Self::search_prev_text_object(sel, &captures[..]))
.collect(),

text_objects::OperationMode::SearchExtendNext => selections
.iter()
.flat_map(|sel| Self::search_extend_next_text_object(sel, &captures[..]))
.collect(),

text_objects::OperationMode::SearchExtendPrev => selections
.iter()
.flat_map(|sel| Self::search_extend_prev_text_object(sel, &captures[..]))
.collect(),

text_objects::OperationMode::FindNext => selections
.iter()
.flat_map(|sel| Self::find_text_object(sel, &captures[..], false))
.collect(),

text_objects::OperationMode::FindPrev => selections
.iter()
.flat_map(|sel| Self::find_text_object(sel, &captures[..], true))
.collect(),

text_objects::OperationMode::ExtendNext => selections
.iter()
.flat_map(|sel| Self::extend_text_object(sel, &captures[..], false))
.collect(),

text_objects::OperationMode::ExtendPrev => selections
.iter()
.flat_map(|sel| Self::extend_text_object(sel, &captures[..], true))
.collect(),
text_objects::OperationMode::SearchNext => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::search_next_text_object(sel, &captures[..]))
.collect()
}

text_objects::OperationMode::SearchPrev => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::search_prev_text_object(sel, &captures[..]))
.collect()
}

text_objects::OperationMode::SearchExtendNext => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::search_extend_next_text_object(sel, &captures[..]))
.collect()
}

text_objects::OperationMode::SearchExtendPrev => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::search_extend_prev_text_object(sel, &captures[..]))
.collect()
}

text_objects::OperationMode::FindNext => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::find_text_object(sel, &captures[..], false))
.collect()
}

text_objects::OperationMode::FindPrev => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::find_text_object(sel, &captures[..], true))
.collect()
}

text_objects::OperationMode::ExtendNext => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::extend_text_object(sel, &captures[..], false))
.collect()
}

text_objects::OperationMode::ExtendPrev => {
let captures = get_captures(pattern)?;
selections
.iter()
.flat_map(|sel| Self::extend_text_object(sel, &captures[..], true))
.collect()
}

text_objects::OperationMode::Object { mode, flags } => {
let flags = ObjectFlags::parse_kak_str(flags);

let pattern = format!(
"{pattern}.{}",
if flags.inner { "inside" } else { "around" }
);
let captures = get_captures(&pattern)?;

selections
.iter()
.flat_map(|sel| Self::object_text_object(sel, &captures[..], *mode, flags))
.collect()
}
};

Ok(sels)
Expand Down Expand Up @@ -210,6 +253,60 @@ impl TreeState {
Some(Sel { anchor, cursor })
}

/// Object-mode text-objects.
///
/// Object-mode is a special in Kakoune aggregating many features, allowing to match inner / whole objects. The
/// tree-sitter version enhances the mode with all possible tree-sitter capture groups.
fn object_text_object(
sel: &Sel,
captures: &[QueryCapture],
mode: SelectMode,
flags: ObjectFlags,
) -> Option<Sel> {
// FIXME: this is probably wrong, as we should instead look for the enclosing object, not the previous one, but it’s
// fine for the heck of testing
let capture = Self::node_before(&sel.cursor, captures)?;

match mode {
// extend only moves the cursor
SelectMode::Extend => {
let anchor = sel.anchor;
let cursor = if flags.to_begin {
Pos::from(capture.node.start_position())
} else if flags.to_end {
let mut p = Pos::from(capture.node.end_position());
p.col -= 1;
p
} else {
return None;
};

Some(Sel { anchor, cursor })
}

SelectMode::Replace => {
// brute force but eh it works lol
if flags.to_begin && !flags.to_end {
let anchor = sel.cursor;
let cursor = Pos::from(capture.node.start_position());
Some(Sel { anchor, cursor })
} else if !flags.to_begin && flags.to_end {
let anchor = sel.cursor;
let mut cursor = Pos::from(capture.node.end_position());
cursor.col -= 1;
Some(Sel { anchor, cursor })
} else if flags.to_begin && flags.to_end {
let anchor = Pos::from(capture.node.start_position());
let mut cursor = Pos::from(capture.node.end_position());
cursor.col -= 1;
Some(Sel { anchor, cursor })
} else {
None
}
}
}
}

/// Get the next node after given position.
fn node_after<'a>(p: &Pos, captures: &[QueryCapture<'a>]) -> Option<QueryCapture<'a>> {
// tree-sitter API here is HORRIBLE as it mutates in-place on Iterator::next(); we can’t collect();
Expand Down

0 comments on commit 42935c9

Please sign in to comment.