Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/mq-lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Language Server Protocol (LSP) implementation for the [mq](https://mqlang.org/)
- 🎯 **Go To Definition**: Navigate to symbol definitions with a single click
- 🔗 **Find References**: Locate all usages of a symbol across your workspace
- 🗂️ **Document Symbols**: Outline view of all symbols in the current file
- 🗃️ **Workspace Symbols**: Search for symbols by name across all loaded files/modules
- 🎨 **Semantic Tokens**: Enhanced syntax highlighting based on semantic analysis
- ✨ **Code Formatting**: Automatic code formatting following mq style guidelines
- 🛠️ **Code Actions**: Quick fixes such as adding a missing `include`/`import` for an unresolved function or module reference
Expand Down
1 change: 1 addition & 0 deletions crates/mq-lsp/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) fn server_capabilities() -> ServerCapabilities {
},
})),
document_symbol_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
Expand Down
53 changes: 30 additions & 23 deletions crates/mq-lsp/src/document_symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use bimap::BiMap;
use tower_lsp_server::ls_types::{DocumentSymbol, DocumentSymbolResponse, Position, Range, SymbolKind, SymbolTag};
use url::Url;

#[allow(deprecated)]
pub(crate) fn response(
hir: Arc<RwLock<mq_hir::Hir>>,
url: Url,
Expand Down Expand Up @@ -32,7 +31,31 @@ pub(crate) fn response(
None
} else {
let is_deprecated = symbol.is_deprecated();
Some(DocumentSymbol {
let range = Range {
start: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
end: Position {
line: text_range.end.line - 1,
character: (text_range.end.column - 1) as u32,
},
};
let selection_range = Range {
start: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
end: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
};

// `deprecated` is superseded by `tags`, but still set for
// clients that predate tag support (LSP < 3.15).
#[allow(deprecated)]
let symbol = DocumentSymbol {
name: name.to_string(),
detail: None,
kind,
Expand All @@ -41,29 +64,13 @@ pub(crate) fn response(
} else {
None
},
range: Range {
start: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
end: Position {
line: text_range.end.line - 1,
character: (text_range.end.column - 1) as u32,
},
},
selection_range: Range {
start: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
end: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
},
range,
selection_range,
children: None,
deprecated: Some(is_deprecated),
})
};

Some(symbol)
}
})
})
Expand Down
1 change: 1 addition & 0 deletions crates/mq-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ pub mod references;
pub mod rename;
pub mod semantic_tokens;
pub mod server;
pub mod workspace_symbol;

pub use server::start;
1 change: 1 addition & 0 deletions crates/mq-lsp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod references;
pub mod rename;
pub mod semantic_tokens;
pub mod server;
pub mod workspace_symbol;

#[derive(Parser, Debug)]
#[command(name = "mq-lsp")]
Expand Down
89 changes: 88 additions & 1 deletion crates/mq-lsp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use url::Url;
use crate::error::LspError;
use crate::{
capabilities, code_action, completions, document_symbol, execute_command, goto_definition, hover, inlay_hints,
references, rename, semantic_tokens,
references, rename, semantic_tokens, workspace_symbol,
};
use tower_lsp_server::{Client, LanguageServer, LspService, Server, jsonrpc, ls_types};

Expand Down Expand Up @@ -147,6 +147,18 @@ impl LanguageServer for Backend {
))
}

async fn symbol(
&self,
params: ls_types::WorkspaceSymbolParams,
) -> jsonrpc::Result<Option<ls_types::WorkspaceSymbolResponse>> {
let source_map_guard = self.source_map.read().unwrap();
Ok(workspace_symbol::response(
Arc::clone(&self.hir),
&params.query,
&source_map_guard,
))
}

async fn hover(&self, params: ls_types::HoverParams) -> jsonrpc::Result<Option<ls_types::Hover>> {
let url = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
Expand Down Expand Up @@ -1200,6 +1212,79 @@ mod tests {
}
}

