1use std::path::PathBuf;
9use std::sync::Arc;
10
11use dashmap::DashMap;
12use tower_lsp::jsonrpc::Result as LspResult;
13use tower_lsp::lsp_types::{
14 DiagnosticOptions, DiagnosticServerCapabilities, DidChangeTextDocumentParams,
15 DidCloseTextDocumentParams, DidOpenTextDocumentParams, GotoDefinitionParams,
16 GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
17 InitializeParams, InitializeResult, InitializedParams, Location, MarkupContent, MarkupKind,
18 MessageType, OneOf, ServerCapabilities, ServerInfo, TextDocumentSyncCapability,
19 TextDocumentSyncKind, Url, WorkDoneProgressOptions,
20};
21use tower_lsp::{Client, LanguageServer};
22
23use crate::diagnostics::{span_to_range, to_lsp_diagnostic};
24use crate::goto_definition::find_definition;
25use crate::hover::hover;
26use crate::pipeline::check_document;
27
28#[derive(Debug)]
33pub struct BockLanguageServer {
34 client: Client,
35 documents: Arc<DashMap<Url, String>>,
36}
37
38impl BockLanguageServer {
39 #[must_use]
41 pub fn new(client: Client) -> Self {
42 Self {
43 client,
44 documents: Arc::new(DashMap::new()),
45 }
46 }
47
48 fn server_capabilities() -> ServerCapabilities {
49 ServerCapabilities {
50 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
51 hover_provider: Some(HoverProviderCapability::Simple(true)),
52 definition_provider: Some(OneOf::Left(true)),
53 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
54 identifier: Some("bock".to_string()),
55 inter_file_dependencies: true,
56 workspace_diagnostics: false,
57 work_done_progress_options: WorkDoneProgressOptions::default(),
58 })),
59 ..ServerCapabilities::default()
60 }
61 }
62
63 async fn publish(&self, uri: Url, version: Option<i32>) {
66 let Some(content) = self.documents.get(&uri).map(|e| e.value().clone()) else {
67 return;
68 };
69
70 let path = url_to_path(&uri);
71 let uri_for_task = uri.clone();
72 let result =
75 tokio::task::spawn_blocking(move || check_document(path, content)).await;
76
77 let result = match result {
78 Ok(r) => r,
79 Err(err) => {
80 self.client
81 .log_message(MessageType::ERROR, format!("check pipeline panicked: {err}"))
82 .await;
83 return;
84 }
85 };
86
87 let source_file = result.source_map.get_file(result.file_id);
88 let lsp_diags: Vec<_> = result
89 .diagnostics
90 .iter()
91 .map(|d| to_lsp_diagnostic(d, &uri_for_task, source_file))
92 .collect();
93
94 self.client
95 .publish_diagnostics(uri_for_task, lsp_diags, version)
96 .await;
97 }
98}
99
100fn url_to_path(uri: &Url) -> PathBuf {
104 uri.to_file_path()
105 .unwrap_or_else(|_| PathBuf::from(uri.path()))
106}
107
108#[tower_lsp::async_trait]
109impl LanguageServer for BockLanguageServer {
110 async fn initialize(&self, _params: InitializeParams) -> LspResult<InitializeResult> {
111 Ok(InitializeResult {
112 capabilities: Self::server_capabilities(),
113 server_info: Some(ServerInfo {
114 name: "bock-lsp".to_string(),
115 version: Some(env!("CARGO_PKG_VERSION").to_string()),
116 }),
117 })
118 }
119
120 async fn initialized(&self, _: InitializedParams) {
121 self.client
122 .log_message(MessageType::INFO, "Bock LSP ready")
123 .await;
124 }
125
126 async fn shutdown(&self) -> LspResult<()> {
127 Ok(())
128 }
129
130 async fn did_open(&self, params: DidOpenTextDocumentParams) {
131 let doc = params.text_document;
132 self.documents.insert(doc.uri.clone(), doc.text);
133 self.publish(doc.uri, Some(doc.version)).await;
134 }
135
136 async fn did_change(&self, params: DidChangeTextDocumentParams) {
137 let uri = params.text_document.uri;
138 if let Some(change) = params.content_changes.into_iter().last() {
142 self.documents.insert(uri.clone(), change.text);
143 }
144 self.publish(uri, Some(params.text_document.version)).await;
145 }
146
147 async fn goto_definition(
148 &self,
149 params: GotoDefinitionParams,
150 ) -> LspResult<Option<GotoDefinitionResponse>> {
151 let uri = params.text_document_position_params.text_document.uri;
152 let pos = params.text_document_position_params.position;
153
154 let Some(content) = self.documents.get(&uri).map(|e| e.value().clone()) else {
155 return Ok(None);
156 };
157
158 let path = url_to_path(&uri);
159 let result = tokio::task::spawn_blocking(move || {
162 find_definition(path, content, pos.line, pos.character)
163 })
164 .await;
165
166 let result = match result {
167 Ok(r) => r,
168 Err(err) => {
169 self.client
170 .log_message(
171 MessageType::ERROR,
172 format!("goto_definition panicked: {err}"),
173 )
174 .await;
175 return Ok(None);
176 }
177 };
178
179 let Some(def) = result else { return Ok(None) };
180
181 let source_file = def.source_map.get_file(def.file_id);
182 let range = span_to_range(def.target, source_file);
183 Ok(Some(GotoDefinitionResponse::Scalar(Location {
184 uri,
185 range,
186 })))
187 }
188
189 async fn hover(&self, params: HoverParams) -> LspResult<Option<Hover>> {
190 let uri = params.text_document_position_params.text_document.uri;
191 let pos = params.text_document_position_params.position;
192
193 let Some(content) = self.documents.get(&uri).map(|e| e.value().clone()) else {
194 return Ok(None);
195 };
196
197 let path = url_to_path(&uri);
198 let result =
201 tokio::task::spawn_blocking(move || hover(path, content, pos.line, pos.character))
202 .await;
203
204 let result = match result {
205 Ok(r) => r,
206 Err(err) => {
207 self.client
208 .log_message(MessageType::ERROR, format!("hover panicked: {err}"))
209 .await;
210 return Ok(None);
211 }
212 };
213
214 let Some(info) = result else { return Ok(None) };
215
216 let source_file = info.source_map.get_file(info.file_id);
217 let range = span_to_range(info.span, source_file);
218 Ok(Some(Hover {
219 contents: HoverContents::Markup(MarkupContent {
220 kind: MarkupKind::Markdown,
221 value: info.contents,
222 }),
223 range: Some(range),
224 }))
225 }
226
227 async fn did_close(&self, params: DidCloseTextDocumentParams) {
228 let uri = params.text_document.uri;
229 self.documents.remove(&uri);
230 self.client.publish_diagnostics(uri, Vec::new(), None).await;
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn capabilities_declare_required_providers() {
241 let caps = BockLanguageServer::server_capabilities();
242
243 match caps.text_document_sync {
244 Some(TextDocumentSyncCapability::Kind(kind)) => {
245 assert_eq!(kind, TextDocumentSyncKind::FULL);
246 }
247 _ => panic!("expected Full text document sync"),
248 }
249
250 assert!(
251 matches!(caps.hover_provider, Some(HoverProviderCapability::Simple(true))),
252 "hover provider must be enabled",
253 );
254
255 assert!(
256 matches!(caps.definition_provider, Some(OneOf::Left(true))),
257 "definition provider must be enabled",
258 );
259
260 assert!(
261 caps.diagnostic_provider.is_some(),
262 "diagnostic provider must be declared for F.1.2",
263 );
264 }
265
266 #[test]
267 fn url_to_path_handles_file_uri() {
268 let url = Url::parse("file:///tmp/foo.bock").unwrap();
269 assert_eq!(url_to_path(&url), PathBuf::from("/tmp/foo.bock"));
270 }
271
272 #[test]
273 fn url_to_path_falls_back_for_non_file_scheme() {
274 let url = Url::parse("untitled:Untitled-1").unwrap();
275 let path = url_to_path(&url);
276 assert!(!path.as_os_str().is_empty());
277 }
278}