cooklang_language_server/
backend.rs1use std::path::PathBuf;
2
3use tower_lsp::jsonrpc::Result;
4use tower_lsp::lsp_types::*;
5use tower_lsp::{Client, LanguageServer};
6
7use crate::completion;
8use crate::diagnostics;
9use crate::hover;
10use crate::semantic_tokens;
11use crate::state::ServerState;
12use crate::symbols;
13
14pub struct Backend {
15 client: Client,
16 state: ServerState,
17 workspace_root: std::sync::RwLock<Option<PathBuf>>,
19}
20
21impl Backend {
22 pub fn new(client: Client) -> Self {
23 Self {
24 client,
25 state: ServerState::new(),
26 workspace_root: std::sync::RwLock::new(None),
27 }
28 }
29
30 fn load_aisle_config(&self) {
32 if let Ok(guard) = self.workspace_root.read() {
33 if let Some(ref path) = *guard {
34 self.state.load_aisle_config(path);
35 }
36 }
37 }
38
39 async fn publish_diagnostics(&self, uri: &Url) {
40 let diagnostics = if let Some(doc) = self.state.get_document(uri) {
41 diagnostics::get_diagnostics(&doc)
42 } else {
43 vec![]
44 };
45
46 self.client
47 .publish_diagnostics(uri.clone(), diagnostics, None)
48 .await;
49 }
50}
51
52#[tower_lsp::async_trait]
53impl LanguageServer for Backend {
54 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
55 let workspace_path = params
57 .workspace_folders
58 .as_ref()
59 .and_then(|folders| folders.first())
60 .and_then(|folder| folder.uri.to_file_path().ok())
61 .or_else(|| {
62 #[allow(deprecated)]
63 params
64 .root_uri
65 .as_ref()
66 .and_then(|uri| uri.to_file_path().ok())
67 })
68 .or_else(|| {
69 #[allow(deprecated)]
70 params.root_path.as_ref().map(PathBuf::from)
71 });
72
73 if let Some(path) = workspace_path {
74 tracing::info!("Workspace root: {:?}", path);
75 if let Ok(mut guard) = self.workspace_root.write() {
76 *guard = Some(path);
77 }
78 }
79
80 Ok(InitializeResult {
81 capabilities: ServerCapabilities {
82 text_document_sync: Some(TextDocumentSyncCapability::Options(
83 TextDocumentSyncOptions {
84 open_close: Some(true),
85 change: Some(TextDocumentSyncKind::FULL),
86 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
87 include_text: Some(false),
88 })),
89 ..Default::default()
90 },
91 )),
92 completion_provider: Some(CompletionOptions {
93 trigger_characters: Some(vec![
94 "@".into(),
95 "#".into(),
96 "~".into(),
97 "%".into(),
98 "{".into(),
99 ".".into(),
100 "/".into(),
101 ]),
102 resolve_provider: Some(false),
103 ..Default::default()
104 }),
105 hover_provider: Some(HoverProviderCapability::Simple(true)),
106 document_symbol_provider: Some(OneOf::Left(true)),
107 semantic_tokens_provider: Some(semantic_tokens::capabilities()),
108 workspace: Some(WorkspaceServerCapabilities {
109 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
110 supported: Some(true),
111 change_notifications: Some(OneOf::Left(true)),
112 }),
113 ..Default::default()
114 }),
115 ..Default::default()
116 },
117 server_info: Some(ServerInfo {
118 name: "cooklang-language-server".into(),
119 version: Some(env!("CARGO_PKG_VERSION").into()),
120 }),
121 })
122 }
123
124 async fn initialized(&self, _: InitializedParams) {
125 tracing::info!("Cooklang LSP initialized");
126
127 self.load_aisle_config();
129
130 self.client
131 .log_message(MessageType::INFO, "Cooklang Language Server initialized")
132 .await;
133 }
134
135 async fn shutdown(&self) -> Result<()> {
136 tracing::info!("Cooklang LSP shutting down");
137 Ok(())
138 }
139
140 async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
141 let new_root = params
144 .event
145 .added
146 .first()
147 .and_then(|folder| folder.uri.to_file_path().ok());
148
149 if let Ok(mut guard) = self.workspace_root.write() {
150 match (&new_root, guard.as_ref()) {
151 (Some(new), Some(current)) if new == current => return,
152 (None, None) => return,
153 _ => {}
154 }
155 tracing::info!("Workspace root changed to: {:?}", new_root);
156 *guard = new_root;
157 }
158
159 self.load_aisle_config();
161 }
162
163 async fn did_open(&self, params: DidOpenTextDocumentParams) {
164 let uri = params.text_document.uri;
165 let version = params.text_document.version;
166 let content = params.text_document.text;
167
168 tracing::debug!("Document opened: {}", uri);
169 self.state.open_document(uri.clone(), version, content);
170 self.publish_diagnostics(&uri).await;
171 }
172
173 async fn did_change(&self, params: DidChangeTextDocumentParams) {
174 let uri = params.text_document.uri;
175 let version = params.text_document.version;
176
177 if let Some(change) = params.content_changes.into_iter().last() {
178 tracing::debug!("Document changed: {}", uri);
179 self.state.update_document(&uri, version, change.text);
180 self.publish_diagnostics(&uri).await;
181 }
182 }
183
184 async fn did_save(&self, params: DidSaveTextDocumentParams) {
185 tracing::debug!("Document saved: {}", params.text_document.uri);
186 self.publish_diagnostics(¶ms.text_document.uri).await;
187 }
188
189 async fn did_close(&self, params: DidCloseTextDocumentParams) {
190 let uri = params.text_document.uri;
191 tracing::debug!("Document closed: {}", uri);
192 self.state.close_document(&uri);
193 self.client.publish_diagnostics(uri, vec![], None).await;
194 }
195
196 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
197 let uri = ¶ms.text_document_position.text_document.uri;
198 let workspace_root = self
199 .workspace_root
200 .read()
201 .ok()
202 .and_then(|guard| guard.clone())
203 .or_else(|| {
204 uri.to_file_path()
207 .ok()
208 .and_then(|p| p.parent().map(|p| p.to_path_buf()))
209 });
210
211 let response = if let Some(doc) = self.state.get_document(uri) {
212 completion::get_completions(&doc, ¶ms, &self.state, workspace_root.as_deref())
213 } else {
214 None
215 };
216
217 Ok(response)
218 }
219
220 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
221 let uri = ¶ms.text_document_position_params.text_document.uri;
222
223 let response = if let Some(doc) = self.state.get_document(uri) {
224 hover::get_hover(&doc, ¶ms)
225 } else {
226 None
227 };
228
229 Ok(response)
230 }
231
232 async fn document_symbol(
233 &self,
234 params: DocumentSymbolParams,
235 ) -> Result<Option<DocumentSymbolResponse>> {
236 let uri = ¶ms.text_document.uri;
237
238 let response = if let Some(doc) = self.state.get_document(uri) {
239 symbols::get_document_symbols(&doc)
240 } else {
241 None
242 };
243
244 Ok(response)
245 }
246
247 async fn semantic_tokens_full(
248 &self,
249 params: SemanticTokensParams,
250 ) -> Result<Option<SemanticTokensResult>> {
251 let uri = ¶ms.text_document.uri;
252
253 let tokens = if let Some(doc) = self.state.get_document(uri) {
254 semantic_tokens::get_semantic_tokens(&doc)
255 } else {
256 vec![]
257 };
258
259 Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
260 result_id: None,
261 data: tokens,
262 })))
263 }
264}