typst_analyzer/
backend.rs

1use dashmap::DashMap;
2use serde_json::Value;
3use tower_lsp::jsonrpc::Result;
4use tower_lsp::lsp_types::*;
5use tower_lsp::{Client, LanguageServer};
6use typst_syntax::Source;
7
8use crate::code_actions::handle::TypstCodeActions;
9use crate::completion::handle::TypstCompletion;
10use crate::diagnostics::handle::check_unclosed_delimiters;
11use crate::hints::handle::TypstInlayHints;
12
13#[derive(Debug)]
14pub struct Backend {
15    pub client: Client,
16    pub document: DashMap<String, String>,
17    pub sources: DashMap<String, Source>,
18}
19
20impl Backend {
21    pub fn position_to_offset(&self, text: &str, position: Position) -> Option<usize> {
22        let mut offset = 0;
23        for (line_idx, line) in text.lines().enumerate() {
24            if line_idx == position.line as usize {
25                return Some(offset + position.character as usize);
26            }
27            offset += line.len() + 1; // +1 for the newline character
28        }
29        None
30    }
31}
32
33#[tower_lsp::async_trait]
34impl LanguageServer for Backend {
35    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
36        Ok(InitializeResult {
37            server_info: None,
38            capabilities: ServerCapabilities {
39                inlay_hint_provider: Some(OneOf::Left(true)),
40                hover_provider: Some(HoverProviderCapability::Simple(true)),
41                code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
42                text_document_sync: Some(TextDocumentSyncCapability::Kind(
43                    TextDocumentSyncKind::INCREMENTAL,
44                )),
45                completion_provider: Some(CompletionOptions {
46                    resolve_provider: Some(false),
47                    trigger_characters: Some(vec![".".to_owned()]),
48                    work_done_progress_options: Default::default(),
49                    all_commit_characters: None,
50                    ..Default::default()
51                }),
52                execute_command_provider: Some(ExecuteCommandOptions {
53                    commands: vec!["dummy.do_something".to_owned()],
54                    work_done_progress_options: Default::default(),
55                }),
56                workspace: Some(WorkspaceServerCapabilities {
57                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
58                        supported: Some(true),
59                        change_notifications: Some(OneOf::Left(true)),
60                    }),
61                    file_operations: None,
62                }),
63                ..ServerCapabilities::default()
64            },
65        })
66    }
67
68    async fn initialized(&self, _: InitializedParams) {
69        self.client
70            .log_message(MessageType::INFO, "Language Server initialized!")
71            .await;
72    }
73
74    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
75        let cmp =
76            TypstCompletion::get_completion_items_from_typst(__self, params.text_document_position);
77        Ok(Some(CompletionResponse::Array(
78            cmp, /*handle_completions() */
79        )))
80    }
81
82    async fn hover(&self, _params: HoverParams) -> Result<Option<Hover>> {
83        Ok(Some(Hover {
84            contents: HoverContents::Markup(MarkupContent {
85                kind: MarkupKind::Markdown,
86                value: "# will this get displayed\nyes it will".to_owned(),
87            }),
88            range: None,
89        }))
90    }
91
92    async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
93        let uri = params.text_document.uri.to_string();
94        if let Some(doc) = self.document.get(&uri) {
95            let hints = self.calculate_inlay_hints(&doc);
96            return Ok(Some(hints));
97        }
98        Ok(None)
99    }
100
101    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
102        let uri = params.text_document.uri.to_string();
103        self.client
104            .log_message(MessageType::INFO, "Code action requested")
105            .await;
106
107        if let Some(doc) = self.document.get(&uri) {
108            let content = doc.value();
109            let range = params.range;
110
111            let actions = self.calculate_code_actions(content, range, params.text_document.uri);
112
113            if !actions.is_empty() {
114                self.client
115                    .log_message(MessageType::INFO, "Code actions generated")
116                    .await;
117                return Ok(Some(actions));
118            }
119        }
120        Ok(None)
121    }
122
123    async fn did_open(&self, params: DidOpenTextDocumentParams) {
124        let text_document = params.text_document;
125        self.document
126            .insert(text_document.uri.to_string(), text_document.text.clone());
127        let source = Source::detached(text_document.text.clone());
128        self.sources.insert(text_document.uri.to_string(), source);
129        self.client
130            .log_message(
131                MessageType::INFO,
132                format!("Opened file: {}", text_document.uri),
133            )
134            .await;
135    }
136
137    async fn did_change(&self, params: DidChangeTextDocumentParams) {
138        let uri = params.text_document.uri.to_string();
139        if let Some(mut doc) = self.document.get_mut(&uri) {
140            for change in params.content_changes {
141                // Apply changes incrementally
142                let mut current_text = doc.value().clone();
143
144                // Apply the text edit
145                if let Some(range) = change.range {
146                    let start = range.start;
147                    let end = range.end;
148
149                    let start_idx = self.position_to_offset(&current_text, start).unwrap_or(0);
150                    let end_idx = self
151                        .position_to_offset(&current_text, end)
152                        .unwrap_or(current_text.len());
153
154                    current_text.replace_range(start_idx..end_idx, &change.text);
155                } else {
156                    // If range is None, replace the whole text
157                    current_text = change.text.clone();
158                }
159
160                *doc.value_mut() = current_text;
161            }
162            if let Some(mut source) = self.sources.get_mut(&uri) {
163                source.replace(doc.value());
164            }
165            // Check for unclosed delimiters
166            let diagnostics = check_unclosed_delimiters(&doc);
167            self.client
168                .publish_diagnostics(params.text_document.uri.clone(), diagnostics, None)
169                .await;
170
171            self.client
172                .log_message(MessageType::INFO, "File changed!")
173                .await;
174        }
175    }
176
177    async fn did_save(&self, _: DidSaveTextDocumentParams) {
178        self.client
179            .log_message(MessageType::INFO, "File saved!")
180            .await;
181    }
182
183    async fn did_close(&self, params: DidCloseTextDocumentParams) {
184        let uri = params.text_document.uri.to_string();
185        self.document.remove(&uri);
186        self.client
187            .log_message(MessageType::INFO, format!("Closed file: {}", uri))
188            .await;
189    }
190
191    async fn shutdown(&self) -> Result<()> {
192        Ok(())
193    }
194
195    async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
196        self.client
197            .log_message(MessageType::INFO, "Workspace folders changed!")
198            .await;
199    }
200
201    async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
202        self.client
203            .log_message(MessageType::INFO, "Configuration changed!")
204            .await;
205    }
206
207    async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
208        self.client
209            .log_message(MessageType::INFO, "Watched files have changed!")
210            .await;
211    }
212
213    async fn execute_command(&self, _: ExecuteCommandParams) -> Result<Option<Value>> {
214        self.client
215            .log_message(MessageType::INFO, "Command executed!")
216            .await;
217
218        match self.client.apply_edit(WorkspaceEdit::default()).await {
219            Ok(res) if res.applied => {
220                self.client
221                    .log_message(MessageType::INFO, "Edit applied")
222                    .await
223            }
224            Ok(_) => {
225                self.client
226                    .log_message(MessageType::INFO, "Edit rejected")
227                    .await
228            }
229            Err(err) => self.client.log_message(MessageType::ERROR, err).await,
230        }
231
232        Ok(None)
233    }
234}