feat: implement sticky-context

A lot more work has been put into this and those were 116 commits up to
this point.
I decided to squash all of them so that i will have an easier time
rebasing in the future.
pull/6118/head
SoraTenshi 4 months ago
parent 237cbe4bca
commit c59f72f237
No known key found for this signature in database

@ -17,6 +17,7 @@
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section) - [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
- [`[editor.smart-tab]` Section](#editorsmart-tab-section) - [`[editor.smart-tab]` Section](#editorsmart-tab-section)
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section) - [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
- [`[editor.sticky-context]` Section](#editorsticky-context-section)
### `[editor]` Section ### `[editor]` Section
@ -433,3 +434,23 @@ end-of-line-diagnostics = "hint"
[editor.inline-diagnostics] [editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline cursor-line = "warning" # show warnings and errors on the cursorline inline
``` ```
### `[editor.sticky-context]` Section
Option for sticky context, which is a feature that puts bigger blocks of code
e.g. Functions to the top of the viewport
| Key | Description | Default |
| --- | --- | --- |
| `enable` | Display context of current line if outside the view | `false` |
| `indicator` | Display an additional line to indicate what part of the view is the sticky context | `false` |
| `max-lines` | The maximum number of lines to be shown as sticky context. 0 = 1/3 of the viewports height | `0` |
Example:
```toml
[editor.sticky-context]
enable = true
indicator = true
max-lines = 10
```

@ -0,0 +1,26 @@
# Adding context queries
Helix uses tree-sitter to filter out specific scopes in which said scope may exceed the current
editor view, but which may be important for the developer to know.
These context require an accompanying tree-sitter grammar and a `context.scm` query file
to work properly.
Query files should be placed in `runtime/queries/{language}/context.scm`
when contributing to Helix. Note that to test the query files locally you should put
them under your local runtime directory (`~/.config/helix/runtime` on Linux for example).
The following [captures][tree-sitter-captures] are recognized:
| Capture Name |
| --- |
| `context` |
| `context.params` |
[Example query files][context-examples] can be found in the helix GitHub repository.
## Queries for the sticky-context feature
All nodes that have a scope, should be captured with `context`, as an example a basic class.
The `context.params` is a capture for all the function parameters.
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
[context-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Acontext.scm&type=Code&ref=advsearch&l=&l=

@ -155,6 +155,10 @@ pub struct LanguageConfiguration {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>, pub debugger: Option<DebugAdapterConfig>,
/// The Grammar query for Sticky Context
#[serde(skip)]
pub(crate) context_query: OnceCell<Option<ContextQuery>>,
/// Automatic insertion of pairs to parentheses, brackets, /// Automatic insertion of pairs to parentheses, brackets,
/// etc. Defaults to true. Optionally, this can be a list of 2-tuples /// etc. Defaults to true. Optionally, this can be a list of 2-tuples
/// to specify a list of characters to pair. This overrides the /// to specify a list of characters to pair. This overrides the
@ -609,6 +613,11 @@ pub struct TextObjectQuery {
pub query: Query, pub query: Query,
} }
#[derive(Debug)]
pub struct ContextQuery {
pub query: Query,
}
#[derive(Debug)] #[derive(Debug)]
pub enum CapturedNode<'a> { pub enum CapturedNode<'a> {
Single(Node<'a>), Single(Node<'a>),
@ -804,6 +813,15 @@ impl LanguageConfiguration {
.as_ref() .as_ref()
} }
pub fn context_query(&self) -> Option<&ContextQuery> {
self.context_query
.get_or_init(|| {
self.load_query("context.scm")
.map(|query| ContextQuery { query })
})
.as_ref()
}
pub fn scope(&self) -> &str { pub fn scope(&self) -> &str {
&self.scope &self.scope
} }

@ -12,11 +12,17 @@ pub enum TsFeature {
Highlight, Highlight,
TextObject, TextObject,
AutoIndent, AutoIndent,
Context,
} }
impl TsFeature { impl TsFeature {
pub fn all() -> &'static [Self] { pub fn all() -> &'static [Self] {
&[Self::Highlight, Self::TextObject, Self::AutoIndent] &[
Self::Highlight,
Self::TextObject,
Self::AutoIndent,
Self::Context,
]
} }
pub fn runtime_filename(&self) -> &'static str { pub fn runtime_filename(&self) -> &'static str {
@ -24,6 +30,7 @@ impl TsFeature {
Self::Highlight => "highlights.scm", Self::Highlight => "highlights.scm",
Self::TextObject => "textobjects.scm", Self::TextObject => "textobjects.scm",
Self::AutoIndent => "indents.scm", Self::AutoIndent => "indents.scm",
Self::Context => "context.scm",
} }
} }
@ -32,6 +39,7 @@ impl TsFeature {
Self::Highlight => "Syntax Highlighting", Self::Highlight => "Syntax Highlighting",
Self::TextObject => "Treesitter Textobjects", Self::TextObject => "Treesitter Textobjects",
Self::AutoIndent => "Auto Indent", Self::AutoIndent => "Auto Indent",
Self::Context => "Sticky Context",
} }
} }
@ -40,6 +48,7 @@ impl TsFeature {
Self::Highlight => "Highlight", Self::Highlight => "Highlight",
Self::TextObject => "Textobject", Self::TextObject => "Textobject",
Self::AutoIndent => "Indent", Self::AutoIndent => "Indent",
Self::Context => "Context",
} }
} }
} }

