manen

Fancy Lua REPL
Log | Files | Refs | README | LICENSE

commit a595dfc0108c294f12b851a19a1355bd06312a67
parent ddd2f6ff5cf2dc33405aa3c56e49b38ab9497310
Author: Sylvia Ivory <git@sivory.net>
Date:   Tue, 24 Jun 2025 15:37:42 -0700

Implement Completer trait on LuaCompleter

Diffstat:
Msrc/completion.rs | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/editor.rs | 25+++++++++++++++++++------
2 files changed, 114 insertions(+), 26 deletions(-)

diff --git a/src/completion.rs b/src/completion.rs @@ -1,4 +1,5 @@ use mlua::prelude::*; +use reedline::{Completer, Span, Suggestion}; use tree_sitter::{Parser, Point, Query, QueryCursor, Range, StreamingIterator, Tree}; #[derive(Debug)] @@ -19,6 +20,7 @@ pub struct LuaCompleter { tree: Tree, locals_query: Query, + identifiers_query: Query, scopes: Vec<Scope>, text: String, } @@ -39,11 +41,18 @@ impl LuaCompleter { ) .unwrap(); + let identifiers_query = Query::new( + &tree_sitter_lua::LANGUAGE.into(), + "(identifier) @identifier", + ) + .unwrap(); + Self { lua, parser, tree, locals_query, + identifiers_query, scopes: Vec::new(), text: String::new(), } @@ -74,8 +83,21 @@ impl LuaCompleter { ); let names = self.locals_query.capture_names(); - let mut scopes: Vec<Scope> = Vec::new(); - let mut scope_hierarchy: Vec<usize> = Vec::new(); + let lines = self.text.split("\n").collect::<Vec<_>>(); + + let mut scopes: Vec<Scope> = vec![ + // fallback scope + Scope { + range: Range { + start_byte: 0, + end_byte: self.text.len(), + start_point: Point::new(0, 0), + end_point: Point::new(lines.len(), lines.last().map_or("", |v| v).len()), + }, + variables: Vec::new(), + }, + ]; + let mut scope_hierarchy: Vec<usize> = vec![0]; matches.for_each(|m| { for capture in m.captures { @@ -129,13 +151,13 @@ impl LuaCompleter { scopes } - fn locals(&self, point: Point) -> Vec<String> { + fn locals(&self, position: usize) -> Vec<String> { let mut variables = Vec::new(); for scope in self.scopes.iter() { - if point > scope.range.start_point && point < scope.range.end_point { + if position > scope.range.start_byte && position < scope.range.end_byte { for var in scope.variables.iter() { - if point > var.range.end_point { + if position > var.range.end_byte { variables.push(var.name.clone()); } } @@ -189,8 +211,8 @@ impl LuaCompleter { // to summarize, this function is not properly named // // globals either exist or are an extension of _ENV - fn autocomplete_upvalue(&self, query: &str, point: Point) -> Vec<String> { - let mut upvalues = self.locals(point); + fn autocomplete_upvalue(&self, query: &str, position: usize) -> Vec<String> { + let mut upvalues = self.locals(position); upvalues.extend(self.globals()); upvalues.sort(); @@ -199,18 +221,70 @@ impl LuaCompleter { .filter(|s| s.starts_with(query)) .collect() } + + fn current_identifier(&self, position: usize) -> Option<(Range, String)> { + let mut cursor = QueryCursor::new(); + + let mut matches = cursor.matches( + &self.identifiers_query, + self.tree.root_node(), + self.text.as_bytes(), + ); + + while let Some(m) = matches.next() { + for capture in m.captures { + let text = capture + .node + .utf8_text(self.text.as_bytes()) + .unwrap() + .to_string(); + let range = capture.node.range(); + + if position > range.start_byte && position < range.end_byte { + return Some((range, text.to_string())); + } + } + } + + None + } +} + +impl Completer for LuaCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> { + // TODO; proper autocomplete + self.refresh_tree(line); + + if let Some((range, current)) = self.current_identifier(pos.saturating_sub(1)) { + return self + .autocomplete_upvalue(&current, pos) + .into_iter() + .map(|s| Suggestion { + value: s, + span: Span::new(range.start_byte, range.end_byte), + ..Default::default() + }) + .collect(); + } + + Vec::new() + } } #[cfg(test)] mod tests { use super::*; + fn line_to_position(line: usize, text: &str) -> usize { + let split = text.split("\n").collect::<Vec<_>>(); + split[0..line].join("\n").len() + } + #[test] fn locals() { let mut completer = LuaCompleter::new(Lua::new()); - completer.refresh_tree( - r#" + let text = r#" local function foo(a, b) -- 2: foo, a, b print(a, b) @@ -224,27 +298,28 @@ mod tests { end -- 13: foo, bar - "#, - ); + "#; + + completer.refresh_tree(text); assert_eq!( &["foo", "a", "b"].as_slice(), - &completer.locals(Point { row: 2, column: 0 }), + &completer.locals(line_to_position(2, text)), ); assert_eq!( &["foo"].as_slice(), - &completer.locals(Point { row: 6, column: 0 }), + &completer.locals(line_to_position(6, text)), ); assert_eq!( &["foo", "bar", "c"].as_slice(), - &completer.locals(Point { row: 9, column: 0 }), + &completer.locals(line_to_position(9, text)), ); assert_eq!( &["foo", "bar"].as_slice(), - &completer.locals(Point { row: 13, column: 0 }), + &completer.locals(line_to_position(13, text)), ); } @@ -255,20 +330,20 @@ mod tests { let mut completer = LuaCompleter::new(lua); - completer.refresh_tree( - r#" + let text = r#" local function foo(a, fooing) local foobaz = 3 -- 3: foo, foobar, fooing, foobaz end - "#, - ); + "#; + + completer.refresh_tree(text); assert_eq!( &["foo", "foobar", "foobaz", "fooing"] .map(|s| s.to_string()) .as_slice(), - &completer.autocomplete_upvalue("foo", Point { row: 3, column: 0 }) + &completer.autocomplete_upvalue("foo", line_to_position(3, text)) ); } } diff --git a/src/editor.rs b/src/editor.rs @@ -9,13 +9,14 @@ use std::{ use directories::ProjectDirs; use mlua::prelude::*; use reedline::{ - DefaultPrompt, DefaultPromptSegment, EditCommand, Emacs, FileBackedHistory, KeyCode, - KeyModifiers, Reedline, ReedlineEvent, Signal, default_emacs_keybindings, + DefaultPrompt, DefaultPromptSegment, EditCommand, Emacs, FileBackedHistory, IdeMenu, KeyCode, + KeyModifiers, MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu, Signal, + default_emacs_keybindings, }; use crate::{ - format::TableFormat, highlight::LuaHighlighter, hinter::LuaHinter, inspect::display_basic, - validator::LuaValidator, + completion::LuaCompleter, format::TableFormat, highlight::LuaHighlighter, hinter::LuaHinter, + inspect::display_basic, validator::LuaValidator, }; pub struct Editor { @@ -52,16 +53,28 @@ impl Editor { let mut keybindings = default_emacs_keybindings(); keybindings.add_binding( - KeyModifiers::SHIFT, + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu(String::from("completion_menu")), + ReedlineEvent::MenuNext, + ]), + ); + keybindings.add_binding( + KeyModifiers::ALT, KeyCode::Enter, ReedlineEvent::Edit(vec![EditCommand::InsertNewline]), ); + let ide_menu = IdeMenu::default().with_name("completion_menu"); + let mut editor = Reedline::create() .with_highlighter(Box::new(LuaHighlighter::new())) .with_validator(Box::new(LuaValidator::new())) + .with_completer(Box::new(LuaCompleter::new(lua.clone()))) .with_hinter(Box::new(LuaHinter)) - .with_edit_mode(Box::new(Emacs::new(keybindings))); + .with_edit_mode(Box::new(Emacs::new(keybindings))) + .with_menu(ReedlineMenu::EngineCompleter(Box::new(ide_menu))); if let Some(proj_dirs) = ProjectDirs::from("net.sivory", "", "Manen") { let history = FileBackedHistory::with_file(256, proj_dirs.data_dir().join("history"));