1use std::collections::HashMap;
2use std::error::Error;
3use std::path::{Path, PathBuf};
4
5use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
6use lsp_types::notification::{
7 DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification,
8 PublishDiagnostics,
9};
10use lsp_types::request::{Completion, GotoDefinition, HoverRequest, Request as RequestTrait};
11use lsp_types::{
12 CompletionList, CompletionOptions, CompletionParams, CompletionResponse, GotoDefinitionParams,
13 GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
14 InitializeParams, Location, MarkupContent, MarkupKind, OneOf, Position,
15 PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability,
16 TextDocumentSyncKind, Url,
17};
18
19use crate::complete::{self, Completer};
20use crate::metadata::Metadata;
21use crate::workspace::Workspace;
22use crate::{diagnose, errors, hover, live, reference, resolve, sync, validate};
23
24pub fn serve() -> Result<(), Box<dyn Error + Sync + Send>> {
25 let (connection, io_threads) = Connection::stdio();
26
27 let capabilities = serde_json::to_value(ServerCapabilities {
28 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
29 definition_provider: Some(OneOf::Left(true)),
30 hover_provider: Some(HoverProviderCapability::Simple(true)),
31 completion_provider: Some(CompletionOptions {
32 trigger_characters: Some(["<", " ", ".", "\"", "'"].map(str::to_string).to_vec()),
35 ..Default::default()
36 }),
37 ..Default::default()
38 })?;
39
40 let initialize_params = connection.initialize(capabilities)?;
41 let params: InitializeParams = serde_json::from_value(initialize_params)?;
42 sync::report(
43 workspace_roots(¶ms),
44 sync::Options::from_settings(params.initialization_options.as_ref()),
45 connection.sender.clone(),
46 );
47 errors::report(
48 workspace_roots(¶ms),
49 errors::Options::from_settings(params.initialization_options.as_ref()),
50 connection.sender.clone(),
51 );
52
53 let server = Server::new(params);
54 server.run(&connection)?;
55
56 drop(connection);
59 io_threads.join()?;
60 Ok(())
61}
62
63pub struct Server {
64 workspace: Workspace,
65 metadata: Metadata,
66 documents: HashMap<Url, String>,
67}
68
69impl Server {
70 fn new(params: InitializeParams) -> Self {
71 let roots = workspace_roots(¶ms);
72 let settings = params.initialization_options.unwrap_or_default();
73 Server {
74 workspace: Workspace::scan(&roots, &settings),
75 metadata: Metadata::scan(&roots),
76 documents: HashMap::new(),
77 }
78 }
79
80 fn run(mut self, connection: &Connection) -> Result<(), Box<dyn Error + Sync + Send>> {
81 for message in &connection.receiver {
82 match message {
83 Message::Request(request) => {
84 if connection.handle_shutdown(&request)? {
85 return Ok(());
86 }
87 let response = self.respond(request);
88 connection.sender.send(Message::Response(response))?;
89 }
90 Message::Notification(notification) => {
91 if let Some(uri) = self.apply(notification) {
92 connection.sender.send(self.published(uri))?;
93 }
94 }
95 Message::Response(_) => {}
96 }
97 }
98 Ok(())
99 }
100
101 fn respond(&self, request: Request) -> Response {
102 let id = request.id.clone();
103 let answer = match request.method.as_str() {
104 GotoDefinition::METHOD => cast::<GotoDefinition>(request)
105 .ok()
106 .and_then(|(_, params)| serde_json::to_value(self.definition(params)?).ok()),
107 Completion::METHOD => cast::<Completion>(request)
108 .ok()
109 .and_then(|(_, params)| serde_json::to_value(self.completion(params)?).ok()),
110 HoverRequest::METHOD => cast::<HoverRequest>(request)
111 .ok()
112 .and_then(|(_, params)| serde_json::to_value(self.hover(params)?).ok()),
113 _ => None,
114 };
115 Response::new_ok(id, answer.unwrap_or(serde_json::Value::Null))
116 }
117
118 fn completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
119 let position = params.text_document_position;
120 let uri = position.text_document.uri;
121 let text = self.documents.get(&uri)?;
122 let offset = char_offset_at(text, position.position)?;
123 let file = uri.to_file_path().ok()?;
124
125 let context = complete::context_at(text, offset, is_isml(&uri))?;
126 let completer = Completer {
127 workspace: &self.workspace,
128 metadata: &self.metadata,
129 text,
130 file: &file,
131 };
132 let items = completer.items(&context, position.position);
133 (!items.is_empty()).then_some(CompletionResponse::List(CompletionList {
134 is_incomplete: false,
135 items,
136 }))
137 }
138
139 fn hover(&self, params: HoverParams) -> Option<Hover> {
140 let position = params.text_document_position_params;
141 let uri = position.text_document.uri;
142 let file = uri.to_file_path().ok()?;
143 let text = self.documents.get(&uri)?;
144 let line = text.lines().nth(position.position.line as usize)?;
145 let column = char_offset(line, position.position.character as usize);
146
147 let docs = hover::member_markdown(line, column, text)
148 .or_else(|| self.reference_hover(&uri, position.position, &file));
149 let value = match (live::markdown(&file, line, column), docs) {
150 (Some(live), Some(docs)) => format!(
151 "{live}
152---
153
154{docs}"
155 ),
156 (live, docs) => live.or(docs)?,
157 };
158 Some(Hover {
159 contents: HoverContents::Markup(MarkupContent {
160 kind: MarkupKind::Markdown,
161 value,
162 }),
163 range: None,
164 })
165 }
166
167 fn reference_hover(&self, uri: &Url, position: Position, file: &Path) -> Option<String> {
168 let reference = self.reference_at(uri, position)?;
169 if let Some(text) = hover::module_markdown(&reference) {
170 return Some(text);
171 }
172 let route = hover::route_of(&reference, file)?;
173 let chains = hover::chains(&route, &self.workspace);
174 hover::markdown(&route, &chains, file)
175 }
176
177 fn reference_at(&self, uri: &Url, position: Position) -> Option<reference::Reference> {
178 let text = self.documents.get(uri)?;
179 let line = text.lines().nth(position.line as usize)?;
180 reference::at_cursor(line, char_offset(line, position.character as usize))
181 }
182
183 fn published(&self, uri: Url) -> Message {
184 let diagnostics = self
185 .documents
186 .get(&uri)
187 .map(|text| self.check(&uri, text))
188 .unwrap_or_default();
189 let params = PublishDiagnosticsParams {
190 uri,
191 diagnostics,
192 version: None,
193 };
194 Message::Notification(lsp_server::Notification::new(
195 PublishDiagnostics::METHOD.to_string(),
196 params,
197 ))
198 }
199
200 fn check(&self, uri: &Url, text: &str) -> Vec<lsp_types::Diagnostic> {
202 let path = uri.path();
205 let mut found = match path.ends_with(".js") || path.ends_with(".isml") {
206 true => diagnose::diagnostics(text, &self.metadata),
207 false => Vec::new(),
208 };
209 if let Ok(file) = uri.to_file_path() {
210 found.extend(validate::diagnostics(&file, text, &self.workspace));
211 }
212 found
213 }
214
215 fn definition(&self, params: GotoDefinitionParams) -> Option<GotoDefinitionResponse> {
216 let position = params.text_document_position_params;
217 let uri = position.text_document.uri;
218 let file = uri.to_file_path().ok()?;
219 let reference = self.reference_at(&uri, position.position)?;
220
221 let locations: Vec<Location> = resolve::resolve(&reference, &file, &self.workspace)
222 .into_iter()
223 .filter_map(|hit| {
224 let target = Url::from_file_path(&hit.path).ok()?;
225 let start = Position::new(hit.line, 0);
226 Some(Location::new(target, Range::new(start, start)))
227 })
228 .collect();
229
230 (!locations.is_empty()).then_some(GotoDefinitionResponse::Array(locations))
231 }
232
233 fn apply(&mut self, notification: lsp_server::Notification) -> Option<Url> {
235 match notification.method.as_str() {
236 DidOpenTextDocument::METHOD => {
237 let params: lsp_types::DidOpenTextDocumentParams =
238 serde_json::from_value(notification.params).ok()?;
239 let uri = params.text_document.uri;
240 self.documents
241 .insert(uri.clone(), params.text_document.text);
242 Some(uri)
243 }
244 DidChangeTextDocument::METHOD => {
245 let params: lsp_types::DidChangeTextDocumentParams =
246 serde_json::from_value(notification.params).ok()?;
247 let change = params.content_changes.into_iter().next_back()?;
248 let uri = params.text_document.uri;
249 self.documents.insert(uri.clone(), change.text);
250 Some(uri)
251 }
252 DidCloseTextDocument::METHOD => {
253 let params: lsp_types::DidCloseTextDocumentParams =
254 serde_json::from_value(notification.params).ok()?;
255 self.documents.remove(¶ms.text_document.uri);
256 Some(params.text_document.uri)
258 }
259 _ => None,
260 }
261 }
262}
263
264fn workspace_roots(params: &InitializeParams) -> Vec<PathBuf> {
265 if let Some(folders) = ¶ms.workspace_folders {
266 let roots: Vec<PathBuf> = folders
267 .iter()
268 .filter_map(|folder| folder.uri.to_file_path().ok())
269 .collect();
270 if !roots.is_empty() {
271 return roots;
272 }
273 }
274 #[allow(deprecated)]
275 params
276 .root_uri
277 .as_ref()
278 .and_then(|uri| uri.to_file_path().ok())
279 .into_iter()
280 .collect()
281}
282
283fn char_offset(line: &str, utf16_column: usize) -> usize {
285 let mut utf16 = 0;
286 for (index, c) in line.chars().enumerate() {
287 if utf16 >= utf16_column {
288 return index;
289 }
290 utf16 += c.len_utf16();
291 }
292 line.chars().count()
293}
294
295fn char_offset_at(text: &str, position: Position) -> Option<usize> {
297 let mut offset = 0;
298 for (number, line) in text.split('\n').enumerate() {
299 if number == position.line as usize {
300 return Some(offset + char_offset(line, position.character as usize));
301 }
302 offset += line.chars().count() + 1;
303 }
304 None
305}
306
307fn is_isml(uri: &Url) -> bool {
310 uri.path().ends_with(".isml")
311}
312
313fn cast<R>(request: Request) -> Result<(RequestId, R::Params), ExtractError<Request>>
314where
315 R: RequestTrait,
316 R::Params: serde::de::DeserializeOwned,
317{
318 request.extract(R::METHOD)
319}
320
321#[cfg(test)]
322mod tests {
323 use super::char_offset;
324
325 #[test]
326 fn maps_utf16_columns_to_char_indices() {
327 assert_eq!(char_offset("abc", 2), 2);
328 assert_eq!(char_offset("💾ab", 3), 2);
330 }
331}