test rust lsp

This commit is contained in:
2025-09-07 07:18:18 +02:00
parent a8bf55882e
commit 7a3e3ae9f7
330 changed files with 1714 additions and 70094 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "text"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = { workspace = true }
lsp-types = { workspace = true }
tree-sitter = { workspace = true }
tree-sitter-tcl = { workspace = true }
url = { workspace = true }
+179
View File
@@ -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
}
}