Skip to main content

isml_lsp/
server.rs

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            // `<` opens a tag, `.` reaches a custom attribute, a quote opens an
33            // attribute value or the argument of getCustomPreferenceValue.
34            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(&params),
44        sync::Options::from_settings(params.initialization_options.as_ref()),
45        connection.sender.clone(),
46    );
47    errors::report(
48        workspace_roots(&params),
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    // The writer thread ends when the last sender goes, and the connection
57    // holds one: without this, join() blocks and the process outlives Zed.
58    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(&params);
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    /// Each check answers only for the file types it recognises.
201    fn check(&self, uri: &Url, text: &str) -> Vec<lsp_types::Diagnostic> {
202        // `.custom.` is script, so a JSON description that happens to mention
203        // one is prose, not an access.
204        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    /// The document whose diagnostics are now stale, if any.
234    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(&params.text_document.uri);
256                // Published empty, so what the file used to report disappears.
257                Some(params.text_document.uri)
258            }
259            _ => None,
260        }
261    }
262}
263
264fn workspace_roots(params: &InitializeParams) -> Vec<PathBuf> {
265    if let Some(folders) = &params.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
283/// LSP columns are UTF-16 code units; the reference scanner works in chars.
284fn 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
295/// Split on `\n` rather than `lines()`, so a CRLF file does not drift by one char per line.
296fn 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
307/// The tag completions only make sense in a template; the custom-attribute
308/// ones apply to JavaScript as well.
309fn 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        // An emoji is two UTF-16 units but one char.
329        assert_eq!(char_offset("💾ab", 3), 2);
330    }
331}