Skip to main content

graphix_shell/
lsp_backend.rs

1//! LSP backend that owns a graphix runtime with the stdlib loaded
2//! and exposes a synchronous interface for the LSP server.
3
4use crate::deps;
5use ahash::AHashMap;
6use anyhow::{Context, Result};
7use arcstr::ArcStr;
8use enumflags2::BitFlags;
9use graphix_compiler::{
10    env::Env,
11    expr::{BufferOverrides, ModuleResolver, Source},
12    CFlag, ExecCtx,
13};
14use graphix_lsp::{LspBackend, TypecheckResult};
15use graphix_rt::{CheckResult, GXConfig, GXEvent, GXHandle, GXRt, NoExt};
16use lsp_types::{InitializeParams, Uri};
17use netidx::InternalOnly;
18use parking_lot::Mutex;
19use poolshark::global::GPooled;
20use std::{
21    path::{Path, PathBuf},
22    sync::Arc as StdArc,
23};
24use tokio::{
25    runtime::{Handle, Runtime},
26    sync::mpsc,
27    task,
28};
29use triomphe::Arc;
30
31/// Build a tokio runtime, stand up a graphix runtime with the
32/// in-process netidx and the full stdlib loaded, and run the LSP
33/// server until it shuts down.
34pub fn run() -> Result<()> {
35    let rt = Runtime::new().context("building tokio runtime")?;
36    let result = graphix_lsp::serve(|init| {
37        let roots = project_roots(init);
38        rt.block_on(build_backend(roots))
39    });
40    drop(rt);
41    result
42}
43
44async fn build_backend(roots: Vec<PathBuf>) -> Result<StdArc<dyn LspBackend>> {
45    let netidx = InternalOnly::new().await.context("starting internal netidx")?;
46    let publisher = netidx.publisher().clone();
47    let subscriber = netidx.subscriber().clone();
48    let mut ctx = ExecCtx::new(GXRt::<NoExt>::new(publisher, subscriber))
49        .context("creating graphix context")?;
50    let mut vfs = AHashMap::default();
51    let res = deps::register::<NoExt>(&mut ctx, &mut vfs)
52        .context("registering stdlib modules")?;
53    let mut resolvers: Vec<ModuleResolver> = vec![ModuleResolver::VFS(vfs)];
54    // Cache the stdlib (+ later, GRAPHIX_MODPATH) layer so per-project
55    // checks can prepend it under their own BufferOverride resolver.
56    let base_resolvers = resolvers.clone();
57    for root in roots {
58        resolvers.push(ModuleResolver::Files { base: root, overrides: None });
59    }
60    let flags = CFlag::WarnUnhandled | CFlag::WarnUnused;
61    // We don't consume runtime events in the LSP — drain them on a task
62    // so the channel doesn't fill and stall the runtime.
63    let (tx, rx) = mpsc::channel(100);
64    task::spawn(drain(rx));
65    let gx = GXConfig::builder(ctx, tx)
66        .flags(BitFlags::from(flags))
67        .root(res.root)
68        .resolvers(resolvers)
69        .lsp_mode(true)
70        .build()
71        .context("building runtime config")?
72        .start()
73        .await
74        .context("loading stdlib")?;
75    let _keep_netidx = StdArc::new(netidx);
76    let buffer_overrides: BufferOverrides = Arc::new(Mutex::new(AHashMap::default()));
77    Ok(StdArc::new(ShellLspBackend {
78        gx,
79        rt_handle: Handle::current(),
80        _keep_netidx,
81        base_resolvers,
82        buffer_overrides,
83    }))
84}
85
86async fn drain(mut rx: mpsc::Receiver<GPooled<Vec<GXEvent>>>) {
87    while rx.recv().await.is_some() {}
88}
89
90/// Collect filesystem roots the editor told us about. Prefers
91/// `workspaceFolders` (multi-root capable) and falls back to the
92/// deprecated `rootUri` / `rootPath`.
93fn project_roots(init: &InitializeParams) -> Vec<PathBuf> {
94    let mut roots = Vec::new();
95    if let Some(folders) = &init.workspace_folders {
96        for folder in folders {
97            if let Some(p) = file_uri_to_path(&folder.uri) {
98                roots.push(p);
99            }
100        }
101    }
102    if roots.is_empty() {
103        #[allow(deprecated)]
104        if let Some(uri) = &init.root_uri {
105            if let Some(p) = file_uri_to_path(uri) {
106                roots.push(p);
107            }
108        }
109    }
110    if roots.is_empty() {
111        #[allow(deprecated)]
112        if let Some(p) = init.root_path.as_ref() {
113            roots.push(PathBuf::from(p));
114        }
115    }
116    roots
117}
118
119fn file_uri_to_path(uri: &Uri) -> Option<PathBuf> {
120    graphix_lsp::uri::uri_to_path(uri)
121}
122
123struct ShellLspBackend {
124    gx: GXHandle<NoExt>,
125    rt_handle: Handle,
126    _keep_netidx: StdArc<InternalOnly>,
127    /// Stdlib + GRAPHIX_MODPATH-derived resolvers. Anything that should
128    /// be in scope regardless of which project we're checking. The
129    /// per-call `BufferOverride` (rooted at the file's parent dir) is
130    /// appended in `typecheck_project`.
131    base_resolvers: Vec<ModuleResolver>,
132    /// Shared open-buffer override map. Mutated by the LSP on every
133    /// `did_open` / `did_change` / `did_close`. Layered into every
134    /// resolver chain, so unsaved edits in any open buffer are visible
135    /// to all check calls.
136    buffer_overrides: BufferOverrides,
137}
138
139impl ShellLspBackend {
140    /// Build the resolver chain for checking a project rooted at
141    /// `file`. Appends a `BufferOverride` whose base is the file's
142    /// parent dir — the override map shadows on-disk text per path,
143    /// and falls through to the disk for anything not in the map.
144    /// Sibling-module imports work the same way they do when running
145    /// the file directly, plus the editor-buffer view is honored.
146    fn resolvers_for(&self, file: &Path) -> Vec<ModuleResolver> {
147        let mut resolvers = self.base_resolvers.clone();
148        if let Some(parent) = file.parent() {
149            resolvers.push(ModuleResolver::Files {
150                base: parent.to_path_buf(),
151                overrides: Some(self.buffer_overrides.clone()),
152            });
153        }
154        resolvers
155    }
156}
157
158impl LspBackend for ShellLspBackend {
159    fn env(&self) -> Env {
160        self.rt_handle.block_on(self.gx.get_env()).unwrap_or_default()
161    }
162
163    fn buffer_overrides(&self) -> BufferOverrides {
164        self.buffer_overrides.clone()
165    }
166
167    fn typecheck_project(
168        &self,
169        root: &Path,
170        initial_scope: Option<ArcStr>,
171    ) -> Result<TypecheckResult> {
172        let CheckResult { env, references, module_references, scope_map, lsp } =
173            self.rt_handle.block_on(self.gx.check_with_resolvers(
174                Source::File(root.to_path_buf()),
175                self.resolvers_for(root),
176                initial_scope,
177            ))?;
178        Ok(TypecheckResult { env, references, module_references, scope_map, lsp })
179    }
180}