use crate::annotation_discovery::AnnotationDiscovery;
use crate::call_hierarchy::{
incoming_calls as ch_incoming, outgoing_calls as ch_outgoing,
prepare_call_hierarchy as ch_prepare,
};
use crate::code_actions::get_code_actions;
use crate::code_lens::{get_code_lenses, resolve_code_lens};
use crate::completion::get_completions_with_context;
use crate::definition::{
get_declaration, get_definition, get_document_highlights, get_implementations,
get_references_cross_file, get_type_definition,
};
use crate::diagnostics::error_to_diagnostic;
use crate::document::DocumentManager;
use crate::document_symbols::{get_document_symbols, get_workspace_symbols};
use crate::folding::get_folding_ranges;
use crate::formatting::{format_document, format_on_type, format_range};
use crate::hover::get_hover;
use crate::inlay_hints::{InlayHintConfig, get_inlay_hints_with_context};
use crate::rename::{prepare_rename, rename_cross_file};
use crate::semantic_tokens::{get_legend, get_semantic_tokens};
use crate::signature_help::get_signature_help;
use crate::util::{
mask_leading_prefix_for_parse, offset_to_line_col, parser_source, position_to_offset,
};
use dashmap::DashMap;
use shape_ast::ParseErrorKind;
use shape_ast::ast::Program;
use shape_ast::parser::parse_program;
use std::collections::HashSet;
use tower_lsp_server::ls_types::request::{
GotoDeclarationParams, GotoDeclarationResponse, GotoImplementationParams,
GotoImplementationResponse, GotoTypeDefinitionParams, GotoTypeDefinitionResponse,
};
use tower_lsp_server::ls_types::{
CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CallHierarchyServerCapability, CodeActionKind, CodeActionOptions, CodeActionParams,
CodeActionProviderCapability, CodeActionResponse, CodeLens, CodeLensOptions, CodeLensParams,
CompletionItem, CompletionOptions, CompletionParams, CompletionResponse, DeclarationCapability,
Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities, DiagnosticSeverity,
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentDiagnosticParams,
DocumentDiagnosticReport, DocumentDiagnosticReportResult, DocumentFormattingParams,
DocumentHighlight, DocumentHighlightParams, DocumentLink, DocumentLinkOptions,
DocumentLinkParams, DocumentOnTypeFormattingOptions, DocumentOnTypeFormattingParams,
DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
ExecuteCommandOptions, ExecuteCommandParams, FileChangeType, FileOperationFilter,
FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions,
FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability, FullDocumentDiagnosticReport,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, HoverProviderCapability,
ImplementationProviderCapability, InitializeParams, InitializeResult, InitializedParams,
InlayHint, InlayHintOptions, InlayHintParams, InlayHintServerCapabilities, LSPAny, Location,
MessageType, OneOf, Position, PrepareRenameResponse, Range, ReferenceParams,
RelatedFullDocumentDiagnosticReport, RenameFilesParams, RenameOptions, RenameParams,
SemanticToken, SemanticTokens, SemanticTokensDelta, SemanticTokensDeltaParams,
SemanticTokensEdit, SemanticTokensFullDeltaResult, SemanticTokensFullOptions,
SemanticTokensOptions, SemanticTokensParams, SemanticTokensRangeParams,
SemanticTokensRangeResult, SemanticTokensResult, SemanticTokensServerCapabilities,
ServerCapabilities, ServerInfo, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit,
TypeDefinitionProviderCapability, Uri, WorkDoneProgressOptions, WorkspaceDiagnosticParams,
WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
WorkspaceFileOperationsServerCapabilities, WorkspaceEdit, WorkspaceFullDocumentDiagnosticReport,
WorkspaceServerCapabilities, WorkspaceSymbolParams, WorkspaceSymbolResponse,
};
use tower_lsp_server::{Client, LanguageServer, jsonrpc::Result};
pub struct ShapeLanguageServer {
client: Client,
documents: DocumentManager,
project_root: std::sync::OnceLock<std::path::PathBuf>,
last_good_programs: DashMap<Uri, Program>,
foreign_lsp: crate::foreign_lsp::ForeignLspManager,
semantic_tokens_cache: DashMap<Uri, CachedSemanticTokens>,
semantic_tokens_result_counter: std::sync::atomic::AtomicU64,
inlay_hint_config: std::sync::RwLock<InlayHintConfig>,
}
#[derive(Clone)]
struct CachedSemanticTokens {
result_id: String,
data: Vec<SemanticToken>,
}
impl ShapeLanguageServer {
pub fn new(client: Client) -> Self {
let default_workspace =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
Self {
client,
documents: DocumentManager::new(),
project_root: std::sync::OnceLock::new(),
last_good_programs: DashMap::new(),
foreign_lsp: crate::foreign_lsp::ForeignLspManager::new(default_workspace),
semantic_tokens_cache: DashMap::new(),
semantic_tokens_result_counter: std::sync::atomic::AtomicU64::new(0),
inlay_hint_config: std::sync::RwLock::new(InlayHintConfig::default()),
}
}
fn next_semantic_tokens_result_id(&self) -> String {
let n = self
.semantic_tokens_result_counter
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("st-{}", n)
}
async fn collect_full_semantic_tokens(&self, uri: &Uri) -> Option<SemanticTokens> {
let doc = self.documents.get(uri)?;
let text = doc.text();
let mut tokens = get_semantic_tokens(&text)?;
let mut absolute = decode_semantic_tokens(&tokens.data);
let frontmatter_tokens =
crate::toml_support::semantic_tokens::collect_frontmatter_semantic_tokens(&text);
absolute.extend(
frontmatter_tokens
.into_iter()
.map(|token| AbsoluteSemanticToken {
line: token.line,
start_char: token.start_char,
length: token.length,
token_type: token.token_type,
modifiers: token.modifiers,
}),
);
if self.last_good_programs.contains_key(uri) {
let foreign_tokens = self.foreign_lsp.collect_semantic_tokens(uri.as_str()).await;
absolute.extend(
foreign_tokens
.into_iter()
.map(|token| AbsoluteSemanticToken {
line: token.line,
start_char: token.start_char,
length: token.length,
token_type: token.token_type,
modifiers: token.token_modifiers_bitset,
}),
);
}
absolute.sort_by_key(|token| {
(
token.line,
token.start_char,
token.length,
token.token_type,
token.modifiers,
)
});
absolute.dedup_by_key(|token| {
(
token.line,
token.start_char,
token.length,
token.token_type,
token.modifiers,
)
});
tokens.data = encode_semantic_tokens(&absolute);
Some(tokens)
}
fn is_shape_toml(uri: &Uri) -> bool {
uri.as_str().ends_with("shape.toml")
}
async fn analyze_toml_document(&self, uri: &Uri) {
let doc = match self.documents.get(uri) {
Some(doc) => doc,
None => return,
};
let text = doc.text();
let diagnostics = crate::toml_support::diagnostics::validate_toml(&text);
self.client
.publish_diagnostics(uri.clone(), diagnostics, None)
.await;
}
async fn compute_pull_diagnostics(&self, uri: &Uri) -> Vec<Diagnostic> {
let Some(doc) = self.documents.get(uri) else {
return Vec::new();
};
let text = doc.text();
let parse_source = mask_leading_prefix_for_parse(&text, 0);
match parse_program(parse_source.as_ref()) {
Ok(program) => {
let module_cache = self.documents.get_module_cache();
let mut diagnostics = crate::analysis::analyze_program_semantics(
&program,
&text,
uri.to_file_path().as_deref(),
Some(&module_cache),
self.project_root.get().map(|p| p.as_path()),
);
diagnostics.extend(crate::doc_diagnostics::validate_program_docs(
&program,
&text,
Some(&module_cache),
uri.to_file_path().as_deref(),
self.project_root.get().map(|p| p.as_path()),
));
diagnostics.extend(self.foreign_lsp.get_diagnostics(uri.as_str()).await);
diagnostics
}
Err(error) => error_to_diagnostic(&error),
}
}
async fn analyze_document(&self, uri: &Uri) {
let doc = match self.documents.get(uri) {
Some(doc) => doc,
None => return,
};
let text = doc.text();
let mut frontmatter_diagnostics = Vec::new();
let frontmatter_prefix_len;
{
use shape_runtime::frontmatter::{
FrontmatterDiagnosticSeverity, parse_frontmatter, parse_frontmatter_validated,
};
let (config, fm_diags, rest) = parse_frontmatter_validated(&text);
frontmatter_prefix_len = text.len().saturating_sub(rest.len());
for diag in fm_diags {
let severity = match diag.severity {
FrontmatterDiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
FrontmatterDiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
};
let range = diag
.location
.map(|loc| Range {
start: Position {
line: loc.line,
character: loc.character,
},
end: Position {
line: loc.line,
character: loc.character + loc.length.max(1),
},
})
.unwrap_or_else(frontmatter_fallback_range);
frontmatter_diagnostics.push(Diagnostic {
range,
severity: Some(severity),
message: diag.message,
source: Some("shape".to_string()),
..Default::default()
});
}
if config.is_some() {
if let (Some(project_root), Some(path)) =
(self.project_root.get(), uri.to_file_path())
{
if path.as_ref().starts_with(project_root) {
frontmatter_diagnostics.push(Diagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 3,
},
},
severity: Some(DiagnosticSeverity::ERROR),
message: "Frontmatter and shape.toml are mutually exclusive; use one configuration source.".to_string(),
source: Some("shape".to_string()),
..Default::default()
});
}
}
}
if let (Some(frontmatter), Some(script_path)) =
(parse_frontmatter(&text).0, uri.to_file_path())
{
let path_ranges = frontmatter_extension_path_ranges(&text);
if let Some(script_dir) = script_path.as_ref().parent() {
for (index, extension) in frontmatter.extensions.into_iter().enumerate() {
let resolved = if extension.path.is_absolute() {
extension.path.clone()
} else {
script_dir.join(&extension.path)
};
if !resolved.exists() {
let range = path_ranges
.get(index)
.cloned()
.unwrap_or_else(frontmatter_fallback_range);
frontmatter_diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
message: format!(
"Extension '{}' path does not exist: {}",
extension.name,
resolved.display()
),
source: Some("shape".to_string()),
..Default::default()
});
}
}
}
}
}
let parse_source = mask_leading_prefix_for_parse(&text, frontmatter_prefix_len);
let diagnostics = match parse_program(parse_source.as_ref()) {
Ok(program) => {
self.last_good_programs.insert(uri.clone(), program.clone());
let file_path = uri.to_file_path();
let foreign_startup_diagnostics = self
.foreign_lsp
.update_documents(
uri.as_str(),
&text,
&program.items,
file_path.as_deref(),
self.project_root.get().map(|p| p.as_path()),
)
.await;
let module_cache = self.documents.get_module_cache();
let mut diagnostics = crate::analysis::analyze_program_semantics(
&program,
&text,
uri.to_file_path().as_deref(),
Some(&module_cache),
self.project_root.get().map(|p| p.as_path()),
);
diagnostics.extend(crate::doc_diagnostics::validate_program_docs(
&program,
&text,
Some(&module_cache),
uri.to_file_path().as_deref(),
self.project_root.get().map(|p| p.as_path()),
));
diagnostics.extend(foreign_startup_diagnostics);
diagnostics.extend(self.foreign_lsp.get_diagnostics(uri.as_str()).await);
diagnostics
}
Err(error) => {
let partial = shape_ast::parse_program_resilient(parse_source.as_ref());
let mut diagnostics = Vec::new();
let has_non_grammar_partial_error = partial
.errors
.iter()
.any(|e| !matches!(e.kind, ParseErrorKind::GrammarFailure));
if partial.errors.is_empty() || !has_non_grammar_partial_error {
diagnostics.extend(error_to_diagnostic(&error));
}
if has_non_grammar_partial_error {
for parse_error in &partial.errors {
if matches!(parse_error.kind, ParseErrorKind::GrammarFailure) {
continue;
}
let (start_line, start_col) = offset_to_line_col(&text, parse_error.span.0);
let (end_line, end_col) = offset_to_line_col(&text, parse_error.span.1);
diagnostics.push(Diagnostic {
range: Range {
start: Position {
line: start_line,
character: start_col,
},
end: Position {
line: end_line,
character: end_col,
},
},
severity: Some(DiagnosticSeverity::ERROR),
message: parse_error.message.clone(),
source: Some("shape".to_string()),
..Default::default()
});
}
}
if !partial.items.is_empty() {
self.last_good_programs
.insert(uri.clone(), partial.into_program());
}
diagnostics
}
};
let mut all_diagnostics = frontmatter_diagnostics;
all_diagnostics.extend(diagnostics);
self.client
.publish_diagnostics(uri.clone(), all_diagnostics, None)
.await;
}
}
impl LanguageServer for ShapeLanguageServer {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
self.client
.log_message(MessageType::INFO, "Shape Language Server initializing")
.await;
let mut workspace_folder: Option<std::path::PathBuf> = None;
if let Some(folders) = params.workspace_folders.as_ref() {
if let Some(folder) = folders.first() {
if let Some(folder_path) = folder.uri.to_file_path() {
workspace_folder = Some(folder_path.to_path_buf());
if let Some(project) = shape_runtime::project::find_project_root(&folder_path) {
self.client
.log_message(
MessageType::INFO,
format!(
"Detected project root: {} ({})",
project.config.project.name,
project.root_path.display()
),
)
.await;
let _ = self.project_root.set(project.root_path);
}
}
}
}
if let Some(dir) = self
.project_root
.get()
.cloned()
.or_else(|| workspace_folder.clone())
{
self.foreign_lsp.set_workspace_dir(dir);
}
let workspace_hint = self
.project_root
.get()
.map(|path| path.as_path())
.or(workspace_folder.as_deref());
let configured_extensions = configured_extensions_from_lsp_value(
params.initialization_options.as_ref(),
workspace_hint,
);
if !configured_extensions.is_empty() {
self.client
.log_message(
MessageType::INFO,
format!(
"Configured {} always-load extension(s) from LSP initialization options",
configured_extensions.len()
),
)
.await;
}
self.foreign_lsp
.set_configured_extensions(configured_extensions)
.await;
let initial_inlay_cfg =
InlayHintConfig::from_lsp_settings(params.initialization_options.as_ref());
if let Ok(mut guard) = self.inlay_hint_config.write() {
*guard = initial_inlay_cfg;
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(true),
trigger_characters: Some(vec![
".".to_string(),
"(".to_string(),
" ".to_string(),
"@".to_string(),
":".to_string(),
]),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
all_commit_characters: Some(vec![
".".to_string(),
"(".to_string(),
",".to_string(),
";".to_string(),
]),
completion_item: None,
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: None,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),
document_symbol_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
document_highlight_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
semantic_tokens_provider: Some(
SemanticTokensServerCapabilities::SemanticTokensOptions(
SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
legend: get_legend(),
range: Some(true),
full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
},
),
),
inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
InlayHintOptions {
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
resolve_provider: Some(true),
},
))),
code_action_provider: Some(CodeActionProviderCapability::Options(
CodeActionOptions {
code_action_kinds: Some(vec![
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::REFACTOR_REWRITE,
CodeActionKind::SOURCE,
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
CodeActionKind::SOURCE_FIX_ALL,
]),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
resolve_provider: Some(false),
},
)),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
first_trigger_character: "}".to_string(),
more_trigger_character: Some(vec!["\n".to_string()]),
}),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
})),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(true),
}),
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
document_link_provider: Some(DocumentLinkOptions {
resolve_provider: Some(false),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),
execute_command_provider: Some(ExecuteCommandOptions {
commands: registered_commands(),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
DiagnosticOptions {
identifier: Some("shape".to_string()),
inter_file_dependencies: true,
workspace_diagnostics: true,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
},
)),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: None,
file_operations: Some(WorkspaceFileOperationsServerCapabilities {
will_rename: Some(FileOperationRegistrationOptions {
filters: vec![FileOperationFilter {
scheme: Some("file".to_string()),
pattern: FileOperationPattern {
glob: "**/*.shape".to_string(),
matches: Some(FileOperationPatternKind::File),
options: None,
},
}],
}),
..Default::default()
}),
}),
..ServerCapabilities::default()
},
server_info: Some(ServerInfo {
name: "Shape Language Server".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
..InitializeResult::default()
})
}
async fn initialized(&self, _params: InitializedParams) {
self.client
.log_message(MessageType::INFO, "Shape Language Server initialized")
.await;
}
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
let workspace_hint = self.project_root.get().map(|path| path.as_path());
let configured_extensions =
configured_extensions_from_lsp_value(Some(¶ms.settings), workspace_hint);
self.foreign_lsp
.set_configured_extensions(configured_extensions.clone())
.await;
let new_inlay_cfg = InlayHintConfig::from_lsp_settings(Some(¶ms.settings));
if let Ok(mut guard) = self.inlay_hint_config.write() {
*guard = new_inlay_cfg;
}
let _ = self.client.inlay_hint_refresh().await;
self.client
.log_message(
MessageType::INFO,
format!(
"Updated always-load extensions from configuration change ({} configured); inlay-hint config refreshed",
configured_extensions.len()
),
)
.await;
}
async fn shutdown(&self) -> Result<()> {
self.client
.log_message(MessageType::INFO, "Shape Language Server shutting down")
.await;
self.foreign_lsp.shutdown().await;
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let version = params.text_document.version;
let text = params.text_document.text;
self.client
.log_message(
MessageType::INFO,
format!("Document opened: {}", uri.as_str()),
)
.await;
self.documents.open(uri.clone(), version, text);
if Self::is_shape_toml(&uri) {
self.analyze_toml_document(&uri).await;
return;
}
self.analyze_document(&uri).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
let version = params.text_document.version;
if let Some(change) = params.content_changes.into_iter().next() {
let text = change.text;
self.documents.update(&uri, version, text);
self.client
.log_message(
MessageType::INFO,
format!("Document changed: {} (version {})", uri.as_str(), version),
)
.await;
if Self::is_shape_toml(&uri) {
self.analyze_toml_document(&uri).await;
return;
}
self.analyze_document(&uri).await;
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
self.client
.log_message(
MessageType::INFO,
format!("Document closed: {}", uri.as_str()),
)
.await;
self.client
.publish_diagnostics(uri.clone(), vec![], None)
.await;
self.last_good_programs.remove(&uri);
self.semantic_tokens_cache.remove(&uri);
self.documents.close(&uri);
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
self.client
.log_message(
MessageType::INFO,
format!(
"Completion requested at {}:{}:{}",
uri.as_str(),
position.line,
position.character
),
)
.await;
if Self::is_shape_toml(&uri) {
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let items = crate::toml_support::completions::get_toml_completions(&text, position);
return Ok(Some(CompletionResponse::Array(items)));
}
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
if is_position_in_frontmatter(&text, position) {
let items =
crate::toml_support::completions::get_frontmatter_completions(&text, position);
return Ok(Some(CompletionResponse::Array(items)));
}
if let Some(cached_program) = self.last_good_programs.get(&uri) {
if crate::foreign_lsp::is_position_in_foreign_block(
&cached_program.items,
&text,
position,
) {
let completions = self
.foreign_lsp
.handle_completion(uri.as_str(), position, &cached_program.items, &text)
.await;
if let Some(items) = completions {
return Ok(Some(CompletionResponse::Array(items)));
}
}
}
let cached_symbols = self.documents.get_cached_symbols(&uri);
let cached_types = self.documents.get_cached_types(&uri);
let module_cache = self.documents.get_module_cache();
let file_path = uri.to_file_path();
let (completions, updated_symbols, updated_types) = get_completions_with_context(
&text,
position,
&cached_symbols,
&cached_types,
Some(&module_cache),
file_path.as_deref(),
self.project_root.get().map(|p| p.as_path()),
);
if let Some(symbols) = updated_symbols {
self.documents.update_cached_symbols(&uri, symbols);
}
if let Some(types) = updated_types {
self.documents.update_cached_types(&uri, types);
}
Ok(Some(CompletionResponse::Array(completions)))
}
async fn completion_resolve(&self, item: CompletionItem) -> Result<CompletionItem> {
Ok(item)
}
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
self.client
.log_message(
MessageType::INFO,
format!(
"Hover requested at {}:{}:{}",
uri.as_str(),
position.line,
position.character
),
)
.await;
if Self::is_shape_toml(&uri) {
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
return Ok(crate::toml_support::hover::get_toml_hover(&text, position));
}
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
if let Some(cached_program) = self.last_good_programs.get(&uri) {
if crate::foreign_lsp::is_position_in_foreign_block(
&cached_program.items,
&text,
position,
) {
let hover = self
.foreign_lsp
.handle_hover(uri.as_str(), position, &cached_program.items, &text)
.await;
if hover.is_some() {
return Ok(hover);
}
}
}
let module_cache = self.documents.get_module_cache();
let file_path = uri.to_file_path();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let hover = get_hover(
&text,
position,
Some(&module_cache),
file_path.as_deref(),
cached_ref,
);
Ok(hover)
}
async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
if let Some(cached_program) = self.last_good_programs.get(&uri) {
if crate::foreign_lsp::is_position_in_foreign_block(
&cached_program.items,
&text,
position,
) {
let signature_help = self
.foreign_lsp
.handle_signature_help(uri.as_str(), position, &cached_program.items, &text)
.await;
if signature_help.is_some() {
return Ok(signature_help);
}
}
}
let sig_help = get_signature_help(&text, position);
Ok(sig_help)
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> Result<Option<DocumentSymbolResponse>> {
let uri = params.text_document.uri;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let symbols = get_document_symbols(&text);
Ok(symbols)
}
async fn symbol(
&self,
params: WorkspaceSymbolParams,
) -> Result<Option<WorkspaceSymbolResponse>> {
let query = params.query;
let mut all_symbols = Vec::new();
let mut visited: HashSet<std::path::PathBuf> = HashSet::new();
for uri in self.documents.all_uris() {
if let Some(doc) = self.documents.get(&uri) {
if let Some(path_cow) = uri.to_file_path() {
visited.insert(path_cow.into_owned());
}
let text = doc.text();
let symbols = get_workspace_symbols(&text, &uri, &query);
all_symbols.extend(symbols);
}
}
if let Some(root) = self.project_root.get() {
let module_cache = self.documents.get_module_cache();
for path in module_cache.enumerate_workspace_shape_files(root.as_path()) {
if !visited.insert(path.clone()) {
continue;
}
let Some(file_uri) = Uri::from_file_path(&path) else {
continue;
};
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let symbols = get_workspace_symbols(&text, &file_uri, &query);
all_symbols.extend(symbols);
}
}
if all_symbols.is_empty() {
Ok(None)
} else {
Ok(Some(WorkspaceSymbolResponse::Flat(all_symbols)))
}
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
if let Some(cached_program) = self.last_good_programs.get(&uri) {
if crate::foreign_lsp::is_position_in_foreign_block(
&cached_program.items,
&text,
position,
) {
let definition = self
.foreign_lsp
.handle_definition(uri.as_str(), position, &cached_program.items, &text)
.await;
if definition.is_some() {
return Ok(definition);
}
}
}
let module_cache = self.documents.get_module_cache();
let mut annotation_discovery = AnnotationDiscovery::new();
let parse_source = parser_source(&text);
if let Ok(program) = parse_program(parse_source.as_ref()) {
annotation_discovery.discover_from_program(&program);
if let Some(file_path) = uri.to_file_path() {
annotation_discovery.discover_from_imports_with_cache(
&program,
&file_path,
&module_cache,
self.project_root.get().map(|p| p.as_path()),
);
}
}
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let definition = get_definition(
&text,
position,
&uri,
Some(&module_cache),
Some(&annotation_discovery),
cached_ref,
);
Ok(definition)
}
async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
if let Some(cached_program) = self.last_good_programs.get(&uri) {
if crate::foreign_lsp::is_position_in_foreign_block(
&cached_program.items,
&text,
position,
) {
let references = self
.foreign_lsp
.handle_references(uri.as_str(), position, &cached_program.items, &text)
.await;
if references.is_some() {
return Ok(references);
}
}
}
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let module_cache = self.documents.get_module_cache();
let workspace_root = self.project_root.get().map(|p| p.as_path());
let references = get_references_cross_file(
&text,
position,
&uri,
cached_ref,
Some(&self.documents),
Some(&module_cache),
workspace_root,
);
Ok(references)
}
async fn goto_type_definition(
&self,
params: GotoTypeDefinitionParams,
) -> Result<Option<GotoTypeDefinitionResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let module_cache = self.documents.get_module_cache();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let response = get_type_definition(&text, position, &uri, Some(&module_cache), cached_ref);
Ok(response)
}
async fn goto_implementation(
&self,
params: GotoImplementationParams,
) -> Result<Option<GotoImplementationResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let locations = get_implementations(&text, position, &uri, cached_ref);
Ok(locations.map(GotoImplementationResponse::Array))
}
async fn goto_declaration(
&self,
params: GotoDeclarationParams,
) -> Result<Option<GotoDeclarationResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let module_cache = self.documents.get_module_cache();
let mut annotation_discovery = AnnotationDiscovery::new();
let parse_source = parser_source(&text);
if let Ok(program) = parse_program(parse_source.as_ref()) {
annotation_discovery.discover_from_program(&program);
if let Some(file_path) = uri.to_file_path() {
annotation_discovery.discover_from_imports_with_cache(
&program,
&file_path,
&module_cache,
self.project_root.get().map(|p| p.as_path()),
);
}
}
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let response = get_declaration(
&text,
position,
&uri,
Some(&module_cache),
Some(&annotation_discovery),
cached_ref,
);
Ok(response)
}
async fn document_highlight(
&self,
params: DocumentHighlightParams,
) -> Result<Option<Vec<DocumentHighlight>>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let highlights = get_document_highlights(&text, position, cached_ref);
Ok(highlights)
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
let uri = params.text_document.uri;
self.client
.log_message(
MessageType::INFO,
format!("Semantic tokens requested for {}", uri.as_str()),
)
.await;
let Some(mut tokens) = self.collect_full_semantic_tokens(&uri).await else {
return Ok(None);
};
let result_id = self.next_semantic_tokens_result_id();
tokens.result_id = Some(result_id.clone());
self.semantic_tokens_cache.insert(
uri,
CachedSemanticTokens {
result_id,
data: tokens.data.clone(),
},
);
Ok(Some(SemanticTokensResult::Tokens(tokens)))
}
async fn semantic_tokens_range(
&self,
params: SemanticTokensRangeParams,
) -> Result<Option<SemanticTokensRangeResult>> {
let uri = params.text_document.uri;
let range = params.range;
self.client
.log_message(
MessageType::INFO,
format!(
"Semantic tokens (range) requested for {} [{}:{} - {}:{}]",
uri.as_str(),
range.start.line,
range.start.character,
range.end.line,
range.end.character,
),
)
.await;
let Some(tokens) = self.collect_full_semantic_tokens(&uri).await else {
return Ok(None);
};
let absolute = decode_semantic_tokens(&tokens.data);
let filtered: Vec<AbsoluteSemanticToken> = absolute
.into_iter()
.filter(|token| position_in_range(token.line, token.start_char, &range))
.collect();
let result = SemanticTokens {
result_id: None,
data: encode_semantic_tokens(&filtered),
};
Ok(Some(SemanticTokensRangeResult::Tokens(result)))
}
async fn semantic_tokens_full_delta(
&self,
params: SemanticTokensDeltaParams,
) -> Result<Option<SemanticTokensFullDeltaResult>> {
let uri = params.text_document.uri;
let previous_result_id = params.previous_result_id;
self.client
.log_message(
MessageType::INFO,
format!(
"Semantic tokens (delta) requested for {} (previous_result_id={})",
uri.as_str(),
previous_result_id,
),
)
.await;
let Some(mut tokens) = self.collect_full_semantic_tokens(&uri).await else {
return Ok(None);
};
let new_result_id = self.next_semantic_tokens_result_id();
let cached = self
.semantic_tokens_cache
.get(&uri)
.map(|entry| entry.clone());
let edits = match cached {
Some(cached) if cached.result_id == previous_result_id => {
Some(compute_semantic_token_edits(&cached.data, &tokens.data))
}
_ => None,
};
self.semantic_tokens_cache.insert(
uri,
CachedSemanticTokens {
result_id: new_result_id.clone(),
data: tokens.data.clone(),
},
);
match edits {
Some(edits) => Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
SemanticTokensDelta {
result_id: Some(new_result_id),
edits,
},
))),
None => {
tokens.result_id = Some(new_result_id);
Ok(Some(SemanticTokensFullDeltaResult::Tokens(tokens)))
}
}
}
async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
let uri = params.text_document.uri;
let range = params.range;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let config = self
.inlay_hint_config
.read()
.map(|guard| guard.clone())
.unwrap_or_default();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let file_path = uri.to_file_path();
let hints = get_inlay_hints_with_context(
&text,
range,
&config,
cached_ref,
file_path.as_deref(),
self.project_root.get().map(|p| p.as_path()),
);
if hints.is_empty() {
Ok(None)
} else {
Ok(Some(hints))
}
}
async fn inlay_hint_resolve(&self, mut hint: InlayHint) -> Result<InlayHint> {
use tower_lsp_server::ls_types::InlayHintTooltip;
if hint.tooltip.is_some() {
return Ok(hint);
}
let kind = hint
.data
.as_ref()
.and_then(|v| v.get("kind"))
.and_then(|v| v.as_str());
let tooltip = match kind {
Some("chain") => Some(
"Inferred type of the intermediate method-chain step (W2.4 / 1.27)."
.to_string(),
),
Some("binding-kind") => Some(
"LSP-side approximation of BindingStorageClass (ADR-006 §2). \
The compiler at crates/shape-vm/src/type_tracking.rs:286 is authoritative."
.to_string(),
),
_ => None,
};
if let Some(text) = tooltip {
hint.tooltip = Some(InlayHintTooltip::String(text));
}
Ok(hint)
}
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
let uri = params.text_document.uri;
let range = params.range;
let diagnostics = params.context.diagnostics;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let module_cache = self.documents.get_module_cache();
let actions = get_code_actions(
&text,
&uri,
range,
&diagnostics,
Some(&module_cache),
params.context.only.as_deref(),
);
if actions.is_empty() {
Ok(None)
} else {
Ok(Some(actions))
}
}
async fn diagnostic(
&self,
params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
let uri = params.text_document.uri;
let items = self.compute_pull_diagnostics(&uri).await;
Ok(DocumentDiagnosticReportResult::Report(
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items,
},
}),
))
}
async fn workspace_diagnostic(
&self,
_params: WorkspaceDiagnosticParams,
) -> Result<WorkspaceDiagnosticReportResult> {
let mut items: Vec<WorkspaceDocumentDiagnosticReport> = Vec::new();
for uri in self.documents.all_uris() {
let diags = self.compute_pull_diagnostics(&uri).await;
items.push(WorkspaceDocumentDiagnosticReport::Full(
WorkspaceFullDocumentDiagnosticReport {
uri,
version: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: diags,
},
},
));
}
Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items },
))
}
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let edits = format_document(&text, ¶ms.options);
if edits.is_empty() {
Ok(None)
} else {
Ok(Some(edits))
}
}
async fn range_formatting(
&self,
params: DocumentRangeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let range = params.range;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let edits = format_range(&text, range, ¶ms.options);
if edits.is_empty() {
Ok(None)
} else {
Ok(Some(edits))
}
}
async fn on_type_formatting(
&self,
params: DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let edits = format_on_type(&text, position, ¶ms.ch, ¶ms.options);
if edits.is_empty() {
Ok(None)
} else {
Ok(Some(edits))
}
}
async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> Result<Option<PrepareRenameResponse>> {
let uri = params.text_document.uri;
let position = params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let response = prepare_rename(&text, position);
Ok(response)
}
async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let new_name = params.new_name;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let cached = self.last_good_programs.get(&uri);
let cached_ref = cached.as_ref().map(|r| r.value());
let module_cache = self.documents.get_module_cache();
let workspace_root = self.project_root.get().map(|p| p.as_path());
let edit = rename_cross_file(
&text,
&uri,
position,
&new_name,
cached_ref,
Some(&self.documents),
Some(&module_cache),
workspace_root,
);
Ok(edit)
}
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
let uri = params.text_document.uri;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let lenses = get_code_lenses(&text, &uri);
if lenses.is_empty() {
Ok(None)
} else {
Ok(Some(lenses))
}
}
async fn code_lens_resolve(&self, lens: CodeLens) -> Result<CodeLens> {
Ok(resolve_code_lens(lens))
}
async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
let uri = params.text_document.uri;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let parse_source = parser_source(&text);
let program = match parse_program(parse_source.as_ref()) {
Ok(p) => p,
Err(_) => {
match self.last_good_programs.get(&uri) {
Some(cached) => cached.value().clone(),
None => return Ok(None),
}
}
};
let ranges = get_folding_ranges(&text, &program);
if ranges.is_empty() {
Ok(None)
} else {
Ok(Some(ranges))
}
}
async fn document_link(
&self,
params: DocumentLinkParams,
) -> Result<Option<Vec<DocumentLink>>> {
let uri = params.text_document.uri;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let parse_source = parser_source(&text);
let program = match parse_program(parse_source.as_ref()) {
Ok(program) => program,
Err(_) => match self.last_good_programs.get(&uri) {
Some(cached) => cached.value().clone(),
None => return Ok(None),
},
};
let module_cache = self.documents.get_module_cache();
let file_path = uri.to_file_path();
let links = crate::doc_links::collect_document_links(
&program,
&text,
Some(&module_cache),
file_path.as_deref(),
self.project_root.get().map(|p| p.as_path()),
);
if links.is_empty() {
Ok(None)
} else {
Ok(Some(links))
}
}
async fn prepare_call_hierarchy(
&self,
params: CallHierarchyPrepareParams,
) -> Result<Option<Vec<CallHierarchyItem>>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let doc = match self.documents.get(&uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
Ok(ch_prepare(&text, position, &uri))
}
async fn incoming_calls(
&self,
params: CallHierarchyIncomingCallsParams,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
let uri = ¶ms.item.uri;
let doc = match self.documents.get(uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let results = ch_incoming(&text, ¶ms.item, uri);
if results.is_empty() {
Ok(None)
} else {
Ok(Some(results))
}
}
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
let module_cache = self.documents.get_module_cache();
let mut invalidated = 0usize;
for event in ¶ms.changes {
let Some(path_cow) = event.uri.to_file_path() else {
continue;
};
let path = path_cow.into_owned();
if path.extension().and_then(|e| e.to_str()) != Some("shape") {
continue;
}
module_cache.invalidate(&path);
if matches!(event.typ, FileChangeType::DELETED | FileChangeType::CHANGED) {
self.last_good_programs.remove(&event.uri);
}
invalidated += 1;
}
if invalidated > 0 {
self.client
.log_message(
MessageType::INFO,
format!(
"didChangeWatchedFiles: invalidated {} module-cache entr{}",
invalidated,
if invalidated == 1 { "y" } else { "ies" }
),
)
.await;
}
}
async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<LSPAny>> {
if !registered_commands().contains(¶ms.command) {
self.client
.log_message(
MessageType::WARNING,
format!(
"executeCommand: unknown command {:?}; declared commands: {:?}",
params.command,
registered_commands()
),
)
.await;
return Err(tower_lsp_server::jsonrpc::Error::method_not_found());
}
self.client
.log_message(
MessageType::INFO,
format!(
"executeCommand: {:?} acknowledged ({} arg{})",
params.command,
params.arguments.len(),
if params.arguments.len() == 1 { "" } else { "s" }
),
)
.await;
Ok(None)
}
async fn will_rename_files(
&self,
params: RenameFilesParams,
) -> Result<Option<WorkspaceEdit>> {
let Some(workspace_root) = self.project_root.get() else {
return Ok(None);
};
let module_cache = self.documents.get_module_cache();
let mut renames: Vec<(String, String)> = Vec::new();
for rename in ¶ms.files {
let Ok(old_uri) = rename.old_uri.parse::<Uri>() else {
continue;
};
let Ok(new_uri) = rename.new_uri.parse::<Uri>() else {
continue;
};
let Some(old_path) = old_uri.to_file_path() else {
continue;
};
let Some(new_path) = new_uri.to_file_path() else {
continue;
};
let Some(old_mod) = crate::module_cache::path_to_module_path(
&old_path,
workspace_root.as_path(),
) else {
continue;
};
let Some(new_mod) = crate::module_cache::path_to_module_path(
&new_path,
workspace_root.as_path(),
) else {
continue;
};
if old_mod == new_mod {
continue;
}
renames.push((old_mod, new_mod));
}
if renames.is_empty() {
return Ok(None);
}
let mut changes: std::collections::HashMap<Uri, Vec<TextEdit>> =
std::collections::HashMap::new();
let mut visited: HashSet<std::path::PathBuf> = HashSet::new();
for uri in self.documents.all_uris() {
let Some(doc) = self.documents.get(&uri) else {
continue;
};
if let Some(p) = uri.to_file_path() {
visited.insert(p.into_owned());
}
let text = doc.text();
let edits = collect_import_rewrites(&text, &renames);
if !edits.is_empty() {
changes.entry(uri).or_insert_with(Vec::new).extend(edits);
}
}
for path in module_cache.enumerate_workspace_shape_files(workspace_root.as_path()) {
if !visited.insert(path.clone()) {
continue;
}
let Some(uri) = Uri::from_file_path(&path) else {
continue;
};
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let edits = collect_import_rewrites(&text, &renames);
if !edits.is_empty() {
changes.entry(uri).or_insert_with(Vec::new).extend(edits);
}
}
if changes.is_empty() {
Ok(None)
} else {
Ok(Some(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
}))
}
}
async fn outgoing_calls(
&self,
params: CallHierarchyOutgoingCallsParams,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
let uri = ¶ms.item.uri;
let doc = match self.documents.get(uri) {
Some(doc) => doc,
None => return Ok(None),
};
let text = doc.text();
let results = ch_outgoing(&text, ¶ms.item, uri);
if results.is_empty() {
Ok(None)
} else {
Ok(Some(results))
}
}
}
fn registered_commands() -> Vec<String> {
vec![
"shape.findReferences".to_string(),
"shape.findImplementations".to_string(),
"shape.runTests".to_string(),
"shape.debugTests".to_string(),
"shape.showAnnotation".to_string(),
"shape.showTraitMethod".to_string(),
]
}
fn collect_import_rewrites(text: &str, renames: &[(String, String)]) -> Vec<TextEdit> {
use crate::util::offset_to_line_col;
let mut edits = Vec::new();
let mut offset = 0usize;
for line in text.split_inclusive('\n') {
let trimmed_offset = offset
+ (line.len() - line.trim_start().len());
let body = line.trim_start();
if let Some(rest) = body.strip_prefix("from ") {
let path_start_in_body = "from ".len() + rest.len() - rest.trim_start().len();
let path_start = trimmed_offset + path_start_in_body;
let after_from = rest.trim_start();
let mut end = 0usize;
let bytes = after_from.as_bytes();
while end < bytes.len() {
let b = bytes[end];
if b.is_ascii_alphanumeric() || b == b'_' {
end += 1;
} else if end + 1 < bytes.len() && b == b':' && bytes[end + 1] == b':' {
end += 2;
} else {
break;
}
}
if end > 0 {
let actual_path = &after_from[..end];
for (old, new) in renames {
if actual_path == old {
let path_end = path_start + end;
let (sl, sc) = offset_to_line_col(text, path_start);
let (el, ec) = offset_to_line_col(text, path_end);
edits.push(TextEdit {
range: Range {
start: Position {
line: sl,
character: sc,
},
end: Position {
line: el,
character: ec,
},
},
new_text: new.clone(),
});
break;
}
}
}
}
offset += line.len();
}
edits
}
fn frontmatter_fallback_range() -> Range {
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 3,
},
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct AbsoluteSemanticToken {
line: u32,
start_char: u32,
length: u32,
token_type: u32,
modifiers: u32,
}
fn decode_semantic_tokens(tokens: &[SemanticToken]) -> Vec<AbsoluteSemanticToken> {
let mut decoded = Vec::with_capacity(tokens.len());
let mut line = 0u32;
let mut col = 0u32;
for token in tokens {
line += token.delta_line;
if token.delta_line == 0 {
col += token.delta_start;
} else {
col = token.delta_start;
}
decoded.push(AbsoluteSemanticToken {
line,
start_char: col,
length: token.length,
token_type: token.token_type,
modifiers: token.token_modifiers_bitset,
});
}
decoded
}
fn encode_semantic_tokens(tokens: &[AbsoluteSemanticToken]) -> Vec<SemanticToken> {
let mut encoded = Vec::with_capacity(tokens.len());
let mut prev_line = 0u32;
let mut prev_start = 0u32;
for token in tokens {
let delta_line = token.line.saturating_sub(prev_line);
let delta_start = if delta_line == 0 {
token.start_char.saturating_sub(prev_start)
} else {
token.start_char
};
encoded.push(SemanticToken {
delta_line,
delta_start,
length: token.length,
token_type: token.token_type,
token_modifiers_bitset: token.modifiers,
});
prev_line = token.line;
prev_start = token.start_char;
}
encoded
}
fn position_in_range(line: u32, character: u32, range: &Range) -> bool {
let start = (range.start.line, range.start.character);
let end = (range.end.line, range.end.character);
let pos = (line, character);
pos >= start && pos < end
}
fn compute_semantic_token_edits(
old: &[SemanticToken],
new: &[SemanticToken],
) -> Vec<SemanticTokensEdit> {
let mut prefix = 0usize;
let max_prefix = old.len().min(new.len());
while prefix < max_prefix && semantic_tokens_eq(&old[prefix], &new[prefix]) {
prefix += 1;
}
let mut suffix = 0usize;
let max_suffix = old.len().min(new.len()) - prefix;
while suffix < max_suffix
&& semantic_tokens_eq(
&old[old.len() - 1 - suffix],
&new[new.len() - 1 - suffix],
)
{
suffix += 1;
}
let old_replace_start = prefix;
let old_replace_end = old.len() - suffix;
let new_replace_start = prefix;
let new_replace_end = new.len() - suffix;
if old_replace_start == old_replace_end && new_replace_start == new_replace_end {
return Vec::new();
}
let start = (old_replace_start * 5) as u32;
let delete_count = ((old_replace_end - old_replace_start) * 5) as u32;
let replacement: Vec<SemanticToken> =
new[new_replace_start..new_replace_end].to_vec();
let data = if replacement.is_empty() {
None
} else {
Some(replacement)
};
vec![SemanticTokensEdit {
start,
delete_count,
data,
}]
}
fn semantic_tokens_eq(a: &SemanticToken, b: &SemanticToken) -> bool {
a.delta_line == b.delta_line
&& a.delta_start == b.delta_start
&& a.length == b.length
&& a.token_type == b.token_type
&& a.token_modifiers_bitset == b.token_modifiers_bitset
}
fn frontmatter_extension_path_ranges(source: &str) -> Vec<Range> {
let lines: Vec<&str> = source.split('\n').collect();
if lines.is_empty() {
return Vec::new();
}
let delimiter_lines: Vec<usize> = lines
.iter()
.enumerate()
.filter_map(|(idx, line)| {
let trimmed = line.trim_end_matches('\r').trim();
if trimmed == "---" { Some(idx) } else { None }
})
.take(2)
.collect();
if delimiter_lines.len() < 2 {
return Vec::new();
}
let start = delimiter_lines[0] + 1;
let end = delimiter_lines[1];
let mut in_extensions = false;
let mut ranges = Vec::new();
for (line_idx, raw_line) in lines.iter().enumerate().take(end).skip(start) {
let trimmed = raw_line.trim();
if trimmed.starts_with("[[extensions]]") {
in_extensions = true;
continue;
}
if trimmed.starts_with("[[") || (trimmed.starts_with('[') && trimmed.ends_with(']')) {
in_extensions = false;
continue;
}
if !in_extensions {
continue;
}
let Some(eq_pos) = raw_line.find('=') else {
continue;
};
let key = raw_line[..eq_pos].trim();
if key != "path" {
continue;
}
let key_start = raw_line.find("path").unwrap_or_else(|| {
raw_line[..eq_pos]
.find(|c: char| !c.is_whitespace())
.unwrap_or(0)
});
let line_len = raw_line.trim_end_matches('\r').len();
let end_char = line_len.max(key_start + 4);
ranges.push(Range {
start: Position {
line: line_idx as u32,
character: key_start as u32,
},
end: Position {
line: line_idx as u32,
character: end_char as u32,
},
});
}
ranges
}
fn is_position_in_frontmatter(source: &str, position: Position) -> bool {
if source.starts_with("#!") && position.line == 0 {
return false;
}
let (_, _, rest) = shape_runtime::frontmatter::parse_frontmatter_validated(source);
let prefix_len = source.len().saturating_sub(rest.len());
if prefix_len == 0 {
return false;
}
position_to_offset(source, position)
.map(|offset| offset < prefix_len)
.unwrap_or(false)
}
fn configured_extensions_from_lsp_value(
options: Option<&serde_json::Value>,
workspace_root: Option<&std::path::Path>,
) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
let mut specs = collect_configured_extensions_from_options(options, workspace_root);
collect_global_extensions(&mut specs);
dedup_extension_specs(specs)
}
fn collect_configured_extensions_from_options(
options: Option<&serde_json::Value>,
workspace_root: Option<&std::path::Path>,
) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
let mut specs = Vec::new();
if let Some(options) = options {
collect_configured_extensions_from_array(
options.get("alwaysLoadExtensions"),
workspace_root,
&mut specs,
);
collect_configured_extensions_from_array(
options.get("always_load_extensions"),
workspace_root,
&mut specs,
);
if let Some(shape) = options.get("shape") {
collect_configured_extensions_from_array(
shape.get("alwaysLoadExtensions"),
workspace_root,
&mut specs,
);
collect_configured_extensions_from_array(
shape.get("always_load_extensions"),
workspace_root,
&mut specs,
);
}
}
specs
}
fn dedup_extension_specs(
specs: Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
let mut seen = HashSet::new();
specs
.into_iter()
.filter(|spec| {
let key = format!(
"{}|{}|{}",
spec.name,
spec.path.display(),
serde_json::to_string(&spec.config).unwrap_or_default()
);
seen.insert(key)
})
.collect()
}
fn collect_global_extensions(out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>) {
let Some(home) = dirs::home_dir() else {
return;
};
let ext_dir = home.join(".shape").join("extensions");
if !ext_dir.is_dir() {
return;
}
let Ok(entries) = std::fs::read_dir(&ext_dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let is_lib = path
.extension()
.and_then(|e| e.to_str())
.map(|ext| ext == "so" || ext == "dylib" || ext == "dll")
.unwrap_or(false);
if !is_lib {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| {
s.strip_prefix("libshape_ext_")
.or_else(|| s.strip_prefix("shape_ext_"))
.unwrap_or(s)
.to_string()
})
.unwrap_or_else(|| "extension".to_string());
out.push(crate::foreign_lsp::ConfiguredExtensionSpec {
name,
path,
config: serde_json::json!({}),
});
}
}
fn collect_configured_extensions_from_array(
value: Option<&serde_json::Value>,
workspace_root: Option<&std::path::Path>,
out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
) {
let Some(items) = value.and_then(|v| v.as_array()) else {
return;
};
for item in items {
if let Some(spec) = parse_configured_extension_item(item, workspace_root) {
out.push(spec);
}
}
}
fn parse_configured_extension_item(
item: &serde_json::Value,
workspace_root: Option<&std::path::Path>,
) -> Option<crate::foreign_lsp::ConfiguredExtensionSpec> {
let (name, path, config) = if let Some(path) = item.as_str() {
let path_buf = resolve_configured_extension_path(path, workspace_root);
(
configured_extension_name_from_path(&path_buf),
path_buf,
serde_json::json!({}),
)
} else if let Some(obj) = item.as_object() {
let path_str = obj.get("path")?.as_str()?;
let path_buf = resolve_configured_extension_path(path_str, workspace_root);
let name = obj
.get("name")
.and_then(|value| value.as_str())
.map(String::from)
.unwrap_or_else(|| configured_extension_name_from_path(&path_buf));
let config = obj
.get("config")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
(name, path_buf, config)
} else {
return None;
};
Some(crate::foreign_lsp::ConfiguredExtensionSpec { name, path, config })
}
fn resolve_configured_extension_path(
path: &str,
workspace_root: Option<&std::path::Path>,
) -> std::path::PathBuf {
let path = std::path::PathBuf::from(path);
if path.is_absolute() {
return path;
}
workspace_root.map(|root| root.join(&path)).unwrap_or(path)
}
fn configured_extension_name_from_path(path: &std::path::Path) -> String {
path.file_stem()
.and_then(|stem| stem.to_str())
.map(String::from)
.unwrap_or_else(|| "configured-extension".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::parser_source;
use tower_lsp_server::LspService;
#[tokio::test]
async fn test_server_creation() {
let (service, _socket) = LspService::new(|client| ShapeLanguageServer::new(client));
drop(service);
}
#[test]
fn test_frontmatter_extension_path_ranges_points_to_path_line() {
let source = r#"---
[[extensions]]
name = "duckdb"
path = "./extensions/libshape_ext_duckdb.so"
---
let x = 1
"#;
let ranges = frontmatter_extension_path_ranges(source);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start.line, 3);
assert_eq!(ranges[0].start.character, 0);
}
#[test]
fn test_frontmatter_extension_path_ranges_handles_shebang() {
let source = r#"#!/usr/bin/env shape
---
[[extensions]]
name = "duckdb"
path = "./extensions/libshape_ext_duckdb.so"
---
let x = 1
"#;
let ranges = frontmatter_extension_path_ranges(source);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start.line, 4);
}
#[test]
fn test_validate_imports_accepts_namespace_import_from_frontmatter_extension() {
let source = r#"---
# shape.toml
[[extensions]]
name = "duckdb"
path = "./extensions/libshape_ext_duckdb.so"
---
use duckdb
let conn = duckdb.connect("duckdb://analytics.db")
"#;
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("script.shape");
std::fs::write(&file_path, source).unwrap();
let parse_src = parser_source(source);
let program = parse_program(parse_src.as_ref()).expect("program should parse");
let module_cache = crate::module_cache::ModuleCache::new();
let mut compiler = shape_vm::BytecodeCompiler::new();
let diagnostics = crate::analysis::validate_imports_and_register_items(
&program,
source,
&file_path,
&module_cache,
None,
&mut compiler,
);
assert!(
diagnostics.iter().all(|diag| {
!diag.message.contains("Cannot resolve module ''")
&& !diag.message.contains("Cannot resolve module 'duckdb'")
}),
"namespace import from frontmatter extension should not emit resolution errors: {:?}",
diagnostics
);
}
#[test]
fn test_validate_imports_reports_unknown_namespace_module_name() {
let source = "use missingmod\nlet x = 1\n";
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("script.shape");
std::fs::write(&file_path, source).unwrap();
let parse_src = parser_source(source);
let program = parse_program(parse_src.as_ref()).expect("program should parse");
let module_cache = crate::module_cache::ModuleCache::new();
let mut compiler = shape_vm::BytecodeCompiler::new();
let diagnostics = crate::analysis::validate_imports_and_register_items(
&program,
source,
&file_path,
&module_cache,
None,
&mut compiler,
);
assert!(
diagnostics
.iter()
.any(|diag| diag.message.contains("Cannot resolve module 'missingmod'")),
"expected unknown namespace module diagnostic, got {:?}",
diagnostics
);
}
#[test]
fn test_is_position_in_frontmatter() {
let source = r#"---
name = "script"
[[extensions]]
name = "duckdb"
path = "./extensions/libshape_ext_duckdb.so"
---
let x = 1
"#;
assert!(is_position_in_frontmatter(
source,
Position {
line: 1,
character: 0
}
));
assert!(is_position_in_frontmatter(
source,
Position {
line: 3,
character: 2
}
));
assert!(!is_position_in_frontmatter(
source,
Position {
line: 6,
character: 0
}
));
}
#[test]
fn test_is_position_in_frontmatter_ignores_shebang_line() {
let source = r#"#!/usr/bin/env shape
---
name = "script"
---
print("hello")
"#;
assert!(!is_position_in_frontmatter(
source,
Position {
line: 0,
character: 5
}
));
assert!(is_position_in_frontmatter(
source,
Position {
line: 2,
character: 1
}
));
assert!(!is_position_in_frontmatter(
source,
Position {
line: 4,
character: 0
}
));
}
#[test]
fn test_configured_extensions_from_lsp_value_parses_top_level_array() {
let value = serde_json::json!({
"alwaysLoadExtensions": [
"./extensions/libshape_ext_python.so",
{
"name": "duckdb",
"path": "/tmp/libshape_ext_duckdb.so",
"config": { "mode": "readonly" }
}
]
});
let workspace_root = std::path::Path::new("/workspace");
let specs = collect_configured_extensions_from_options(Some(&value), Some(workspace_root));
assert_eq!(specs.len(), 2);
assert_eq!(
specs[0].path,
std::path::PathBuf::from("/workspace").join("./extensions/libshape_ext_python.so")
);
assert_eq!(specs[0].config, serde_json::json!({}));
assert_eq!(specs[1].name, "duckdb");
assert_eq!(
specs[1].path,
std::path::PathBuf::from("/tmp/libshape_ext_duckdb.so")
);
assert_eq!(specs[1].config, serde_json::json!({ "mode": "readonly" }));
}
#[test]
fn test_configured_extensions_from_lsp_value_parses_nested_shape_key_and_dedupes() {
let value = serde_json::json!({
"shape": {
"always_load_extensions": [
"/tmp/libshape_ext_python.so",
"/tmp/libshape_ext_python.so"
]
}
});
let specs = dedup_extension_specs(collect_configured_extensions_from_options(
Some(&value),
None,
));
assert_eq!(specs.len(), 1);
assert_eq!(
specs[0].path,
std::path::PathBuf::from("/tmp/libshape_ext_python.so")
);
assert_eq!(specs[0].name, "libshape_ext_python");
}
fn mk_token(
delta_line: u32,
delta_start: u32,
length: u32,
token_type: u32,
) -> SemanticToken {
SemanticToken {
delta_line,
delta_start,
length,
token_type,
token_modifiers_bitset: 0,
}
}
#[test]
fn position_in_range_inclusive_start_exclusive_end() {
let range = Range {
start: Position { line: 1, character: 0 },
end: Position { line: 3, character: 0 },
};
assert!(position_in_range(1, 0, &range));
assert!(position_in_range(2, 5, &range));
assert!(!position_in_range(3, 0, &range));
assert!(!position_in_range(0, 99, &range));
assert!(!position_in_range(3, 1, &range));
}
#[test]
fn position_in_range_single_line() {
let range = Range {
start: Position { line: 5, character: 2 },
end: Position { line: 5, character: 8 },
};
assert!(position_in_range(5, 2, &range));
assert!(position_in_range(5, 7, &range));
assert!(!position_in_range(5, 8, &range));
assert!(!position_in_range(5, 1, &range));
assert!(!position_in_range(4, 5, &range));
assert!(!position_in_range(6, 0, &range));
}
#[test]
fn compute_semantic_token_edits_empty_when_identical() {
let tokens = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
let edits = compute_semantic_token_edits(&tokens, &tokens);
assert!(edits.is_empty(), "identical sequences must produce zero edits");
}
#[test]
fn compute_semantic_token_edits_full_replacement_when_no_overlap() {
let old = vec![mk_token(0, 0, 3, 8)];
let new = vec![mk_token(0, 0, 5, 5), mk_token(1, 2, 4, 8)];
let edits = compute_semantic_token_edits(&old, &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 0);
assert_eq!(edit.delete_count, 5);
assert_eq!(
edit.data.as_ref().map(|d| d.len()),
Some(2),
"new tokens carried verbatim"
);
}
#[test]
fn compute_semantic_token_edits_preserves_common_prefix() {
let old = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
let new = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 7, 1)];
let edits = compute_semantic_token_edits(&old, &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 5);
assert_eq!(edit.delete_count, 5);
let data = edit.data.as_ref().expect("replacement data present");
assert_eq!(data.len(), 1);
assert_eq!(data[0].length, 7);
assert_eq!(data[0].token_type, 1);
}
#[test]
fn compute_semantic_token_edits_preserves_common_suffix() {
let old = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
let new = vec![mk_token(0, 0, 4, 1), mk_token(0, 4, 5, 5)];
let edits = compute_semantic_token_edits(&old, &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 0);
assert_eq!(edit.delete_count, 5);
let data = edit.data.as_ref().expect("replacement data present");
assert_eq!(data.len(), 1);
assert_eq!(data[0].length, 4);
}
#[test]
fn compute_semantic_token_edits_pure_insertion_emits_zero_delete() {
let old = vec![mk_token(0, 0, 3, 8)];
let new = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
let edits = compute_semantic_token_edits(&old, &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 5);
assert_eq!(edit.delete_count, 0);
let data = edit.data.as_ref().expect("inserted tokens present");
assert_eq!(data.len(), 1);
assert_eq!(data[0].length, 4);
}
#[test]
fn compute_semantic_token_edits_pure_deletion_emits_none_data() {
let old = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
let new = vec![mk_token(0, 0, 3, 8)];
let edits = compute_semantic_token_edits(&old, &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 5);
assert_eq!(edit.delete_count, 5);
assert!(
edit.data.is_none(),
"pure deletion serializes as `data: null` (None)"
);
}
#[test]
fn compute_semantic_token_edits_empty_to_empty_is_noop() {
let edits = compute_semantic_token_edits(&[], &[]);
assert!(edits.is_empty());
}
#[test]
fn compute_semantic_token_edits_empty_to_nonempty_is_insert() {
let new = vec![mk_token(0, 0, 3, 8)];
let edits = compute_semantic_token_edits(&[], &new);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 0);
assert_eq!(edit.delete_count, 0);
assert_eq!(edit.data.as_ref().map(|d| d.len()), Some(1));
}
#[test]
fn compute_semantic_token_edits_nonempty_to_empty_is_delete() {
let old = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
let edits = compute_semantic_token_edits(&old, &[]);
assert_eq!(edits.len(), 1);
let edit = &edits[0];
assert_eq!(edit.start, 0);
assert_eq!(edit.delete_count, 10);
assert!(edit.data.is_none());
}
#[test]
fn semantic_tokens_eq_compares_all_fields() {
let a = mk_token(0, 0, 3, 8);
let b = mk_token(0, 0, 3, 8);
assert!(semantic_tokens_eq(&a, &b));
let mut c = a;
c.token_modifiers_bitset = 4;
assert!(!semantic_tokens_eq(&a, &c));
let mut d = a;
d.length = 99;
assert!(!semantic_tokens_eq(&a, &d));
}
#[test]
fn registered_commands_cover_every_code_lens_command_w27() {
let declared: std::collections::HashSet<String> =
registered_commands().into_iter().collect();
for cmd in &[
"shape.findReferences",
"shape.findImplementations",
"shape.runTests",
"shape.debugTests",
"shape.showAnnotation",
"shape.showTraitMethod",
] {
assert!(
declared.contains(*cmd),
"executeCommand provider missing code-lens command {:?}",
cmd
);
}
}
#[test]
fn collect_import_rewrites_rewrites_matching_from_clause_w27() {
let text = "from foo::bar use { x, y };\nlet a = 1\n";
let renames = vec![("foo::bar".to_string(), "baz::qux".to_string())];
let edits = collect_import_rewrites(text, &renames);
assert_eq!(edits.len(), 1, "expected one rewrite, got {:?}", edits);
let edit = &edits[0];
assert_eq!(edit.new_text, "baz::qux");
assert_eq!(edit.range.start.line, 0);
assert_eq!(edit.range.start.character, 5);
assert_eq!(edit.range.end.character, 5 + "foo::bar".len() as u32);
}
#[test]
fn collect_import_rewrites_skips_non_matching_paths_w27() {
let text = "from other::mod use { x };\nfrom foo::bar use { y };\n";
let renames = vec![("foo::bar".to_string(), "new::path".to_string())];
let edits = collect_import_rewrites(text, &renames);
assert_eq!(edits.len(), 1, "only the foo::bar line should rewrite");
assert_eq!(edits[0].range.start.line, 1);
}
#[test]
fn collect_import_rewrites_does_not_match_substring_inside_identifier_w27() {
let text = "from foobar use { x };\nfrom foo use { y };\n";
let renames = vec![("foo".to_string(), "renamed".to_string())];
let edits = collect_import_rewrites(text, &renames);
assert_eq!(edits.len(), 1, "only the bare `from foo` line should match");
assert_eq!(edits[0].range.start.line, 1);
assert_eq!(edits[0].new_text, "renamed");
}
#[test]
fn collect_import_rewrites_handles_indented_imports_w27() {
let text = " from a::b use { x };\nfrom c::d use { y };\n";
let renames = vec![
("a::b".to_string(), "AA::BB".to_string()),
("c::d".to_string(), "CC::DD".to_string()),
];
let edits = collect_import_rewrites(text, &renames);
assert_eq!(edits.len(), 2);
assert_eq!(edits[0].range.start.character, 4 + 5);
assert_eq!(edits[0].new_text, "AA::BB");
assert_eq!(edits[1].range.start.line, 1);
assert_eq!(edits[1].new_text, "CC::DD");
}
#[test]
fn collect_import_rewrites_returns_empty_when_no_imports_match_w27() {
let text = "let x = 1\nlet y = 2\n";
let renames = vec![("foo".to_string(), "bar".to_string())];
assert!(collect_import_rewrites(text, &renames).is_empty());
}
#[tokio::test]
async fn workspace_symbol_eager_indexes_closed_files_w27() {
use crate::document_symbols::get_workspace_symbols;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let open_path = root.join("open.shape");
let closed_path = root.join("nested/closed.shape");
std::fs::create_dir_all(closed_path.parent().unwrap()).unwrap();
std::fs::write(&open_path, "fn open_sym() { 1 }\n").unwrap();
std::fs::write(&closed_path, "fn closed_sym() { 2 }\n").unwrap();
let cache = crate::module_cache::ModuleCache::new();
let files = cache.enumerate_workspace_shape_files(root);
let mut found_closed = false;
for path in files {
if path != closed_path {
continue;
}
let uri = Uri::from_file_path(&path).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
let symbols = get_workspace_symbols(&text, &uri, "closed_sym");
assert!(
symbols.iter().any(|s| s.name == "closed_sym"),
"closed-file symbol must appear in workspace results: {:?}",
symbols
);
found_closed = true;
}
assert!(
found_closed,
"workspace enumerator must surface the closed.shape file"
);
}
#[test]
fn did_change_watched_files_invalidates_module_cache_w27() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("watched.shape");
std::fs::write(&path, "pub fn before() { 1 }\n").unwrap();
let cache = crate::module_cache::ModuleCache::new();
let info1 = cache
.load_module_with_context(&path, &path, None)
.expect("first load must succeed");
assert!(
info1.exports.iter().any(|e| e.name == "before"),
"before-invalidate export should be visible: {:?}",
info1.exports
);
std::fs::write(&path, "pub fn after() { 2 }\n").unwrap();
cache.invalidate(&path);
let info2 = cache
.load_module_with_context(&path, &path, None)
.expect("second load must succeed");
assert!(
info2.exports.iter().any(|e| e.name == "after"),
"after-invalidate export should reflect the new file contents: {:?}",
info2.exports
);
assert!(
!info2.exports.iter().any(|e| e.name == "before"),
"stale pre-invalidate export must NOT linger: {:?}",
info2.exports
);
}
}