@ -0,0 +1,521 @@
#[allow(deprecated)]
use helix_core::visual_coords_at_pos;
use helix_core::{
syntax::RopeProvider,
text_annotations::TextAnnotations,
tree_sitter::{Node, QueryCursor, QueryMatch},
Position,
};
use helix_view::{editor::Config, graphics::Rect, Document, Theme, View, ViewId};
use tui::buffer::Buffer as Surface;
use crate::ui::text_decorations::DecorationManager;
use super::{
document::{render_text, TextRenderer},
EditorView,
};
#[derive(Debug, Default, Clone)]
pub struct StickyNode {
pub line: usize,
pub visual_line: u16,
pub byte_range: std::ops::Range<usize>,
pub indicator: Option<String>,
pub anchor: usize,
pub has_context_end: bool,
pub view_id: ViewId,
}
#[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,
}
impl StickyNodeContext {
pub fn from_context(
last_nodes: Option<&Vec<StickyNode>>,
doc: &Document,
view: &View,
config: &Config,
cursor_cache: Option<&Position>,
) -> Option<Self> {
let Some(cursor_cache) = cursor_cache else {
return None;
};
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(doc.view_offset(view.id).anchor);
let visual_cursor_row = cursor_cache.row;
if visual_cursor_row == 0 {
return None;
}
let top_first_byte = text
.try_line_to_byte(anchor_line + last_nodes.as_ref().map_or(0, |nodes| nodes.len()))
.ok()?;
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
pub fn calculate_sticky_nodes(
nodes: Option<&Vec<StickyNode>>,
doc: &Document,
view: &View,
config: &Config,
cursor_cache: Option<&Position>,
) -> Option<Vec<StickyNode>> {
let Some(mut context) = StickyNodeContext::from_context(nodes, doc, view, config, cursor_cache)
else {
return None;
};
let syntax = doc.syntax()?;
let tree = syntax.tree();
let text = doc.text().slice(..);
let mut cached_nodes = build_cached_nodes(doc, nodes, view, &mut context).unwrap_or_default();
if cached_nodes.iter().any(|node| node.view_id != view.id) {
cached_nodes.clear();
}
let start_byte_range = cached_nodes
.last()
.unwrap_or(&StickyNode::default())
.byte_range
.clone();
let start_byte = if start_byte_range.start != tree.root_node().start_byte() {
start_byte_range.start
} else {
context.context_location
};
let mut result: Vec<StickyNode> = Vec::new();
let mut start_node = tree
.root_node()
.descendant_for_byte_range(start_byte, start_byte.saturating_sub(1));
// When the start_node is the root node... there's no point in searching further
if let Some(start_node) = start_node {
if start_node.byte_range() == tree.root_node().byte_range() {
return None;
}
}
// Traverse to the parent node
while start_node
.unwrap_or_else(|| tree.root_node())
.parent()
.unwrap_or_else(|| tree.root_node())
.byte_range()
!= tree.root_node().byte_range()
{
start_node = start_node.expect("parent exists").parent();
}
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);
let mut cursor = QueryCursor::new();
cursor.set_byte_range(start_byte_range.start..context.context_location);
let query = &context_nodes.query;
// Collect the query, for further iteration
let query_matches = cursor.matches(
query,
start_node.unwrap_or_else(|| tree.root_node()),
RopeProvider(text),
);
for matched_node in query_matches {
// find @context.params nodes
let node_byte_range = get_context_paired_range(
&matched_node,
start_index,
end_index,
context.topmost_byte,
context.context_location,
);
for node in matched_node.nodes_for_capture_index(start_index) {
let mut last_node_add = 0;
if let Some(last_node) = result.last() {
if last_node.line == (node.start_position().row + 1) {
last_node_add += text
.line(text.byte_to_line(context.topmost_byte))
.len_bytes();
}
}
if node_in_range(
node,
context.anchor_line,
node_byte_range.as_ref(),
context.context_location,
context.topmost_byte + last_node_add,
result.len(),
) {
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.end_byte()))
.clone(),
indicator: None,
anchor: doc.view_offset(view.id).anchor,
has_context_end: node_byte_range.is_some(),
view_id: view.id,
});
}
}
// result should be filled by now
if result.is_empty() {
if !cached_nodes.is_empty() {
if config.sticky_context.indicator {
return Some(add_indicator(doc, &context.viewport, view, cached_nodes));
}
return Some(cached_nodes);
}
return None;
}
let mut res = {
cached_nodes.append(&mut result);
cached_nodes
};
// Order of commands is important here
res.sort_unstable_by(|lhs, rhs| lhs.line.cmp(&rhs.line));
res.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(context.viewport.height / 3) as usize;
let skip = res.len().saturating_sub(max_nodes_amount);
res = res
.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::<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();
new_node.visual_line = i as u16;
new_node
})
.collect();
if config.sticky_context.indicator {
res = add_indicator(doc, &context.viewport, view, res);
}
Some(res)
}
fn build_cached_nodes(
doc: &Document,
nodes: Option<&Vec<StickyNode>>,
view: &View,
context: &mut StickyNodeContext,
) -> Option<Vec<StickyNode>> {
if let Some(nodes) = nodes {
if nodes.iter().any(|node| view.id != node.view_id) {
return None;
}
// nothing has changed, so the cached result can be returned
if nodes
.iter()
.any(|node| doc.view_offset(view.id).anchor == node.anchor)
{
return Some(nodes.iter().take(context.visual_row).cloned().collect());
}
// Nodes are elligible for reuse
// While the cached nodes are outside our search-range, pop them, too
let valid_nodes: Vec<StickyNode> = nodes
.iter()
.filter(|node| node.byte_range.contains(&context.topmost_byte))
.cloned()
.collect();
return Some(valid_nodes);
}
None
}
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))
})
})
}
/// Tests whether or not a given node is in a specific tree-sitter range
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()
}
/// Adds an indicator line to the Sticky Context
fn add_indicator(
doc: &Document,
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: doc.view_offset(view.id).anchor,
has_context_end: false,
view_id: view.id,
});
res
}
/// Render the sticky context
pub fn render_sticky_context(
doc: &Document,
view: &View,
surface: &mut Surface,
context: Option<&Vec<StickyNode>>,
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;
const DOTS: &str = "...";
for node in context {
surface.clear_with(context_area, context_style);
if let Some(indicator) = node.indicator.as_deref() {
// set the indicator
surface.set_stringn(
context_area.x,
context_area.y,
indicator,
indicator.len(),
indicator_style,
);
continue;
}
let node_start = text.byte_to_char(node.byte_range.start);
let first_node_line = text.line(text.char_to_line(node_start));
// subtract 1 to handle indexes
let mut first_node_line_end = first_node_line.len_chars().saturating_sub(1);
// trim trailing whitespace / newline
first_node_line_end -= first_node_line
.chars()
.reversed()
.position(|c| !c.is_whitespace())
.unwrap_or(0);
#[allow(deprecated)]
let Position {
row: _,
col: first_node_line_end,
} = visual_coords_at_pos(first_node_line, first_node_line_end, doc.tab_width());
// get the highlighting of the basic capture
let syntax_highlights = EditorView::doc_syntax_highlights(doc, node_start, 1, theme);
let overlay_highlights = EditorView::empty_highlight_iter(doc, node_start, 1);
let mut offset_area = context_area;
// Limit scope of borrowed surface
{
let mut renderer =
TextRenderer::new(surface, doc, theme, Position::new(0, 0), context_area);
// create the formatting for the basic node render
let mut formatting = doc.text_format(context_area.width, Some(theme));
formatting.soft_wrap = false;
render_text(
&mut renderer,
text,
node_start,
&formatting,
&TextAnnotations::default(),
syntax_highlights,
overlay_highlights,
theme,
DecorationManager::default(),
);
offset_area.x += first_node_line_end as u16;
}
if node.has_context_end {
let node_end = text.byte_to_char(node.byte_range.end);
let end_node_line = text.line(text.char_to_line(node_end));
let whitespace_offset = end_node_line
.chars()
.position(|c| !c.is_whitespace())
.unwrap_or(0);
#[allow(deprecated)]
let Position {
col: end_vis_offset,
row: _,
} = visual_coords_at_pos(end_node_line, whitespace_offset, doc.tab_width());
surface.set_stringn(
offset_area.x,
offset_area.y,
DOTS,
DOTS.len(),
theme.get("keyword.operator"),
);
offset_area.x += DOTS.len() as u16;
let mut renderer = TextRenderer::new(
surface,
doc,
theme,
Position::new(0, end_vis_offset),
offset_area,
);
let syntax_highlights = EditorView::doc_syntax_highlights(doc, node_end, 1, theme);
let overlay_highlights = EditorView::empty_highlight_iter(doc, node_end, 1);
let mut formatting = doc.text_format(offset_area.width, Some(theme));
formatting.soft_wrap = false;
render_text(
&mut renderer,
text,
node_end,
&formatting,
&TextAnnotations::default(),
syntax_highlights,
overlay_highlights,
theme,
DecorationManager::default(),
);
}
// next node
context_area.y += 1;
}
}

