Skip to content

Commit f9d69c3

Browse files
committed
Add Lua bindings for SpanCalculator
This one is fun, because `SpanCalculator` holds a reference to the file's source code, while the `mlua::UserData` works best for Rust types that are 'static. To get around this, we make sure to only ever create `SpanCalculator` wrappers for source data that is owned by the Lua interpreter, and add that source data as a user value of the Lua wrapper that we create. That should cause Lua's garbage collector to ensure that the source code outlives the `SpanCalculator`, making it safe for us to transmute the source reference to a 'static lifetime.
1 parent 82d0a0a commit f9d69c3

File tree

5 files changed

+172
-2
lines changed

5 files changed

+172
-2
lines changed

lsp-positions/Cargo.toml

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ test = false
1818

1919
[features]
2020
bincode = ["dep:bincode"]
21-
lua = ["dep:mlua"]
21+
lua = ["tree-sitter", "dep:mlua", "dep:mlua-tree-sitter"]
2222
tree-sitter = ["dep:tree-sitter"]
2323

2424
[dependencies]
2525
memchr = "2.4"
2626
mlua = { version = "0.9", optional = true }
27+
mlua-tree-sitter = { version = "0.1", git="https://github.com/dcreager/mlua-tree-sitter", optional = true }
2728
tree-sitter = { version=">= 0.19", optional=true }
2829
unicode-segmentation = { version="1.8" }
2930
serde = { version="1", optional=true, features=["derive"] }
3031
bincode = { version="2.0.0-rc.3", optional=true }
32+
33+
[dev-dependencies]
34+
anyhow = { version = "1.0" }
35+
lua-helpers = { path = "../lua-helpers" }
36+
tree-sitter-python = { version = "0.19.1" }

lsp-positions/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use memchr::memchr;
3434
use unicode_segmentation::UnicodeSegmentation as _;
3535

3636
#[cfg(feature = "lua")]
37-
mod lua;
37+
pub mod lua;
3838

3939
fn grapheme_len(string: &str) -> usize {
4040
string.graphemes(true).count()

lsp-positions/src/lua.rs

+73
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,63 @@ use mlua::Error;
1111
use mlua::FromLua;
1212
use mlua::IntoLua;
1313
use mlua::Lua;
14+
use mlua::UserData;
15+
use mlua::UserDataMethods;
1416
use mlua::Value;
17+
use mlua_tree_sitter::TSNode;
18+
use mlua_tree_sitter::TreeWithSource;
1519

1620
use crate::Offset;
1721
use crate::Position;
1822
use crate::Span;
23+
use crate::SpanCalculator;
24+
25+
/// An extension trait that lets you load the `lsp_positions` module into a Lua environment.
26+
pub trait Module {
27+
/// Loads the `lsp_positions` module into a Lua environment.
28+
fn open_lsp_positions(&self) -> Result<(), mlua::Error>;
29+
}
30+
31+
impl Module for Lua {
32+
fn open_lsp_positions(&self) -> Result<(), mlua::Error> {
33+
let exports = self.create_table()?;
34+
let sc_type = self.create_table()?;
35+
36+
let function = self.create_function(|lua, source_value: mlua::String| {
37+
// We are going to add the Lua string as a user value of the SpanCalculator's Lua
38+
// wrapper. That will ensure that the string is not garbage collected before the
39+
// SpanCalculator, which makes it safe to transmute into a 'static reference.
40+
let source = source_value.to_str()?;
41+
let source: &'static str = unsafe { std::mem::transmute(source) };
42+
let sc = SpanCalculator::new(source);
43+
let sc = lua.create_userdata(sc)?;
44+
sc.set_user_value(source_value)?;
45+
Ok(sc)
46+
})?;
47+
sc_type.set("new", function)?;
48+
49+
#[cfg(feature = "tree-sitter")]
50+
{
51+
let function = self.create_function(|lua, tws_value: Value| {
52+
// We are going to add the tree-sitter treee as a user value of the
53+
// SpanCalculator's Lua wrapper. That will ensure that the tree is not garbage
54+
// collected before the SpanCalculator, which makes it safe to transmute into a
55+
// 'static reference.
56+
let tws = TreeWithSource::from_lua(tws_value.clone(), lua)?;
57+
let source: &'static str = unsafe { std::mem::transmute(tws.src) };
58+
let sc = SpanCalculator::new(source);
59+
let sc = lua.create_userdata(sc)?;
60+
sc.set_user_value(tws_value)?;
61+
Ok(sc)
62+
})?;
63+
sc_type.set("new_from_tree", function)?;
64+
}
65+
66+
exports.set("SpanCalculator", sc_type)?;
67+
self.globals().set("lsp_positions", exports)?;
68+
Ok(())
69+
}
70+
}
1971

2072
impl<'lua> FromLua<'lua> for Offset {
2173
fn from_lua(value: Value<'lua>, _: &'lua Lua) -> Result<Self, Error> {
@@ -142,3 +194,24 @@ impl<'lua> IntoLua<'lua> for Span {
142194
Ok(Value::Table(result))
143195
}
144196
}
197+
198+
impl UserData for SpanCalculator<'static> {
199+
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
200+
methods.add_method_mut(
201+
"for_line_and_column",
202+
|_, sc, (line, line_utf8_offset, column_utf8_offset)| {
203+
Ok(sc.for_line_and_column(line, line_utf8_offset, column_utf8_offset))
204+
},
205+
);
206+
207+
methods.add_method_mut(
208+
"for_line_and_grapheme",
209+
|_, sc, (line, line_utf8_offset, column_grapheme_offset)| {
210+
Ok(sc.for_line_and_grapheme(line, line_utf8_offset, column_grapheme_offset))
211+
},
212+
);
213+
214+
#[cfg(feature = "tree-sitter")]
215+
methods.add_method_mut("for_node", |_, sc, node: TSNode| Ok(sc.for_node(&node)));
216+
}
217+
}

