This commit is contained in:
Josh Thomas 2025-08-22 14:40:32 -05:00
parent 943ab19790
commit 79a0c4cb4f
2 changed files with 37 additions and 168 deletions

View file

@ -1,136 +1,29 @@
use salsa::Database;
use tower_lsp_server::lsp_types::DidOpenTextDocumentParams; use tower_lsp_server::lsp_types::DidOpenTextDocumentParams;
use tower_lsp_server::lsp_types::Position; use tower_lsp_server::lsp_types::Position;
use tower_lsp_server::lsp_types::Range;
use tower_lsp_server::lsp_types::TextDocumentContentChangeEvent;
#[salsa::input(debug)] #[derive(Debug, Clone)]
pub struct TextDocument { pub struct TextDocument {
#[returns(ref)] #[allow(dead_code)]
pub uri: String, pub uri: String,
#[returns(ref)]
pub contents: String,
#[returns(ref)]
pub index: LineIndex,
pub version: i32, pub version: i32,
pub language_id: LanguageId, pub language_id: LanguageId,
} }
impl TextDocument { impl TextDocument {
pub fn from_did_open_params(db: &dyn Database, params: &DidOpenTextDocumentParams) -> Self { pub fn new(uri: String, version: i32, language_id: LanguageId) -> Self {
Self {
uri,
version,
language_id,
}
}
pub fn from_did_open_params(params: &DidOpenTextDocumentParams) -> Self {
let uri = params.text_document.uri.to_string(); let uri = params.text_document.uri.to_string();
let contents = params.text_document.text.clone();
let version = params.text_document.version; let version = params.text_document.version;
let language_id = LanguageId::from(params.text_document.language_id.as_str()); let language_id = LanguageId::from(params.text_document.language_id.as_str());
let index = LineIndex::new(&contents); TextDocument::new(uri, version, language_id)
TextDocument::new(db, uri, contents, index, version, language_id)
}
pub fn with_changes(
self,
db: &dyn Database,
changes: &[TextDocumentContentChangeEvent],
new_version: i32,
) -> Self {
let mut new_contents = self.contents(db).to_string();
for change in changes {
if let Some(range) = change.range {
let index = LineIndex::new(&new_contents);
if let (Some(start_offset), Some(end_offset)) = (
index.offset(range.start).map(|o| o as usize),
index.offset(range.end).map(|o| o as usize),
) {
let mut updated_content = String::with_capacity(
new_contents.len() - (end_offset - start_offset) + change.text.len(),
);
updated_content.push_str(&new_contents[..start_offset]);
updated_content.push_str(&change.text);
updated_content.push_str(&new_contents[end_offset..]);
new_contents = updated_content;
}
} else {
// Full document update
new_contents.clone_from(&change.text);
}
}
let index = LineIndex::new(&new_contents);
TextDocument::new(
db,
self.uri(db).to_string(),
new_contents,
index,
new_version,
self.language_id(db),
)
}
#[allow(dead_code)]
pub fn get_text(self, db: &dyn Database) -> String {
self.contents(db).to_string()
}
#[allow(dead_code)]
pub fn get_text_range(self, db: &dyn Database, range: Range) -> Option<String> {
let index = self.index(db);
let start = index.offset(range.start)? as usize;
let end = index.offset(range.end)? as usize;
let contents = self.contents(db);
Some(contents[start..end].to_string())
}
pub fn get_line(self, db: &dyn Database, line: u32) -> Option<String> {
let index = self.index(db);
let start = index.line_starts.get(line as usize)?;
let end = index
.line_starts
.get(line as usize + 1)
.copied()
.unwrap_or(index.length);
let contents = self.contents(db);
Some(contents[*start as usize..end as usize].to_string())
}
#[allow(dead_code)]
pub fn line_count(self, db: &dyn Database) -> usize {
self.index(db).line_starts.len()
}
pub fn get_template_tag_context(
self,
db: &dyn Database,
position: Position,
) -> Option<TemplateTagContext> {
let line = self.get_line(db, position.line)?;
let char_pos: usize = position.character.try_into().ok()?;
let prefix = &line[..char_pos];
let rest_of_line = &line[char_pos..];
let rest_trimmed = rest_of_line.trim_start();
prefix.rfind("{%").map(|tag_start| {
// Check if we're immediately after {% with no space
let needs_leading_space = prefix.ends_with("{%");
let closing_brace = if rest_trimmed.starts_with("%}") {
ClosingBrace::FullClose
} else if rest_trimmed.starts_with('}') {
ClosingBrace::PartialClose
} else {
ClosingBrace::None
};
TemplateTagContext {
partial_tag: prefix[tag_start + 2..].trim().to_string(),
closing_brace,
needs_leading_space,
}
})
} }
} }
@ -224,3 +117,4 @@ pub struct TemplateTagContext {
pub closing_brace: ClosingBrace, pub closing_brace: ClosingBrace,
pub needs_leading_space: bool, pub needs_leading_space: bool,
} }

View file