#[tokio::test]
async fn test_workspace_symbol() {
let (service, _) = LspService::new(|client| Backend {
client,
hir: Arc::new(RwLock::new(mq_hir::Hir::default())),
source_map: RwLock::new(BiMap::new()),
type_env_map: DashMap::new(),
error_map: DashMap::new(),
text_map: DashMap::new(),
config: LspConfig::default(),
});

let backend = service.inner();

let uri_a = Url::parse("file:///a.mq").unwrap();
let code_a = "def test_func(): 1;";
let (nodes_a, _) = mq_lang::parse_recovery(code_a);
let (source_id_a, _) = backend.hir.write().unwrap().add_nodes(uri_a.clone(), &nodes_a);
backend
.source_map
.write()
.unwrap()
.insert(uri_a.to_string(), source_id_a);

let uri_b = Url::parse("file:///b.mq").unwrap();
let code_b = "def other_func(): 2;";
let (nodes_b, _) = mq_lang::parse_recovery(code_b);
let (source_id_b, _) = backend.hir.write().unwrap().add_nodes(uri_b.clone(), &nodes_b);
backend
.source_map
.write()
.unwrap()
.insert(uri_b.to_string(), source_id_b);

let result = backend
.symbol(ls_types::WorkspaceSymbolParams {
query: "test_func".to_string(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await;

assert!(result.is_ok());
let symbols = result.unwrap().unwrap();

if let ls_types::WorkspaceSymbolResponse::Nested(symbols) = symbols {
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "test_func");
} else {
panic!("Expected nested workspace symbol response");
}

let result = backend
.symbol(ls_types::WorkspaceSymbolParams {
query: "func".to_string(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await;

assert!(result.is_ok());
let symbols = result.unwrap().unwrap();

if let ls_types::WorkspaceSymbolResponse::Nested(symbols) = symbols {
let symbol_names: Vec<String> = symbols.iter().map(|s| s.name.clone()).collect();
assert!(symbol_names.contains(&"test_func".to_string()));
assert!(symbol_names.contains(&"other_func".to_string()));
assert_eq!(symbols.len(), 2);
} else {
panic!("Expected nested workspace symbol response");
}
}

#[tokio::test]
async fn test_did_change() {
let (service, _) = LspService::new(|client| Backend {
Expand Down Expand Up @@ -1362,6 +1447,8 @@ mod tests {
assert!(capabilities.completion_provider.is_some());
assert!(capabilities.code_action_provider.is_some());
assert!(capabilities.rename_provider.is_some());
assert!(capabilities.document_symbol_provider.is_some());
assert!(capabilities.workspace_symbol_provider.is_some());

// Test shutdown
let shutdown_result = backend.shutdown().await;
Expand Down
119 changes: 119 additions & 0 deletions crates/mq-lsp/src/workspace_symbol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::str::FromStr;
use std::sync::{Arc, RwLock};

use bimap::BiMap;
use tower_lsp_server::ls_types::{
self, Location, OneOf, Position, Range, SymbolKind, SymbolTag, WorkspaceSymbol, WorkspaceSymbolResponse,
};

pub(crate) fn response(
hir: Arc<RwLock<mq_hir::Hir>>,
query: &str,
source_map: &BiMap<String, mq_hir::SourceId>,
) -> Option<WorkspaceSymbolResponse> {
let hir_guard = hir.read().unwrap();
let query = query.to_lowercase();

let symbols = hir_guard
.symbols()
.filter_map(|(_, symbol)| {
let name = symbol.value.as_ref()?;
if name.is_empty() || (!query.is_empty() && !name.to_lowercase().contains(&query)) {
return None;
}

let kind = match &symbol.kind {
mq_hir::SymbolKind::Function(_) | mq_hir::SymbolKind::Macro(_) => SymbolKind::FUNCTION,
mq_hir::SymbolKind::Variable | mq_hir::SymbolKind::DestructuringBinding => SymbolKind::FIELD,
mq_hir::SymbolKind::String => SymbolKind::STRING,
mq_hir::SymbolKind::Boolean => SymbolKind::BOOLEAN,
mq_hir::SymbolKind::None => SymbolKind::NULL,
_ => return None,
};

let text_range = symbol.source.text_range?;
let source_id = symbol.source.source_id?;
let url = source_map.get_by_right(&source_id)?;
let is_deprecated = symbol.is_deprecated();

Some(WorkspaceSymbol {
name: name.to_string(),
kind,
tags: if is_deprecated {
Some(vec![SymbolTag::DEPRECATED])
} else {
None
},
container_name: None,
location: OneOf::Left(Location {
uri: ls_types::Uri::from_str(url).unwrap(),
range: Range {
start: Position {
line: text_range.start.line - 1,
character: (text_range.start.column - 1) as u32,
},
end: Position {
line: text_range.end.line - 1,
character: (text_range.end.column - 1) as u32,
},
},
}),
data: None,
})
})
.collect::<Vec<_>>();

Some(WorkspaceSymbolResponse::Nested(symbols))
}

#[cfg(test)]
mod tests {
use super::*;
use url::Url;

#[test]
fn test_response_with_empty_hir() {
let hir = Arc::new(RwLock::new(mq_hir::Hir::default()));
let source_map = BiMap::new();
let res = response(hir, "", &source_map);

assert!(matches!(res, Some(WorkspaceSymbolResponse::Nested(symbols)) if symbols.is_empty()));
}

#[test]
fn test_response_matches_across_multiple_sources() {
let mut hir = mq_hir::Hir::default();
let url1 = Url::parse("file:///a.mq").unwrap();
let url2 = Url::parse("file:///b.mq").unwrap();

let (source_id1, _) = hir.add_code(Some(url1.clone()), "def func_a(): 1;");
let (source_id2, _) = hir.add_code(Some(url2.clone()), "def func_b(): 2; | let var_b = 3");

let mut source_map = BiMap::new();
source_map.insert(url1.to_string(), source_id1);
source_map.insert(url2.to_string(), source_id2);

let hir = Arc::new(RwLock::new(hir));

let res = response(hir.clone(), "func", &source_map);
if let Some(WorkspaceSymbolResponse::Nested(symbols)) = res {
assert_eq!(symbols.len(), 2);
assert!(symbols.iter().any(|s| s.name == "func_a"));
assert!(symbols.iter().any(|s| s.name == "func_b"));
assert!(symbols.iter().all(|s| s.kind == SymbolKind::FUNCTION));
} else {
panic!("Expected Nested response");
}

let res = response(hir.clone(), "var_b", &source_map);
if let Some(WorkspaceSymbolResponse::Nested(symbols)) = res {
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "var_b");
} else {
panic!("Expected Nested response");
}

let res = response(hir, "does_not_exist", &source_map);
assert!(matches!(res, Some(WorkspaceSymbolResponse::Nested(symbols)) if symbols.is_empty()));
}
}