commit 8c33cb5da1456ac32b8427bd7ceb47069a8a9ce2
parent c175db7a36940e9424b75d6ad6ee2e9f3fa3478f
Author: Sylvia Ivory <git@sivory.net>
Date: Wed, 2 Jul 2025 21:28:35 -0700
Allow autocompleting table entries
Diffstat:
| M | src/completion.rs | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
1 file changed, 124 insertions(+), 3 deletions(-)
diff --git a/src/completion.rs b/src/completion.rs
@@ -1,11 +1,12 @@
use std::sync::Arc;
use emmylua_parser::{
- LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaNameExpr, LuaParser, LuaSyntaxTree,
+ LuaAst, LuaAstNode, LuaAstToken, LuaBlock, LuaExpr, LuaIndexExpr, LuaNameExpr, LuaParser,
+ LuaSyntaxTree, LuaTokenKind,
};
use mlua::prelude::*;
use reedline::{Completer, Span, Suggestion};
-use rowan::TextRange;
+use rowan::{TextRange, TextSize};
use crate::{lua::LuaExecutor, parse};
@@ -203,6 +204,78 @@ impl LuaCompleter {
.collect()
}
+ fn table_index(&self, position: u32) -> Option<(TextRange, Vec<String>)> {
+ let chunk = self.tree.get_chunk_node();
+
+ for index in chunk.descendants::<LuaIndexExpr>() {
+ let (range, name, is_dot) = index
+ .get_index_key()
+ .map(|k| k.get_range().map(|r| (r, k.get_path_part(), false)))
+ .unwrap_or_else(|| {
+ index.token_by_kind(LuaTokenKind::TkDot).map(|t| {
+ let range = t.get_range();
+ (
+ TextRange::new(range.start(), range.start() + TextSize::new(1)),
+ String::new(),
+ true,
+ )
+ })
+ })?;
+
+ if position >= range.start().into() && position < range.end().into() {
+ let mut children: Vec<String> = Vec::new();
+
+ for parent_index in index.descendants::<LuaIndexExpr>() {
+ if let Some(token) = parent_index.get_name_token() {
+ children.push(token.get_name_text().to_string());
+ }
+
+ if let Some(LuaExpr::NameExpr(token)) = parent_index.get_prefix_expr() {
+ children.push(token.get_name_text()?);
+ }
+ }
+
+ if children.len() > 1 {
+ children.reverse();
+ children.pop();
+ }
+
+ let fields = if let Ok(globals) = self.lua_executor.globals() {
+ let mut var: LuaResult<LuaValue> = Ok(LuaValue::Table(globals));
+
+ for index in children.iter().rev() {
+ if let Ok(LuaValue::Table(tbl)) = var {
+ var = tbl.get(index.as_str())
+ }
+ }
+
+ if let Ok(LuaValue::Table(tbl)) = var {
+ tbl.pairs()
+ .flatten()
+ .map(|(k, _): (String, LuaValue)| k)
+ .filter(|s| s.starts_with(&name))
+ .collect::<Vec<_>>()
+ } else {
+ Vec::new()
+ }
+ } else {
+ Vec::new()
+ };
+
+ if is_dot {
+ return Some((
+ TextRange::new(range.start() + TextSize::new(1), range.end()),
+ fields,
+ ));
+ } else {
+ return Some((range, fields));
+ }
+ }
+ }
+
+ None
+ }
+
fn current_identifier(&self, position: u32) -> Option<(TextRange, String)> {
let chunk = self.tree.get_chunk_node();
@@ -225,7 +298,6 @@ impl LuaCompleter {
impl Completer for LuaCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let pos = pos as u32;
- // TODO; proper autocomplete
self.refresh_tree(line);
if let Some((range, current)) = self.current_identifier(pos.saturating_sub(1)) {
@@ -240,12 +312,25 @@ impl Completer for LuaCompleter {
.collect();
}
+ if let Some((range, fields)) = self.table_index(pos.saturating_sub(1)) {
+ return fields
+ .into_iter()
+ .map(|s| Suggestion {
+ value: s,
+ span: Span::new(range.start().into(), range.end().into()),
+ ..Default::default()
+ })
+ .collect();
+ }
+
Vec::new()
}
}
#[cfg(test)]
mod tests {
+ use std::collections::HashMap;
+
use crate::lua::MluaExecutor;
use super::*;
@@ -359,4 +444,40 @@ mod tests {
&completer.autocomplete_upvalue("foo", line_to_position(3, text))
);
}
+
+ #[test]
+ fn table_index_query() {
+ let lua = lua_executor();
+
+ let mut completer = LuaCompleter::new(lua);
+
+ completer.refresh_tree("print(table.ins");
+
+ assert_eq!(
+ &["insert"].map(|s| s.to_string()).as_slice(),
+ &completer.table_index(14).map(|t| t.1).unwrap()
+ );
+ }
+
+ #[test]
+ fn table_index_all() {
+ let lua = lua_executor();
+
+ lua.globals()
+ .unwrap()
+ .set("foo", HashMap::from([("bar", 1), ("baz", 2), ("ipsum", 3)]))
+ .unwrap();
+
+ let mut completer = LuaCompleter::new(lua);
+
+ completer.refresh_tree("print(foo.");
+
+ let mut fields = completer.table_index(9).map(|t| t.1).unwrap();
+ fields.sort();
+
+ assert_eq!(
+ &["bar", "baz", "ipsum"].map(|s| s.to_string()).as_slice(),
+ &fields
+ );
+ }
}