@ -59,7 +59,7 @@ impl Store {
false false
} }
} }
pub fn handle_did_open(&mut self, db: &dyn Database, params: &DidOpenTextDocumentParams) { pub fn handle_did_open(&mut self, _db: &dyn Database, params: &DidOpenTextDocumentParams) {
// Only process files within the workspace // Only process files within the workspace
if !self.is_workspace_file(&params.text_document.uri) { if !self.is_workspace_file(&params.text_document.uri) {
// Silently ignore files outside workspace // Silently ignore files outside workspace
@ -85,7 +85,7 @@ impl Store {
} }
} }
let document = TextDocument::from_did_open_params(db, params); let document = TextDocument::from_did_open_params(params);
self.add_document(document, uri.clone()); self.add_document(document, uri.clone());
self.versions.insert(uri, version); self.versions.insert(uri, version);
@ -93,7 +93,7 @@ impl Store {
pub fn handle_did_change( pub fn handle_did_change(
&mut self, &mut self,
db: &dyn Database, _db: &dyn Database,
params: &DidChangeTextDocumentParams, params: &DidChangeTextDocumentParams,
) -> Result<()> { ) -> Result<()> {
// Only process files within the workspace // Only process files within the workspace
@ -110,21 +110,9 @@ impl Store {
if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) { if let Ok(relative_path) = absolute_path.strip_prefix(&self.root_path) {
let relative_path_str = relative_path.to_string_lossy(); let relative_path_str = relative_path.to_string_lossy();
// Read current content from VFS // Read current content from VFS (single source of truth)
let current_content = match self.vfs.read_to_string(&relative_path_str) { let current_content = self.vfs.read_to_string(&relative_path_str)
Ok(content) => content, .map_err(|e| anyhow!("Failed to read from VFS: {}", e))?;
Err(e) => {
eprintln!("Warning: Failed to read from VFS, falling back to TextDocument: {}", e);
// Fallback to existing TextDocument approach
let document = self
.get_document(&uri)
.ok_or_else(|| anyhow!("Document not found: {}", uri))?;
let new_document = document.with_changes(db, &params.content_changes, version);
self.documents.insert(uri.clone(), new_document);
self.versions.insert(uri, version);
return Ok(());
}
};
// Apply text changes to VFS content // Apply text changes to VFS content
let updated_content = self.apply_changes_to_content(current_content, &params.content_changes)?; let updated_content = self.apply_changes_to_content(current_content, &params.content_changes)?;
@ -134,31 +122,18 @@ impl Store {
eprintln!("Warning: Failed to write to VFS: {}", e); eprintln!("Warning: Failed to write to VFS: {}", e);
} }
// Create new TextDocument with updated content for backward compatibility // Update document metadata (just version)
let index = LineIndex::new(&updated_content); if let Some(document) = self.documents.get_mut(&uri) {
let language_id = self.get_document(&uri) document.version = version;
.map(|doc| doc.language_id(db)) }
.unwrap_or(LanguageId::HtmlDjango);
let new_document = TextDocument::new(db, uri.clone(), updated_content.clone(), index, version, language_id);
self.documents.insert(uri.clone(), new_document);
self.versions.insert(uri, version); self.versions.insert(uri, version);
return Ok(()); return Ok(());
} }
} }
// Fallback to original implementation if path conversion fails // If path conversion fails, this is an error since we need VFS
let document = self Err(anyhow!("Document not in workspace or path conversion failed: {}", uri))
.get_document(&uri)
.ok_or_else(|| anyhow!("Document not found: {}", uri))?;
let new_document = document.with_changes(db, &params.content_changes, version);
self.documents.insert(uri.clone(), new_document);
self.versions.insert(uri, version);
Ok(())
} }
pub fn handle_did_close(&mut self, params: &DidCloseTextDocumentParams) { pub fn handle_did_close(&mut self, params: &DidCloseTextDocumentParams) {
@ -266,12 +241,12 @@ impl Store {
#[allow(dead_code)] #[allow(dead_code)]
pub fn get_documents_by_language<'db>( pub fn get_documents_by_language<'db>(
&'db self, &'db self,
db: &'db dyn Database, _db: &'db dyn Database,
language_id: LanguageId, language_id: LanguageId,
) -> impl Iterator<Item = &'db TextDocument> + 'db { ) -> impl Iterator<Item = &'db TextDocument> + 'db {
self.documents self.documents
.values() .values()
.filter(move |doc| doc.language_id(db) == language_id) .filter(move |doc| doc.language_id == language_id)
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -286,14 +261,14 @@ impl Store {
pub fn get_completions( pub fn get_completions(
&self, &self,
db: &dyn Database, _db: &dyn Database,
uri: &str, uri: &str,
position: Position, position: Position,
tags: &TemplateTags, tags: &TemplateTags,
) -> Option<CompletionResponse> { ) -> Option<CompletionResponse> {
let document = self.get_document(uri)?; let document = self.get_document(uri)?;
if document.language_id(db) != LanguageId::HtmlDjango { if document.language_id != LanguageId::HtmlDjango {
return None; return None;
} }
@ -307,21 +282,21 @@ impl Store {
match self.vfs.read_to_string(&relative_path_str) { match self.vfs.read_to_string(&relative_path_str) {
Ok(vfs_content) => vfs_content, Ok(vfs_content) => vfs_content,
Err(_) => { Err(_) => {
// Fallback to document content if VFS read fails // Return None if we can't read from VFS
document.contents(db).to_string() return None;
} }
} }
} else { } else {
// Path not within workspace, use document content // Path not within workspace
document.contents(db).to_string() return None;
} }
} else { } else {
// URI parsing failed, use document content // URI parsing failed
document.contents(db).to_string() return None;
} }
} else { } else {
// URI parsing failed, use document content // URI parsing failed
document.contents(db).to_string() return None;
}; };
// Use standalone analyzer instead of salsa-tracked method // Use standalone analyzer instead of salsa-tracked method