lsp-positions/tests/it/lua.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// -*- coding: utf-8 -*-
2+
// ------------------------------------------------------------------------------------------------
3+
// Copyright © 2023, stack-graphs authors.
4+
// Licensed under either of Apache License, Version 2.0, or MIT license, at your option.
5+
// Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details.
6+
// ------------------------------------------------------------------------------------------------
7+
8+
use lsp_positions::lua::Module;
9+
use lua_helpers::new_lua;
10+
use lua_helpers::CheckLua;
11+
12+
#[test]
13+
fn can_calculate_positions_from_lua() -> Result<(), mlua::Error> {
14+
let l = new_lua()?;
15+
l.open_lsp_positions()?;
16+
l.check(
17+
r#"
18+
local source = " from a import * "
19+
local sc = lsp_positions.SpanCalculator.new(source)
20+
local position = sc:for_line_and_column(0, 0, 9)
21+
local expected = {
22+
line=0,
23+
column={
24+
utf8_offset=9,
25+
utf16_offset=9,
26+
grapheme_offset=9,
27+
},
28+
containing_line={start=0, ["end"]=21},
29+
trimmed_line={start=3, ["end"]=18},
30+
}
31+
assert_deepeq("position", expected, position)
32+
"#,
33+
)?;
34+
Ok(())
35+
}
36+
37+
#[cfg(feature = "tree-sitter")]
38+
#[test]
39+
fn can_calculate_tree_sitter_spans_from_lua() -> Result<(), anyhow::Error> {
40+
let code = br#"
41+
def double(x):
42+
return x * 2
43+
"#;
44+
let mut parser = tree_sitter::Parser::new();
45+
parser.set_language(tree_sitter_python::language()).unwrap();
46+
let parsed = parser.parse(code, None).unwrap();
47+
48+
use mlua_tree_sitter::Module;
49+
use mlua_tree_sitter::WithSource;
50+
let l = new_lua()?;
51+
l.open_lsp_positions()?;
52+
l.open_ltreesitter()?;
53+
l.globals().set("parsed", parsed.with_source(code))?;
54+
55+
l.check(
56+
r#"
57+
local module = parsed:root()
58+
local double = module:child(0)
59+
local name = double:child_by_field_name("name")
60+
local sc = lsp_positions.SpanCalculator.new_from_tree(parsed)
61+
local position = sc:for_node(name)
62+
local expected = {
63+
start={
64+
line=1,
65+
column={
66+
utf8_offset=10,
67+
utf16_offset=10,
68+
grapheme_offset=10,
69+
},
70+
containing_line={start=1, ["end"]=21},
71+
trimmed_line={start=7, ["end"]=21},
72+
},
73+
["end"]={
74+
line=1,
75+
column={
76+
utf8_offset=16,
77+
utf16_offset=16,
78+
grapheme_offset=16,
79+
},
80+
containing_line={start=1, ["end"]=21},
81+
trimmed_line={start=7, ["end"]=21},
82+
},
83+
}
84+
assert_deepeq("position", expected, position)
85+
"#,
86+
)?;
87+
Ok(())
88+
}

lsp-positions/tests/it/main.rs

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use unicode_segmentation::UnicodeSegmentation as _;
99

1010
use lsp_positions::Offset;
1111

12+
#[cfg(feature = "lua")]
13+
mod lua;
14+
1215
fn check_offsets(line: &str) {
1316
let offsets = Offset::all_chars(line).collect::<Vec<_>>();
1417
assert!(!offsets.is_empty());

0 commit comments

Comments
 (0)