test rust lsp
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
use anyhow::{ bail, Context, Result };
|
||||
use lsp_types as lsp;
|
||||
use std::collections::HashMap;
|
||||
use tree_sitter::{ Parser, Tree };
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DocumentStore {
|
||||
docs: HashMap<lsp::Uri, Document>,
|
||||
}
|
||||
|
||||
impl DocumentStore {
|
||||
pub fn open(&mut self, item: lsp::TextDocumentItem) -> Result<()> {
|
||||
let uri = item.uri.clone();
|
||||
let doc = Document::open(item.text)?;
|
||||
self.docs.insert(uri, doc);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_full_change(
|
||||
&mut self,
|
||||
uri: &lsp::Uri,
|
||||
changes: &[lsp::TextDocumentContentChangeEvent]
|
||||
) -> Result<()> {
|
||||
let Some(doc) = self.docs.get_mut(uri) else {
|
||||
bail!("no such document: {:?}", uri);
|
||||
};
|
||||
// FULL sync: assume single change with whole text
|
||||
let new_text = changes.last().context("empty changes for full sync")?.text.clone();
|
||||
doc.reparse(&new_text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(&mut self, uri: &lsp::Uri) {
|
||||
self.docs.remove(uri);
|
||||
}
|
||||
|
||||
pub fn get(&self, uri: &lsp::Uri) -> Option<&Document> {
|
||||
self.docs.get(uri)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
text: String,
|
||||
parser: Parser,
|
||||
tree: Option<Tree>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn open(text: String) -> Result<Self> {
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&tree_sitter_tcl::LANGUAGE.into()).context("load tcl grammar")?;
|
||||
let tree = parser.parse(&text, None);
|
||||
Ok(Self { text, parser, tree })
|
||||
}
|
||||
|
||||
pub fn reparse(&mut self, new_text: &str) -> Result<()> {
|
||||
self.text.clear();
|
||||
self.text.push_str(new_text);
|
||||
self.tree = self.parser.parse(&self.text, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
self.text.clone()
|
||||
}
|
||||
|
||||
pub fn tree(&self) -> Option<&Tree> {
|
||||
self.tree.as_ref()
|
||||
}
|
||||
|
||||
/// Convert a byte range (UTF-8) into an LSP Range (UTF-16 columns).
|
||||
pub fn byte_range_to_lsp_range(&self, start: usize, end: usize) -> lsp::Range {
|
||||
fn byte_to_line_col_utf16(s: &str, byte: usize) -> (u32, u32) {
|
||||
let clamped = byte.min(s.len());
|
||||
let slice = &s[..clamped];
|
||||
let mut line = 0u32;
|
||||
let mut col_u16 = 0u32;
|
||||
let mut last_nl = 0usize;
|
||||
|
||||
for (i, b) in slice.bytes().enumerate() {
|
||||
if b == b'\n' {
|
||||
line += 1;
|
||||
last_nl = i + 1;
|
||||
}
|
||||
}
|
||||
// compute utf16 col from last newline to clamped
|
||||
let seg = &slice[last_nl..];
|
||||
col_u16 = seg.encode_utf16().count() as u32;
|
||||
(line, col_u16)
|
||||
}
|
||||
|
||||
let (sl, sc) = byte_to_line_col_utf16(&self.text, start);
|
||||
let (el, ec) = byte_to_line_col_utf16(&self.text, end);
|
||||
lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: sl,
|
||||
character: sc,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: el,
|
||||
character: ec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the UTF-8 byte offset of the "word" under a UTF-16 position (very naive).
|
||||
pub fn word_under_position(
|
||||
&self,
|
||||
pos: lsp::Position
|
||||
) -> Option<(String, std::ops::Range<usize>)> {
|
||||
// Convert UTF-16 (LSP) position -> UTF-8 byte offset in self.text
|
||||
fn line_col_utf16_to_byte(s: &str, line: u32, col_u16: u32) -> usize {
|
||||
// Find start byte of the requested line
|
||||
let mut cur_line: u32 = 0;
|
||||
let mut line_start_byte: usize = 0;
|
||||
|
||||
if line == 0 {
|
||||
line_start_byte = 0;
|
||||
} else {
|
||||
for (i, b) in s.bytes().enumerate() {
|
||||
if b == b'\n' {
|
||||
cur_line += 1;
|
||||
if cur_line == line {
|
||||
line_start_byte = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If line beyond EOF, clamp to end
|
||||
if cur_line < line {
|
||||
return s.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Advance by UTF-16 code units from line_start_byte
|
||||
let mut remaining = col_u16 as isize;
|
||||
let tail = &s[line_start_byte..];
|
||||
for (off, ch) in tail.char_indices() {
|
||||
if remaining <= 0 {
|
||||
return line_start_byte + off;
|
||||
}
|
||||
// subtract the number of UTF-16 code units this char occupies (1 for BMP, 2 for surrogates)
|
||||
let mut buf = [0u16; 2];
|
||||
remaining -= ch.encode_utf16(&mut buf).len() as isize;
|
||||
}
|
||||
// If column beyond EOL, clamp to end
|
||||
s.len()
|
||||
}
|
||||
|
||||
let bytes = self.text.as_bytes();
|
||||
let byte = line_col_utf16_to_byte(&self.text, pos.line, pos.character);
|
||||
if byte > self.text.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Expand to simple Tcl-ish identifier characters: letters/digits/_ : / . -
|
||||
fn is_ident(c: u8) -> bool {
|
||||
c.is_ascii_alphanumeric() || matches!(c, b'_' | b':' | b'/' | b'.' | b'-')
|
||||
}
|
||||
|
||||
// Walk backward to start
|
||||
let mut start = byte;
|
||||
while start > 0 && is_ident(bytes[start - 1]) {
|
||||
start -= 1;
|
||||
}
|
||||
// Walk forward to end
|
||||
let mut end = byte;
|
||||
while end < bytes.len() && is_ident(bytes[end]) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = self.text[start..end].to_string();
|
||||
Some((text, start..end)) // <-- no semicolon here, this is the tail expression
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user