|
|
|
@ -4,11 +4,11 @@ use helix_core::visual_coords_at_pos;
|
|
|
|
|
use helix_core::{
|
|
|
|
|
syntax::RopeProvider,
|
|
|
|
|
text_annotations::TextAnnotations,
|
|
|
|
|
tree_sitter::{QueryCursor, QueryMatch},
|
|
|
|
|
tree_sitter::{Node, QueryCursor, QueryMatch},
|
|
|
|
|
Position,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use helix_view::{view::ViewPosition, Document, Theme, View};
|
|
|
|
|
use helix_view::{editor::Config, graphics::Rect, view::ViewPosition, Document, Theme, View};
|
|
|
|
|
|
|
|
|
|
use tui::buffer::Buffer as Surface;
|
|
|
|
|
|
|
|
|
@ -27,42 +27,61 @@ pub struct StickyNode {
|
|
|
|
|
pub has_context_end: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_context_paired_range(
|
|
|
|
|
query_match: &QueryMatch,
|
|
|
|
|
start_index: u32,
|
|
|
|
|
end_index: u32,
|
|
|
|
|
top_first_byte: usize,
|
|
|
|
|
last_scan_byte: usize,
|
|
|
|
|
) -> Option<std::ops::Range<usize>> {
|
|
|
|
|
// get all the captured @context.params nodes
|
|
|
|
|
let end_nodes = once_cell::unsync::Lazy::new(|| {
|
|
|
|
|
query_match
|
|
|
|
|
.nodes_for_capture_index(end_index)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
});
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct StickyNodeContext {
|
|
|
|
|
/// The row on which the cursor is placed
|
|
|
|
|
pub visual_row: usize,
|
|
|
|
|
/// This marks the location of which we take possible out of range nodes
|
|
|
|
|
/// e.g. on follows-cursor: 'on', this would be the parent nodes, relative to the *cursor*
|
|
|
|
|
/// on follows-cursor: 'off', this would be the parent nodes, relative to the *topmost screen*
|
|
|
|
|
pub context_location: usize,
|
|
|
|
|
/// The topmost byte that is visible (and not hidden behind sticky nodes)
|
|
|
|
|
pub topmost_byte: usize,
|
|
|
|
|
/// The anchor of the view offset
|
|
|
|
|
pub anchor_line: usize,
|
|
|
|
|
/// The viewport
|
|
|
|
|
pub viewport: Rect,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query_match
|
|
|
|
|
.nodes_for_capture_index(start_index)
|
|
|
|
|
.find_map(|context| {
|
|
|
|
|
let ctx_start_range = context.byte_range();
|
|
|
|
|
impl StickyNodeContext {
|
|
|
|
|
pub fn from_context(
|
|
|
|
|
last_nodes: &Option<Vec<StickyNode>>,
|
|
|
|
|
doc: &Document,
|
|
|
|
|
view: &View,
|
|
|
|
|
config: &Config,
|
|
|
|
|
cursor_cache: &Option<Option<Position>>,
|
|
|
|
|
) -> Option<Self> {
|
|
|
|
|
let Some(cursor_cache) = cursor_cache else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
let cursor_cache = cursor_cache.as_ref()?;
|
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
|
let viewport = view.inner_area(doc);
|
|
|
|
|
let cursor_byte = text.char_to_byte(doc.selection(view.id).primary().cursor(text));
|
|
|
|
|
|
|
|
|
|
// filter all matches that are out of scope, based on follows-cursor
|
|
|
|
|
let start_range_contains_bytes = ctx_start_range.contains(&top_first_byte)
|
|
|
|
|
&& ctx_start_range.contains(&last_scan_byte);
|
|
|
|
|
if !start_range_contains_bytes {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let anchor_line = text.char_to_line(view.offset.anchor);
|
|
|
|
|
let visual_cursor_row = cursor_cache.row;
|
|
|
|
|
|
|
|
|
|
let ctx_start_row = context.start_position().row;
|
|
|
|
|
let ctx_start_byte = ctx_start_range.start;
|
|
|
|
|
if visual_cursor_row == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
end_nodes.iter().find_map(|it| {
|
|
|
|
|
let end = it.end_byte();
|
|
|
|
|
// check whether or not @context.params nodes are on different lines
|
|
|
|
|
(ctx_start_row != it.end_position().row && ctx_start_range.contains(&end))
|
|
|
|
|
.then_some(ctx_start_byte..end.saturating_sub(1))
|
|
|
|
|
})
|
|
|
|
|
let top_first_byte =
|
|
|
|
|
text.line_to_byte(anchor_line + last_nodes.as_ref().map_or(0, |nodes| nodes.len()));
|
|
|
|
|
|
|
|
|
|
let last_scan_byte = if config.sticky_context.follow_cursor {
|
|
|
|
|
cursor_byte
|
|
|
|
|
} else {
|
|
|
|
|
top_first_byte
|
|
|
|
|
};
|
|
|
|
|
Some(Self {
|
|
|
|
|
visual_row: visual_cursor_row,
|
|
|
|
|
context_location: last_scan_byte,
|
|
|
|
|
topmost_byte: top_first_byte,
|
|
|
|
|
anchor_line,
|
|
|
|
|
viewport,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Calculates the sticky nodes
|
|
|
|
@ -70,59 +89,33 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
nodes: &Option<Vec<StickyNode>>,
|
|
|
|
|
doc: &Document,
|
|
|
|
|
view: &View,
|
|
|
|
|
config: &helix_view::editor::Config,
|
|
|
|
|
config: &Config,
|
|
|
|
|
cursor_cache: &Option<Option<Position>>,
|
|
|
|
|
) -> Option<Vec<StickyNode>> {
|
|
|
|
|
let Some(cursor_cache) = cursor_cache else {
|
|
|
|
|
let Some(context) = StickyNodeContext::from_context(nodes, doc, view, config, cursor_cache)
|
|
|
|
|
else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
let cursor_cache = cursor_cache.as_ref()?;
|
|
|
|
|
|
|
|
|
|
let syntax = doc.syntax()?;
|
|
|
|
|
let tree = syntax.tree();
|
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
|
let viewport = view.inner_area(doc);
|
|
|
|
|
let cursor_byte = text.char_to_byte(doc.selection(view.id).primary().cursor(text));
|
|
|
|
|
|
|
|
|
|
let anchor_line = text.char_to_line(view.offset.anchor);
|
|
|
|
|
let visual_cursor_row = cursor_cache.row;
|
|
|
|
|
|
|
|
|
|
if visual_cursor_row == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let top_first_byte =
|
|
|
|
|
text.line_to_byte(anchor_line + nodes.as_ref().map_or(0, |nodes| nodes.len()));
|
|
|
|
|
|
|
|
|
|
let last_scan_byte = if config.sticky_context.follow_cursor {
|
|
|
|
|
cursor_byte
|
|
|
|
|
} else {
|
|
|
|
|
top_first_byte
|
|
|
|
|
};
|
|
|
|
|
let mut cached_nodes: Vec<StickyNode> = Vec::new();
|
|
|
|
|
|
|
|
|
|
// nothing has changed, so the cached result can be returned
|
|
|
|
|
if let Some(nodes) = nodes {
|
|
|
|
|
if nodes.iter().any(|node| view.offset.anchor == node.anchor) {
|
|
|
|
|
return Some(
|
|
|
|
|
nodes
|
|
|
|
|
.iter()
|
|
|
|
|
.take(visual_cursor_row as usize)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect(),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
cached_nodes = nodes.clone();
|
|
|
|
|
// clear up the last node
|
|
|
|
|
if let Some(popped) = cached_nodes.pop() {
|
|
|
|
|
if popped.indicator.is_some() {
|
|
|
|
|
_ = cached_nodes.pop();
|
|
|
|
|
}
|
|
|
|
|
return Some(nodes.iter().take(context.visual_row).cloned().collect());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cached_nodes = nodes.clone();
|
|
|
|
|
if let Some(popped) = cached_nodes.pop() {
|
|
|
|
|
if popped.indicator.is_some() {
|
|
|
|
|
_ = cached_nodes.pop();
|
|
|
|
|
}
|
|
|
|
|
// the node before is also important to clear, as in upwards movement
|
|
|
|
|
// we might encounter issues there
|
|
|
|
|
_ = cached_nodes.pop();
|
|
|
|
|
}
|
|
|
|
|
_ = cached_nodes.pop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let start_byte_range = cached_nodes
|
|
|
|
@ -134,7 +127,7 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
let start_byte = if start_byte_range.start != tree.root_node().start_byte() {
|
|
|
|
|
start_byte_range.start
|
|
|
|
|
} else {
|
|
|
|
|
last_scan_byte
|
|
|
|
|
context.context_location
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut start_node = tree
|
|
|
|
@ -172,7 +165,7 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
|
|
|
|
|
// only run the query from start to the cursor location
|
|
|
|
|
let mut cursor = QueryCursor::new();
|
|
|
|
|
cursor.set_byte_range(start_byte_range.start..last_scan_byte);
|
|
|
|
|
cursor.set_byte_range(start_byte_range.start..context.context_location);
|
|
|
|
|
let query = &context_nodes.query;
|
|
|
|
|
let query_nodes = cursor.matches(
|
|
|
|
|
query,
|
|
|
|
@ -186,16 +179,19 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
&matched_node,
|
|
|
|
|
start_index,
|
|
|
|
|
end_index,
|
|
|
|
|
top_first_byte,
|
|
|
|
|
last_scan_byte,
|
|
|
|
|
context.topmost_byte,
|
|
|
|
|
context.context_location,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for node in matched_node.nodes_for_capture_index(start_index) {
|
|
|
|
|
if (!node.byte_range().contains(&last_scan_byte)
|
|
|
|
|
|| !node.byte_range().contains(&top_first_byte))
|
|
|
|
|
&& node.start_position().row != anchor_line + result.len()
|
|
|
|
|
&& node_byte_range.is_none()
|
|
|
|
|
{
|
|
|
|
|
if node_in_range(
|
|
|
|
|
node,
|
|
|
|
|
context.anchor_line,
|
|
|
|
|
&node_byte_range.clone(),
|
|
|
|
|
context.context_location,
|
|
|
|
|
context.topmost_byte,
|
|
|
|
|
result.len(),
|
|
|
|
|
) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -233,7 +229,7 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
// always cap the maximum amount of sticky contextes to 1/3 of the viewport
|
|
|
|
|
// unless configured otherwise
|
|
|
|
|
let max_lines = config.sticky_context.max_lines as u16;
|
|
|
|
|
let max_nodes_amount = max_lines.min(viewport.height / 3) as usize;
|
|
|
|
|
let max_nodes_amount = max_lines.min(context.viewport.height / 3) as usize;
|
|
|
|
|
|
|
|
|
|
let skip = res.len().saturating_sub(max_nodes_amount);
|
|
|
|
|
|
|
|
|
@ -243,7 +239,7 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
.skip(skip)
|
|
|
|
|
.enumerate()
|
|
|
|
|
.take_while(|(i, _)| {
|
|
|
|
|
*i + Into::<usize>::into(config.sticky_context.indicator) != visual_cursor_row as usize
|
|
|
|
|
*i + Into::<usize>::into(config.sticky_context.indicator) != context.visual_row
|
|
|
|
|
}) // also only nodes that don't overlap with the visual cursor position
|
|
|
|
|
.map(|(i, node)| {
|
|
|
|
|
let mut new_node = node.clone();
|
|
|
|
@ -253,21 +249,78 @@ pub fn calculate_sticky_nodes(
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if config.sticky_context.indicator {
|
|
|
|
|
let str = "─".repeat(viewport.width as usize);
|
|
|
|
|
|
|
|
|
|
res.push(StickyNode {
|
|
|
|
|
line: usize::MAX,
|
|
|
|
|
visual_line: res.len() as u16,
|
|
|
|
|
byte_range: 0..0,
|
|
|
|
|
indicator: Some(str),
|
|
|
|
|
anchor: view.offset.anchor,
|
|
|
|
|
has_context_end: false,
|
|
|
|
|
});
|
|
|
|
|
res = add_indicator(&context.viewport, view, res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_context_paired_range(
|
|
|
|
|
query_match: &QueryMatch,
|
|
|
|
|
start_index: u32,
|
|
|
|
|
end_index: u32,
|
|
|
|
|
top_first_byte: usize,
|
|
|
|
|
last_scan_byte: usize,
|
|
|
|
|
) -> Option<std::ops::Range<usize>> {
|
|
|
|
|
// get all the captured @context.params nodes
|
|
|
|
|
let end_nodes = once_cell::unsync::Lazy::new(|| {
|
|
|
|
|
query_match
|
|
|
|
|
.nodes_for_capture_index(end_index)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
query_match
|
|
|
|
|
.nodes_for_capture_index(start_index)
|
|
|
|
|
.find_map(|context| {
|
|
|
|
|
let ctx_start_range = context.byte_range();
|
|
|
|
|
|
|
|
|
|
// filter all matches that are out of scope, based on follows-cursor
|
|
|
|
|
let start_range_contains_bytes = ctx_start_range.contains(&top_first_byte)
|
|
|
|
|
&& ctx_start_range.contains(&last_scan_byte);
|
|
|
|
|
if !start_range_contains_bytes {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ctx_start_row = context.start_position().row;
|
|
|
|
|
let ctx_start_byte = ctx_start_range.start;
|
|
|
|
|
|
|
|
|
|
end_nodes.iter().find_map(|it| {
|
|
|
|
|
let end = it.end_byte();
|
|
|
|
|
// check whether or not @context.params nodes are on different lines
|
|
|
|
|
(ctx_start_row != it.end_position().row && ctx_start_range.contains(&end))
|
|
|
|
|
.then_some(ctx_start_byte..end.saturating_sub(1))
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn node_in_range(
|
|
|
|
|
node: Node,
|
|
|
|
|
anchor: usize,
|
|
|
|
|
node_byte_range: &Option<std::ops::Range<usize>>,
|
|
|
|
|
last_scan_byte: usize,
|
|
|
|
|
topmost_byte: usize,
|
|
|
|
|
result_len: usize,
|
|
|
|
|
) -> bool {
|
|
|
|
|
(!node.byte_range().contains(&last_scan_byte) || !node.byte_range().contains(&topmost_byte))
|
|
|
|
|
&& node.start_position().row != anchor + result_len
|
|
|
|
|
&& node_byte_range.is_none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn add_indicator(viewport: &Rect, view: &View, res: Vec<StickyNode>) -> Vec<StickyNode> {
|
|
|
|
|
let mut res = res;
|
|
|
|
|
let str = "─".repeat(viewport.width as usize);
|
|
|
|
|
res.push(StickyNode {
|
|
|
|
|
line: usize::MAX,
|
|
|
|
|
visual_line: res.len() as u16,
|
|
|
|
|
byte_range: 0..0,
|
|
|
|
|
indicator: Some(str),
|
|
|
|
|
anchor: view.offset.anchor,
|
|
|
|
|
has_context_end: false,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Render the sticky context
|
|
|
|
|
pub fn render_sticky_context(
|
|
|
|
|
doc: &Document,
|
|
|
|
|