use helix_core::{ syntax::RopeProvider, text_annotations::TextAnnotations, tree_sitter::{QueryCursor, QueryMatch}, visual_offset_from_block, Position, }; use helix_view::{Document, Theme, View}; use tui::buffer::Buffer as Surface; use super::{ document::{render_text, LineDecoration, TextRenderer, TranslatedPosition}, EditorView, }; #[derive(Debug, Clone)] pub struct StickyNode { line: usize, visual_line: u16, byte_range: std::ops::Range, indicator: Option, anchor: usize, 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> { // get all the captured @context.params nodes let end_nodes = once_cell::unsync::Lazy::new(|| { query_match .nodes_for_capture_index(end_index) .collect::>() }); 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)) }) }) } /// Calculates the sticky nodes pub fn calculate_sticky_nodes( nodes: &Option>, doc: &Document, view: &View, config: &helix_view::editor::Config, cursor_cache: &Option>, ) -> Option> { let cursor_cache = cursor_cache.expect("cursor is cached")?; 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 = anchor_line.saturating_sub(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 }; // 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(), ); } } let context_nodes = doc .language_config() .and_then(|lang| lang.context_query())?; let start_index = context_nodes.query.capture_index_for_name("context")?; let end_index = context_nodes .query .capture_index_for_name("context.params") .unwrap_or(start_index); // result is list of numbers of lines that should be rendered in the LSP context let mut result: Vec = Vec::new(); let mut cursor = QueryCursor::new(); // only run the query from start to the cursor location cursor.set_byte_range(0..last_scan_byte); let query = &context_nodes.query; let query_nodes = cursor.matches(query, tree.root_node(), RopeProvider(text)); for matched_node in query_nodes { // find @context.params nodes let node_byte_range = get_context_paired_range( &matched_node, start_index, end_index, top_first_byte, last_scan_byte, ); 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() { continue; } result.push(StickyNode { line: node.start_position().row, visual_line: 0, byte_range: node_byte_range .as_ref() .unwrap_or(&(node.start_byte()..node.start_byte())) .clone(), indicator: None, anchor: view.offset.anchor, has_context_end: node_byte_range.is_some(), }); } } // result should be filled by now if result.is_empty() { return None; } // Order of commands is important here result.sort_unstable_by(|lhs, rhs| lhs.line.cmp(&rhs.line)); result.dedup_by(|lhs, rhs| lhs.line == rhs.line); // 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 skip = result.len().saturating_sub(max_nodes_amount); result = result .iter() // only take the nodes until 1 / 3 of the viewport is reached or the maximum amount of sticky nodes .skip(skip) .enumerate() .take_while(|(i, _)| { *i + Into::::into(config.sticky_context.indicator) != visual_cursor_row as usize }) // also only nodes that don't overlap with the visual cursor position .map(|(i, node)| { let mut new_node = node.clone(); new_node.visual_line = i as u16; new_node }) .collect(); if config.sticky_context.indicator { let str = "─".repeat(viewport.width as usize); result.push(StickyNode { line: usize::MAX, visual_line: result.len() as u16, byte_range: 0..0, indicator: Some(str), anchor: view.offset.anchor, has_context_end: false, }); } Some(result) } /// Render the sticky context pub fn render_sticky_context( doc: &Document, view: &View, surface: &mut Surface, context: &Option>, line_decoration: &mut [Box], translated_positions: &mut [TranslatedPosition], theme: &Theme, ) { let Some(context) = context else { return; }; let text = doc.text().slice(..); let viewport = view.inner_area(doc); // backup (status line) shall always exist let status_line_style = theme .try_get("ui.statusline.context") .expect("`ui.statusline.context` exists"); // define sticky context styles let context_style = theme .try_get("ui.sticky.context") .unwrap_or(status_line_style); let indicator_style = theme .try_get("ui.sticky.indicator") .unwrap_or(status_line_style); let mut context_area = viewport; context_area.height = 1; for node in context { surface.clear_with(context_area, context_style); let mut new_offset = view.offset; let mut line_context_area = context_area; if let Some(indicator) = node.indicator.as_deref() { // set the indicator surface.set_stringn( line_context_area.x, line_context_area.y, indicator, indicator.len(), indicator_style, ); continue; } // get the len of bytes of the text that will be written (the "definition" line) let line = text.line(node.line); let already_written = line.len_bytes() as u16; let dots = "..."; let virtual_text_annotations = TextAnnotations::default(); // if the definition of the function contains multiple lines if node.has_context_end { let (whitespace_offset, _) = visual_offset_from_block( text, text.line_to_byte(text.byte_to_line(node.byte_range.end)), node.byte_range.end, &helix_core::doc_formatter::TextFormat::default(), &TextAnnotations::default(), ); let whitespace_offset = whitespace_offset.col as u16 + 1; // calculation of the correct space on where the end of the signature // should be drawn at let mut additional_area = line_context_area; additional_area.x += (already_written + dots.len() as u16).saturating_sub(whitespace_offset); // render the end of the function definition let mut renderer = TextRenderer::new( surface, doc, theme, view.offset.horizontal_offset, additional_area, ); new_offset.anchor = text.byte_to_char(node.byte_range.end); let highlights = EditorView::doc_syntax_highlights(doc, new_offset.anchor, 1, theme); let mut text_format = doc.text_format(additional_area.width, Some(theme)); text_format.soft_wrap = false; render_text( &mut renderer, text, new_offset, &text_format, &virtual_text_annotations, highlights, theme, line_decoration, translated_positions, ); // draw the "..." with the keyword.operator style let new_x_location = (already_written + line_context_area.x).saturating_sub(match doc.line_ending { helix_core::LineEnding::Crlf => 2, helix_core::LineEnding::LF => 1, }); surface.set_stringn( new_x_location, additional_area.y, dots, dots.len(), theme.get("keyword.operator"), ); } new_offset.anchor = text.byte_to_char(node.byte_range.start); // get all highlights from the latest point let highlights = EditorView::doc_syntax_highlights(doc, new_offset.anchor, 1, theme); let mut renderer = TextRenderer::new( surface, doc, theme, view.offset.horizontal_offset, line_context_area, ); // limit the width to its size - 1, so that it won't draw trailing whitespace characters line_context_area.width = already_written - 1; let mut text_format = doc.text_format(line_context_area.width, Some(theme)); text_format.soft_wrap = false; render_text( &mut renderer, text, new_offset, &text_format, &virtual_text_annotations, highlights, theme, line_decoration, translated_positions, ); // next node context_area.y += 1; } }