mod capabilities;
mod state;
pub use capabilities::*;
pub use state::*;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer};
use crate::document::DocumentStore;
use crate::ide::{
CodeActionService, CodeLensService, CompletionService, DefinitionService, DiagnosticService,
HoverService, IdeContext, ReferencesService, RenameService, SemanticTokensService,
WorkspaceSymbolsService,
};
pub struct MaestroServer {
client: Client,
state: ServerState,
}
impl MaestroServer {
pub fn new(client: Client) -> Self {
Self {
client,
state: ServerState::new(),
}
}
pub fn documents(&self) -> &DocumentStore {
&self.state.documents
}
async fn publish_diagnostics(&self, uri: &Url) {
let diagnostics = DiagnosticService::collect(&self.state, uri);
self.client
.publish_diagnostics(uri.clone(), diagnostics, None)
.await;
}
fn get_block_snippets(&self) -> Vec<CompletionItem> {
vec![
CompletionItem {
label: "template".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("Add template block".to_string()),
insert_text: Some("<template>\n\t$1\n</template>".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
label: "script setup".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("Add script setup block".to_string()),
insert_text: Some("<script setup lang=\"ts\">\n$1\n</script>".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
label: "script".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("Add script block".to_string()),
insert_text: Some(
"<script lang=\"ts\">\nexport default {\n\t$1\n}\n</script>".to_string(),
),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
label: "style scoped".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("Add scoped style block".to_string()),
insert_text: Some("<style scoped>\n$1\n</style>".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
CompletionItem {
label: "style".to_string(),
kind: Some(CompletionItemKind::SNIPPET),
detail: Some("Add style block".to_string()),
insert_text: Some("<style>\n$1\n</style>".to_string()),
insert_text_format: Some(InsertTextFormat::SNIPPET),
..Default::default()
},
]
}
}
#[tower_lsp::async_trait]
impl LanguageServer for MaestroServer {
async fn initialize(&self, _params: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
capabilities: server_capabilities(),
server_info: Some(ServerInfo {
name: "vize-maestro".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
})
}
async fn initialized(&self, _params: InitializedParams) {
self.client
.log_message(MessageType::INFO, "vize_maestro LSP server initialized")
.await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let content = params.text_document.text;
let version = params.text_document.version;
let language_id = params.text_document.language_id;
self.state
.documents
.open(uri.clone(), content.clone(), version, language_id);
self.state.update_virtual_docs(&uri, &content);
self.publish_diagnostics(&uri).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
let version = params.text_document.version;
self.state
.documents
.apply_changes(&uri, params.content_changes, version);
if let Some(doc) = self.state.documents.get(&uri) {
let content = doc.text();
self.state.update_virtual_docs(&uri, &content);
}
self.publish_diagnostics(&uri).await;
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri;
self.publish_diagnostics(&uri).await;
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
self.state.documents.close(&uri);
self.state.remove_virtual_docs(&uri);
self.client.publish_diagnostics(uri, vec![], None).await;
}
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
if let Some(hover) = HoverService::hover(&ctx) {
return Ok(Some(hover));
}
}
Ok(None)
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
if let Some(response) = CompletionService::complete(&ctx) {
return Ok(Some(response));
}
}
let items = self.get_block_snippets();
if items.is_empty() {
Ok(None)
} else {
Ok(Some(CompletionResponse::Array(items)))
}
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
if let Some(response) = DefinitionService::definition(&ctx) {
return Ok(Some(response));
}
}
Ok(None)
}
async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let include_declaration = params.context.include_declaration;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
if let Some(locations) = ReferencesService::references(&ctx, include_declaration) {
return Ok(Some(locations));
}
}
Ok(None)
}
#[allow(deprecated)] async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> Result<Option<DocumentSymbolResponse>> {
let uri = ¶ms.text_document.uri;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let options = vize_atelier_sfc::SfcParseOptions {
filename: uri.path().to_string(),
..Default::default()
};
let Ok(descriptor) = vize_atelier_sfc::parse_sfc(&content, options) else {
return Ok(None);
};
let mut symbols = Vec::new();
if let Some(ref template) = descriptor.template {
symbols.push(DocumentSymbol {
name: "template".to_string(),
kind: SymbolKind::MODULE,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: template.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: template.loc.end_line.saturating_sub(1) as u32,
character: 0,
},
},
selection_range: Range {
start: Position {
line: template.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: template.loc.start_line.saturating_sub(1) as u32,
character: 10,
},
},
detail: template.lang.as_ref().map(|l| l.to_string()),
children: None,
});
}
if let Some(ref script) = descriptor.script {
symbols.push(DocumentSymbol {
name: "script".to_string(),
kind: SymbolKind::MODULE,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: script.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: script.loc.end_line.saturating_sub(1) as u32,
character: 0,
},
},
selection_range: Range {
start: Position {
line: script.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: script.loc.start_line.saturating_sub(1) as u32,
character: 8,
},
},
detail: script.lang.as_ref().map(|l| l.to_string()),
children: None,
});
}
if let Some(ref script_setup) = descriptor.script_setup {
symbols.push(DocumentSymbol {
name: "script setup".to_string(),
kind: SymbolKind::MODULE,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: script_setup.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: script_setup.loc.end_line.saturating_sub(1) as u32,
character: 0,
},
},
selection_range: Range {
start: Position {
line: script_setup.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: script_setup.loc.start_line.saturating_sub(1) as u32,
character: 14,
},
},
detail: script_setup.lang.as_ref().map(|l| l.to_string()),
children: None,
});
}
for (i, style) in descriptor.styles.iter().enumerate() {
let name = if let Some(ref module) = style.module {
format!("style module={}", module)
} else if style.scoped {
"style scoped".to_string()
} else {
format!("style[{}]", i)
};
symbols.push(DocumentSymbol {
name,
kind: SymbolKind::MODULE,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: style.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: style.loc.end_line.saturating_sub(1) as u32,
character: 0,
},
},
selection_range: Range {
start: Position {
line: style.loc.start_line.saturating_sub(1) as u32,
character: 0,
},
end: Position {
line: style.loc.start_line.saturating_sub(1) as u32,
character: 7,
},
},
detail: style.lang.as_ref().map(|l| l.to_string()),
children: None,
});
}
Ok(Some(DocumentSymbolResponse::Nested(symbols)))
}
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
let uri = ¶ms.text_document.uri;
let range = params.range;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, range.start.line, range.start.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
let actions = CodeActionService::code_actions(&ctx, range);
if !actions.is_empty() {
return Ok(Some(actions));
}
}
Ok(None)
}
async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> Result<Option<PrepareRenameResponse>> {
let uri = ¶ms.text_document.uri;
let position = params.position;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
return Ok(RenameService::prepare_rename(&ctx));
}
Ok(None)
}
async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let new_name = ¶ms.new_name;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let offset =
crate::utils::position_to_offset_str(&content, position.line, position.character);
if let Some(ctx) = IdeContext::new(&self.state, uri, offset) {
return Ok(RenameService::rename(&ctx, new_name));
}
Ok(None)
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
let uri = ¶ms.text_document.uri;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
Ok(SemanticTokensService::get_tokens(&content, uri))
}
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
let uri = ¶ms.text_document.uri;
let Some(doc) = self.state.documents.get(uri) else {
return Ok(None);
};
let content = doc.text();
let lenses = CodeLensService::get_lenses(&content, uri);
if lenses.is_empty() {
Ok(None)
} else {
Ok(Some(lenses))
}
}
async fn symbol(
&self,
params: WorkspaceSymbolParams,
) -> Result<Option<Vec<SymbolInformation>>> {
let query = ¶ms.query;
let symbols = WorkspaceSymbolsService::search(&self.state, query);
if symbols.is_empty() {
Ok(None)
} else {
Ok(Some(symbols))
}
}
}