char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somexperiment: use native typescript lsp supportc7a9569

main
21.9 KiB563 linesraw
1use crate::{
2    Documents, ProjectService, Result,
3    project::{Project, canonical_uri},
4    wire,
5};
6use serde_json::{Value, json};
7use std::{
8    collections::{BTreeMap, BTreeSet, HashSet},
9    path::PathBuf,
10    sync::{Arc, Mutex},
11};
12use tokio::{
13    io::{AsyncRead, AsyncWrite, BufReader},
14    sync::{Notify, mpsc},
15};
16use tokio_util::sync::CancellationToken;
17
18struct Session {
19    service: Box<dyn ProjectService>,
20    documents: Documents,
21    failure: Option<String>,
22    binary: PathBuf,
23    project: Option<Project>,
24    initialized: bool,
25    shutdown: bool,
26    client: Value,
27    preferences: Value,
28    capabilities: Value,
29    methods: BTreeSet<String>,
30    open: BTreeMap<String, i64>,
31    published: BTreeSet<String>,
32    dirty: bool,
33    refresh_request: u64,
34}
35impl Session {
36    async fn rebuild(&mut self) -> Result<()> {
37        self.service.changed();
38        self.dirty = false;
39        if let Some(project) = &mut self.project {
40            // Teardown still kills/reaps a failed backend; allow the next generation
41            // to recover rather than retaining a permanently failed connection.
42            if let Err(error) = project.close().await {
43                eprintln!("LSP backend cleanup: {error}");
44            }
45        }
46        self.project = None;
47        let result = async {
48            let input = self.service.refresh(self.documents.clone()).await?;
49            let project = Project::create(&self.binary, input).await?;
50            let capabilities = project.capabilities(&self.client).await?;
51            let methods: BTreeSet<_> = capabilities.methods.into_iter().collect();
52            if self.initialized
53                && (capabilities.capabilities != self.capabilities || methods != self.methods)
54            {
55                return Err("TypeScript backend protocol changed during session".into());
56            }
57            if !self.initialized {
58                self.capabilities = capabilities.capabilities;
59                self.methods = methods;
60            }
61            Ok::<_, crate::Error>(project)
62        }
63        .await;
64        match result {
65            Ok(project) => {
66                self.project = Some(project);
67                self.failure = None;
68                Ok(())
69            }
70            Err(error) => {
71                self.failure = Some(error.to_string());
72                Err(error)
73            }
74        }
75    }
76    async fn publish(&mut self, writer: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
77        let mut current: BTreeSet<String> = self.open.keys().cloned().collect();
78        if let Some(project) = &self.project {
79            current.extend(project.error_uris().cloned());
80        }
81        for uri in current.union(&self.published) {
82            let diagnostics = if !current.contains(uri) {
83                json!([])
84            } else if let Some(project) = &mut self.project {
85                match project
86                    .diagnostics(uri, &self.client, &self.preferences)
87                    .await
88                {
89                    Ok(diagnostics) => diagnostics,
90                    Err(error) => {
91                        self.failure = Some(error.to_string());
92                        json!([{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},
93                            "severity":1,"source":"avril","message":error.to_string()}])
94                    }
95                }
96            } else {
97                json!([{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},
98                    "severity":1,"source":"avril","message":self.failure.as_deref().unwrap_or("Project unavailable")}])
99            };
100            let mut params = json!({"uri": uri, "diagnostics": diagnostics});
101            if let Some(version) = self.open.get(uri) {
102                params["version"] = json!(version);
103            }
104            wire::write(
105                writer,
106                json!({"method": "textDocument/publishDiagnostics", "params": params}),
107            )
108            .await?;
109        }
110        self.published = current;
111        for feature in ["codeLens", "inlayHint", "semanticTokens"] {
112            if self
113                .client
114                .pointer(&format!("/workspace/{feature}/refreshSupport"))
115                .and_then(Value::as_bool)
116                == Some(true)
117            {
118                self.refresh_request += 1;
119                wire::write(
120                    writer,
121                    json!({
122                        "id":format!("avril-refresh-{}",self.refresh_request),
123                        "method":format!("workspace/{feature}/refresh"),
124                    }),
125                )
126                .await?;
127            }
128        }
129        Ok(())
130    }
131    async fn handle(
132        &mut self,
133        method: &str,
134        params: Value,
135        writer: &mut (impl AsyncWrite + Unpin),
136    ) -> Result<Value> {
137        match method {
138            "initialize" => {
139                if self.initialized {
140                    return Err("Already initialized".into());
141                }
142                self.client = params.get("capabilities").cloned().unwrap_or(json!({}));
143                self.preferences = params
144                    .pointer("/initializationOptions/preferences")
145                    .cloned()
146                    .unwrap_or(json!({}));
147                if !self.preferences.is_object() {
148                    return Err(wire::RpcError::invalid("preferences must be an object").into());
149                }
150                // Resolve data is bound independently for every completion item.
151                if let Some(list) = self
152                    .client
153                    .pointer_mut("/textDocument/completion/completionList")
154                    .and_then(Value::as_object_mut)
155                {
156                    list.remove("itemDefaults");
157                }
158                self.rebuild().await?;
159                self.initialized = true;
160                let mut capabilities = self.capabilities.clone();
161                capabilities["positionEncoding"] = json!("utf-16");
162                capabilities["textDocumentSync"] = json!({"openClose":true, "change":1});
163                return Ok(json!({
164                    "capabilities": capabilities,
165                    "serverInfo": {"name": "avril-lsp", "version": env!("CARGO_PKG_VERSION")}
166                }));
167            }
168            "initialized" => return Ok(Value::Null),
169            "workspace/didChangeWatchedFiles" | "workspace/didChangeConfiguration" => {
170                if method == "workspace/didChangeConfiguration"
171                    && let Some(preferences) = params.pointer("/settings/preferences")
172                {
173                    if !preferences.is_object() {
174                        return Err(wire::RpcError::invalid("preferences must be an object").into());
175                    }
176                    self.preferences = preferences.clone();
177                }
178                if self.initialized && !self.shutdown {
179                    self.dirty = true;
180                }
181                return Ok(Value::Null);
182            }
183            "shutdown" => {
184                self.shutdown = true;
185                let cleanup = if let Some(project) = &mut self.project {
186                    project.close().await
187                } else {
188                    Ok(())
189                };
190                self.project = None;
191                cleanup?;
192                return Ok(Value::Null);
193            }
194            _ => {}
195        }
196        let uri = params
197            .pointer("/textDocument/uri")
198            .and_then(Value::as_str)
199            .map(canonical_uri)
200            .transpose()?;
201        match method {
202            "textDocument/didOpen" => {
203                let uri = uri.ok_or("Missing document URI")?;
204                if !uri.starts_with("file:")
205                    && !self.project.as_ref().is_some_and(|p| p.contains(&uri))
206                {
207                    return Ok(Value::Null);
208                }
209                let text = params["textDocument"]["text"]
210                    .as_str()
211                    .ok_or("Invalid didOpen text")?;
212                let version = params["textDocument"]["version"]
213                    .as_i64()
214                    .ok_or("Invalid document version")?;
215                if self.open.contains_key(&uri) {
216                    return Err("Document already open".into());
217                }
218                self.open.insert(uri.clone(), version);
219                if uri.starts_with("file:")
220                    && !self.project.as_ref().is_some_and(|p| p.is_read_only(&uri))
221                {
222                    self.documents.insert(uri, text.into());
223                    self.dirty = true;
224                }
225                Ok(Value::Null)
226            }
227            "textDocument/didChange" => {
228                let uri = uri.ok_or("Missing document URI")?;
229                let version = params["textDocument"]["version"]
230                    .as_i64()
231                    .ok_or("Invalid document version")?;
232                if self.open.get(&uri).is_none_or(|old| version <= *old) {
233                    return Ok(Value::Null);
234                }
235                let changes = params["contentChanges"]
236                    .as_array()
237                    .ok_or("Invalid document changes")?;
238                if changes.is_empty()
239                    || changes
240                        .iter()
241                        .any(|c| c.get("range").is_some() || !c["text"].is_string())
242                {
243                    return Err("Only full-document changes are supported".into());
244                }
245                let text = changes
246                    .last()
247                    .and_then(|c| c["text"].as_str())
248                    .ok_or("Missing change text")?;
249                self.open.insert(uri.clone(), version);
250                if uri.starts_with("file:")
251                    && !self.project.as_ref().is_some_and(|p| p.is_read_only(&uri))
252                {
253                    self.documents.insert(uri, text.into());
254                    self.dirty = true;
255                }
256                Ok(Value::Null)
257            }
258            "textDocument/didClose" => {
259                let uri = uri.ok_or("Missing document URI")?;
260                if self.open.remove(&uri).is_none() {
261                    return Ok(Value::Null);
262                }
263                wire::write(
264                    writer,
265                    json!({
266                        "method": "textDocument/publishDiagnostics",
267                        "params": {"uri": uri, "diagnostics": []}
268                    }),
269                )
270                .await?;
271                if self.documents.remove(&uri).is_some() {
272                    self.dirty = true;
273                }
274                Ok(Value::Null)
275            }
276            "textDocument/diagnostic" => {
277                let uri = uri.ok_or("Missing document URI")?;
278                let items = self
279                    .project
280                    .as_mut()
281                    .ok_or("Project unavailable")?
282                    .diagnostics(&uri, &self.client, &self.preferences)
283                    .await?;
284                Ok(json!({"kind":"full","items":items}))
285            }
286            "avril/readDocument" => {
287                let project = self.project.as_mut().ok_or("Project unavailable")?;
288                let uri = canonical_uri(params["uri"].as_str().ok_or("Missing document URI")?)?;
289                if uri.starts_with("avril-builtin:/typescript/") {
290                    project
291                        .language_service(
292                            method,
293                            json!({"textDocument":{"uri":uri}}),
294                            &self.client,
295                            &self.preferences,
296                        )
297                        .await
298                } else {
299                    Ok(json!(project.read_document(&uri)?))
300                }
301            }
302            _ if self.methods.contains(method) => {
303                self.project
304                    .as_mut()
305                    .ok_or("Project unavailable")?
306                    .language_service(method, params, &self.client, &self.preferences)
307                    .await
308            }
309            _ => Err(format!("Unsupported method: {method}").into()),
310        }
311    }
312}
313
314/// Owns a workspace session and its native backend. Acquisition and resolution
315/// remain in the host's project service; no guest code is executed here.
316pub async fn serve<R, W>(
317    reader: R,
318    mut writer: W,
319    binary: PathBuf,
320    service: Box<dyn ProjectService>,
321) -> Result<()>
322where
323    R: AsyncRead + Unpin + Send + 'static,
324    W: AsyncWrite + Unpin,
325{
326    let mut session = Session {
327        service,
328        documents: Documents::new(),
329        failure: None,
330        binary,
331        project: None,
332        initialized: false,
333        shutdown: false,
334        client: json!({}),
335        preferences: json!({}),
336        capabilities: json!({}),
337        methods: BTreeSet::new(),
338        open: BTreeMap::new(),
339        published: BTreeSet::new(),
340        dirty: false,
341        refresh_request: 0,
342    };
343    #[cfg(unix)]
344    let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
345    let interrupt = async {
346        #[cfg(unix)]
347        {
348            tokio::select! {
349                result = tokio::signal::ctrl_c() => result,
350                _ = terminate.recv() => Ok(()),
351            }
352        }
353        #[cfg(not(unix))]
354        {
355            tokio::signal::ctrl_c().await
356        }
357    };
358    tokio::pin!(interrupt);
359    let disconnected = CancellationToken::new();
360    let pending = Arc::new(Mutex::new(HashSet::<String>::new()));
361    let cancelled = Arc::new(Mutex::new(HashSet::<String>::new()));
362    let cancellation = Arc::new(Notify::new());
363    let (tx, mut rx) = mpsc::channel(64);
364    let reader_task = tokio::spawn({
365        let disconnected = disconnected.clone();
366        let pending = pending.clone();
367        let cancelled = cancelled.clone();
368        let cancellation = cancellation.clone();
369        async move {
370            let mut reader = BufReader::new(reader);
371            loop {
372                let message = match wire::read(&mut reader).await {
373                    Ok(Some(message)) => message,
374                    Ok(None) => {
375                        disconnected.cancel();
376                        return;
377                    }
378                    Err(error) => {
379                        let _ = tx.send(Err(error)).await;
380                        disconnected.cancel();
381                        return;
382                    }
383                };
384                if message["method"] == "$/cancelRequest" {
385                    let key = message["params"]["id"].to_string();
386                    if pending.lock().unwrap().contains(&key) {
387                        cancelled.lock().unwrap().insert(key);
388                        cancellation.notify_one();
389                    }
390                    continue;
391                }
392                if message["method"].is_string()
393                    && let Some(id) = message.get("id")
394                {
395                    pending.lock().unwrap().insert(id.to_string());
396                }
397                let exit = message["method"] == "exit";
398                if tx.send(Ok(message)).await.is_err() {
399                    return;
400                }
401                // Do not start a blocking stdin read after an explicit exit.
402                if exit {
403                    return;
404                }
405            }
406        }
407    });
408    let mut polling = tokio::time::interval(std::time::Duration::from_millis(100));
409    polling.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
410    let debounce = tokio::time::sleep(std::time::Duration::from_millis(100));
411    tokio::pin!(debounce);
412    let result: Result<()> = async {
413        loop {
414            let message = tokio::select! {
415                biased;
416                message = rx.recv() => match message {
417                    Some(message) => message?,
418                    None => return if session.shutdown { Ok(()) } else {
419                        Err("Editor connection closed without shutdown".into())
420                    }
421                },
422                _ = &mut debounce, if session.dirty && session.initialized && !session.shutdown => {
423                    let rebuilt = session.rebuild().await;
424                    session.publish(&mut writer).await?;
425                    if let Err(error) = rebuilt {
426                        wire::write(&mut writer, json!({"method":"window/logMessage","params":{"type":1,"message":error.to_string()}})).await?;
427                    }
428                    continue;
429                },
430                _ = polling.tick(), if session.initialized && !session.shutdown => {
431                    if session.service.changed() {
432                        session.dirty = true;
433                        debounce.as_mut().reset(tokio::time::Instant::now() + std::time::Duration::from_millis(100));
434                    }
435                    continue;
436                },
437                _ = disconnected.cancelled() => return Err("Editor disconnected".into()),
438                _ = &mut interrupt => return Err("LSP interrupted".into()),
439            };
440            let Some(method) = message["method"].as_str() else {
441                continue;
442            };
443            if message.get("id").is_none() && matches!(method, "$/setTrace" | "textDocument/didSave") {
444                continue;
445            }
446            if method == "exit" {
447                return if session.shutdown {
448                    Ok(())
449                } else {
450                    Err("LSP exit without shutdown".into())
451                };
452            }
453            let id = message.get("id").cloned();
454            let key = id.as_ref().map(Value::to_string);
455            let cancel = || {
456                key.as_ref()
457                    .is_some_and(|k| cancelled.lock().unwrap().contains(k))
458            };
459            let mut error_code = -32603;
460            let query = id.is_some() && session.initialized && !session.shutdown && session.methods.contains(method);
461            let recovery = if query
462                && !cancel()
463                && (session.dirty
464                    || session.project.as_ref().is_none_or(Project::failed))
465            {
466                let result = session.rebuild().await;
467                if result.is_ok() {
468                    session.publish(&mut writer).await?;
469                }
470                Some(result)
471            } else {
472                None
473            };
474            let response = if let Some(Err(error)) = recovery {
475                Err(error)
476            } else if cancel() {
477                error_code = -32800;
478                Err("Request cancelled".into())
479            } else if (!session.initialized && method != "initialize") || session.shutdown {
480                error_code = if session.shutdown { -32600 } else { -32002 };
481                Err("Server not initialized or has shut down".into())
482            } else {
483                if !matches!(
484                    method,
485                    "initialize"
486                        | "initialized"
487                        | "shutdown"
488                        | "textDocument/didOpen"
489                        | "textDocument/didChange"
490                        | "textDocument/didClose"
491                        | "avril/readDocument"
492                        | "workspace/didChangeWatchedFiles"
493                        | "workspace/didChangeConfiguration"
494                ) && !session.methods.contains(method)
495                {
496                    error_code = -32601;
497                }
498                tokio::select! {
499                    result = session.handle(method,message["params"].clone(),&mut writer) => result,
500                    _ = async {
501                        loop {
502                            let notified = cancellation.notified();
503                            if cancel() { break; }
504                            notified.await;
505                        }
506                    }, if session.methods.contains(method) => {
507                        error_code = -32800;
508                        Err("Request cancelled".into())
509                    },
510                    _ = disconnected.cancelled() => return Err("Editor disconnected".into()),
511                    _ = &mut interrupt => return Err("LSP interrupted".into()),
512                }
513            };
514            if let Some(id) = id {
515                let response = if cancel() {
516                    json!({"id": id, "error": {"code": -32800, "message": "Request cancelled"}})
517                } else {
518                    match response {
519                        Ok(value) => json!({"id": id, "result": value}),
520                        Err(error) => {
521                            let native = error.downcast_ref::<wire::RpcError>();
522                            json!({"id":id, "error": {
523                                "code": native.map_or(error_code, |e| e.code),
524                                "message": error.to_string(),
525                                "data": native.and_then(|e| e.data.as_ref()),
526                            }})
527                        }
528                    }
529                };
530                wire::write(&mut writer, response).await?;
531            } else if let Err(error) = response {
532                wire::write(
533                    &mut writer,
534                    json!({
535                        "method": "window/logMessage",
536                        "params": {"type": 1, "message": error.to_string()}
537                    }),
538                )
539                .await?;
540            }
541            if session.dirty {
542                debounce.as_mut().reset(tokio::time::Instant::now() + std::time::Duration::from_millis(100));
543            }
544            if let Some(key) = key {
545                pending.lock().unwrap().remove(&key);
546                cancelled.lock().unwrap().remove(&key);
547            }
548        }
549    }
550    .await;
551    reader_task.abort();
552    let _ = reader_task.await;
553    let cleanup = if let Some(mut project) = session.project {
554        if result.is_err() {
555            project.terminate().await
556        } else {
557            project.close().await
558        }
559    } else {
560        Ok(())
561    };
562    result.and(cleanup)
563}