@ -34,6 +34,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span}; use tui::{buffer::Buffer as Surface, text::Span};
use super::context::{self, StickyNode};
pub struct EditorView { pub struct EditorView {
pub keymaps: Keymaps, pub keymaps: Keymaps,
on_next_key: Option<OnKeyCallback>, on_next_key: Option<OnKeyCallback>,
@ -41,6 +43,7 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
sticky_nodes: Option<Vec<StickyNode>>,
/// Tracks if the terminal window is focused by reaction to terminal focus events /// Tracks if the terminal window is focused by reaction to terminal focus events
terminal_focused: bool, terminal_focused: bool,
} }
@ -71,6 +74,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
sticky_nodes: None,
terminal_focused: true, terminal_focused: true,
} }
} }
@ -80,7 +84,7 @@ impl EditorView {
} }
pub fn render_view( pub fn render_view(
&self, &mut self,
editor: &Editor, editor: &Editor,
doc: &Document, doc: &Document,
view: &View, view: &View,
@ -165,29 +169,31 @@ impl EditorView {
} }
} }
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if is_focused {
decorations.add_decoration(text_decorations::Cursor {
cache: &editor.cursor_cache,
primary_cursor,
});
}
let gutter_overflow = view.gutter_offset(doc) == 0; let gutter_overflow = view.gutter_offset(doc) == 0;
if !gutter_overflow { if !gutter_overflow {
Self::render_gutter( Self::render_gutter(
editor, editor,
doc, doc,
self.sticky_nodes.clone(),
view, view,
view.area,
theme, theme,
is_focused & self.terminal_focused, is_focused & self.terminal_focused,
&mut decorations, &mut decorations,
); );
} }
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if is_focused {
decorations.add_decoration(text_decorations::Cursor {
cache: &editor.cursor_cache,
primary_cursor,
});
}
let width = view.inner_width(doc); let width = view.inner_width(doc);
let config = doc.config.load(); let config = doc.config.load();
let enable_cursor_line = view let enable_cursor_line = view
@ -201,6 +207,7 @@ impl EditorView {
inline_diagnostic_config, inline_diagnostic_config,
config.end_of_line_diagnostics, config.end_of_line_diagnostics,
)); ));
render_document( render_document(
surface, surface,
inner, inner,
@ -212,6 +219,19 @@ impl EditorView {
theme, theme,
decorations, decorations,
); );
if config.sticky_context.enable {
self.sticky_nodes = context::calculate_sticky_nodes(
self.sticky_nodes.as_ref(),
doc,
view,
&config,
editor.cursor_cache.get(view, doc).as_ref(),
);
context::render_sticky_context(doc, view, surface, self.sticky_nodes.as_ref(), theme);
}
Self::render_rulers(editor, doc, view, inner, surface, theme); Self::render_rulers(editor, doc, view, inner, surface, theme);
// if we're not at the edge of the screen, draw a right border // if we're not at the edge of the screen, draw a right border
@ -489,7 +509,6 @@ impl EditorView {
let base_primary_cursor_scope = theme let base_primary_cursor_scope = theme
.find_scope_index("ui.cursor.primary") .find_scope_index("ui.cursor.primary")
.unwrap_or(base_cursor_scope); .unwrap_or(base_cursor_scope);
let cursor_scope = match mode { let cursor_scope = match mode {
Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"), Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
Mode::Select => theme.find_scope_index_exact("ui.cursor.select"), Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
@ -647,8 +666,8 @@ impl EditorView {
pub fn render_gutter<'d>( pub fn render_gutter<'d>(
editor: &'d Editor, editor: &'d Editor,
doc: &'d Document, doc: &'d Document,
context: Option<Vec<StickyNode>>,
view: &View, view: &View,
viewport: Rect,
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
decoration_manager: &mut DecorationManager<'d>, decoration_manager: &mut DecorationManager<'d>,
@ -661,18 +680,24 @@ impl EditorView {
.collect(); .collect();
let mut offset = 0; let mut offset = 0;
let viewport = view.area;
let gutter_style = theme.get("ui.gutter"); let gutter_style = theme.get("ui.gutter");
let gutter_selected_style = theme.get("ui.gutter.selected"); let gutter_selected_style = theme.get("ui.gutter.selected");
let gutter_style_virtual = theme.get("ui.gutter.virtual"); let gutter_style_virtual = theme.get("ui.gutter.virtual");
let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual"); let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual");
let context_rc = Rc::new(context);
for gutter_type in view.gutters() { for gutter_type in view.gutters() {
let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused); let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc); let width = gutter_type.width(view, doc);
// avoid lots of small allocations by reusing a text buffer for each line // avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(width); let mut text_to_draw = String::with_capacity(width);
let cursors = cursors.clone(); let cursors = cursors.clone();
let context_instance = context_rc.clone();
let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| { let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
// TODO handle softwrap in gutters // TODO handle softwrap in gutters
let selected = cursors.contains(&pos.doc_line); let selected = cursors.contains(&pos.doc_line);
@ -686,10 +711,22 @@ impl EditorView {
(true, false) => gutter_selected_style_virtual, (true, false) => gutter_selected_style_virtual,
}; };
let mut doc_line = pos.doc_line;
if let Some(current_context) = context_instance
.as_ref()
.as_ref()
.and_then(|c| c.iter().find(|n| n.visual_line == pos.visual_line))
{
doc_line = match current_context.indicator {
Some(_) => return,
None => current_context.line,
};
}
if let Some(style) = if let Some(style) =
gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) gutter(doc_line, selected, pos.first_visual_line, &mut text_to_draw)
{ {
renderer.set_stringn(x, y, &text, width, gutter_style.patch(style)); renderer.set_stringn(x, y, &text_to_draw, width, gutter_style.patch(style));
} else { } else {
renderer.set_style( renderer.set_style(
Rect { Rect {
@ -701,7 +738,7 @@ impl EditorView {
gutter_style, gutter_style,
); );
} }
text.clear(); text_to_draw.clear();
}; };
decoration_manager.add_decoration(gutter_decoration); decoration_manager.add_decoration(gutter_decoration);

@ -1,4 +1,5 @@
mod completion; mod completion;
mod context;
mod document; mod document;
pub(crate) mod editor; pub(crate) mod editor;
mod info; mod info;

@ -325,6 +325,8 @@ pub struct Config {
pub soft_wrap: SoftWrap, pub soft_wrap: SoftWrap,
/// Workspace specific lsp ceiling dirs /// Workspace specific lsp ceiling dirs
pub workspace_lsp_roots: Vec<PathBuf>, pub workspace_lsp_roots: Vec<PathBuf>,
/// Contextual information on top of the viewport
pub sticky_context: StickyContextConfig,
/// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`. /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
pub default_line_ending: LineEndingConfig, pub default_line_ending: LineEndingConfig,
/// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`. /// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
@ -363,6 +365,41 @@ impl Default for SmartTabConfig {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct StickyContextConfig {
/// Display context of current top view if it is outside the view. Default to off
pub enable: bool,
/// Display an indicator whether to indicate if the sticky context is active
/// Eventually making this a string so that it is configurable.
/// Default to off
pub indicator: bool,
/// The max amount of lines to be displayed. (including indicator!)
/// The viewport is taken into account when changing this value.
/// So if the configured amount is more than the viewport height, it will be capped to a max
/// of the complete viewport height.
///
/// Default: 10, which means that it is a fixed size based on the viewport
pub max_lines: u8,
/// Whether or not the Sticky context shall also depend on the cursor position
/// Default to off
pub follow_cursor: bool,
}
impl Default for StickyContextConfig {
fn default() -> Self {
StickyContextConfig {
enable: false,
indicator: false,
max_lines: 10,
follow_cursor: false,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig { pub struct TerminalConfig {
@ -971,6 +1008,7 @@ impl Default for Config {
text_width: 80, text_width: 80,
completion_replace: false, completion_replace: false,
workspace_lsp_roots: Vec::new(), workspace_lsp_roots: Vec::new(),
sticky_context: StickyContextConfig::default(),
default_line_ending: LineEndingConfig::default(), default_line_ending: LineEndingConfig::default(),
insert_final_newline: true, insert_final_newline: true,
smart_tab: Some(SmartTabConfig::default()), smart_tab: Some(SmartTabConfig::default()),

@ -0,0 +1,43 @@
; Credits to nvim-treesitter-context
(interface_declaration) @context
(class_declaration) @context
(enum_declaration) @context
(struct_declaration) @context
(record_declaration) @context
(namespace_declaration) @context
(constructor_declaration
(parameter_list) @context.params
) @context
(destructor_declaration
(parameter_list) @context.params
) @context
(method_declaration
(parameter_list) @context.params
) @context
(switch_statement) @context
(for_statement) @context
(if_statement) @context
([
(do_statement)
(while_statement)
] @context)
(try_statement) @context
(catch_clause) @context
(finally_clause) @context

@ -0,0 +1,16 @@
; Credits to nvim-treesitter/nvim-treesitter-context
(function_definition
(_ parameters: (_) @context.params)
) @context
[
(preproc_if)
(preproc_ifdef)
(for_statement)
(if_statement)
(while_statement)
(do_statement)
(struct_specifier)
(enum_specifier)
] @context

@ -0,0 +1,4 @@
; Credits to nvim-treesitter-context
; inherits c
(class_specifier) @context

@ -0,0 +1,31 @@
(arrow_function
(formal_parameters) @context.params
) @context
(function_declaration
(formal_parameters) @context.params
) @context
(function
(formal_parameters) @context.params
) @context
(generator_function_declaration
(formal_parameters) @context.params
) @context
[
(call_expression)
(class_declaration)
(else_clause)
(for_statement)
; (interface_declaration) ; not usable in javascript
(lexical_declaration)
(method_definition)
(object)
(pair)
(while_statement)
(switch_statement)
(switch_case)
] @context

@ -0,0 +1,17 @@
; Credits to nvim-treesitter/nvim-treesitter-context
(binary_operator
left: (_)
right: (_) @context)
(pair
key: (_)
value: (_) @context)
((unary_operator
operand: (call
target: (identifier)
(arguments (_)))) @_op (#lua-match? @_op "@[%w_]+")) @context
(stab_clause) @context
(call) @context

@ -0,0 +1,21 @@
; Credits to nvim-treesitter/nvim-treesitter-context
(function_declaration
(parameter_list) @context.params
) @context
(method_declaration
(parameter_list) @context.params
) @context
(for_statement
(_)) @context
[
(const_declaration)
(for_statement)
(if_statement)
(import_declaration)
(type_declaration)
(var_declaration)
] @context

@ -0,0 +1 @@
(element) @context

@ -0,0 +1,5 @@
; Credits to nvim-treesitter/nvim-treesitter-context
[
(object)
(pair)
] @context

@ -0,0 +1,2 @@
; Credits to nvim-treesitter/nvim-treesitter-context
(section) @context

@ -0,0 +1,5 @@
[
(attrset_expression)
(binding)
(list_expression)
] @context

@ -0,0 +1,15 @@
(function_definition
_ parameters: (_) @context.params
) @context
(class_definition) @context
[
(if_statement)
(for_statement)
(while_statement)
(with_statement)
(try_statement)
(match_statement)
(case_clause)
] @context

@ -0,0 +1,21 @@
(function_item
(parameters) @context.params
) @context
(mod_item
body: (_)) @context
[
(if_expression)
(else_clause)
(match_expression)
(match_arm)
(for_expression)
(while_expression)
(loop_expression)
(closure_expression)
(impl_item)
(trait_item)
(struct_item)
(enum_item)
] @context

@ -0,0 +1,41 @@
(object_definition) @context
(class_definition
class_parameters: (_) @context.params
) @context
(trait_definition
class_parameters: (_) @context.params
) @context
(enum_definition
class_parameters: (_) @context.params
) @context
(given_definition
parameters: (_) @context.params
) @context
(extension_definition
parameters: (_) @context.params
) @context
(function_definition
parameters: (_) @context.params
) @context
(if_expression
alternative: (_) @context
) @context
[
(call_expression)
(case_clause)
(catch_clause)
(lambda_expression)
(match_expression)
(try_expression)
(while_expression)
] @context

@ -0,0 +1,8 @@
; Credits to nvim-treesitter/nvim-treesitter-context
[
(table)
(table_array_element)
(inline_table)
(array)
(pair)
] @context

@ -0,0 +1,3 @@
; inherits ecma
(interface_declaration) @context

@ -0,0 +1,3 @@
; inherits ecma
(interface_declaration) @context

@ -0,0 +1,6 @@
; Credits to nvim-treesitter/nvim-treesitter-context
[
(block_mapping)
(block_mapping_pair)
(block_sequence_item)
] @context

@ -0,0 +1,17 @@
(Decl
(FnProto (ParamDeclList) @context.params)
) @context
[
(ContainerDecl)
(ForStatement)
(IfStatement)
(InitList)
(LabeledStatement)
(LoopStatement)
(SwitchCase)
(SwitchExpr)
(SwitchProng)
(TestDecl)
(WhileStatement)
] @context

@ -85,7 +85,9 @@ hint = { fg = "hint" }
"ui.statusline.normal" = { bg = "blue", fg = "bg", modifiers = ["bold"] } "ui.statusline.normal" = { bg = "blue", fg = "bg", modifiers = ["bold"] }
"ui.statusline.insert" = { bg = "light-green", fg = "bg", modifiers = ["bold"] } "ui.statusline.insert" = { bg = "light-green", fg = "bg", modifiers = ["bold"] }
"ui.statusline.select" = { bg = "magenta", fg = "bg", modifiers = ["bold"] } "ui.statusline.select" = { bg = "magenta", fg = "bg", modifiers = ["bold"] }
"ui.text" = { bg = "bg", fg = "fg" } "ui.sticky.context" = { bg = "fg-gutter" }
"ui.sticky.indicator" = { fg = "comment", bg = "fg-gutter" }
"ui.text" = { fg = "fg" }
"ui.text.focus" = { bg = "bg-focus" } "ui.text.focus" = { bg = "bg-focus" }
"ui.text.inactive" = { fg = "comment", modifiers = ["italic"] } "ui.text.inactive" = { fg = "comment", modifiers = ["italic"] }
"ui.text.info" = { bg = "bg-menu", fg = "fg" } "ui.text.info" = { bg = "bg-menu", fg = "fg" }

@ -11,6 +11,7 @@ pub fn query_check() -> Result<(), DynError> {
"injections.scm", "injections.scm",
"textobjects.scm", "textobjects.scm",
"indents.scm", "indents.scm",
"context.scm",
]; ];
for language in lang_config().language { for language in lang_config().language {

Loading…
Cancel
Save