taplo_lsp/
world.rs

1use crate::{
2    config::{InitConfig, LspConfig},
3    lsp_ext::notification::{DidChangeSchemaAssociation, DidChangeSchemaAssociationParams},
4};
5use anyhow::anyhow;
6use arc_swap::ArcSwap;
7use lsp_async_stub::{rpc, util::Mapper, Context, RequestWriter};
8use lsp_types::Url;
9use once_cell::sync::Lazy;
10use regex::Regex;
11use serde_json::json;
12use std::{str, sync::Arc, time::Duration};
13use taplo::{dom::Node, parser::Parse};
14use taplo_common::{
15    config::Config,
16    environment::Environment,
17    schema::{
18        associations::{priority, source, AssociationRule, SchemaAssociation},
19        Schemas,
20    },
21    AsyncRwLock, HashMap, IndexMap,
22};
23
24pub type World<E> = Arc<WorldState<E>>;
25
26#[repr(transparent)]
27pub struct Workspaces<E: Environment>(IndexMap<Url, WorkspaceState<E>>);
28
29impl<E: Environment> std::ops::Deref for Workspaces<E> {
30    type Target = IndexMap<Url, WorkspaceState<E>>;
31
32    fn deref(&self) -> &Self::Target {
33        &self.0
34    }
35}
36
37impl<E: Environment> std::ops::DerefMut for Workspaces<E> {
38    fn deref_mut(&mut self) -> &mut Self::Target {
39        &mut self.0
40    }
41}
42
43impl<E: Environment> Workspaces<E> {
44    #[must_use]
45    pub fn by_document(&self, url: &Url) -> &WorkspaceState<E> {
46        self.0
47            .iter()
48            .filter(|(key, _)| url.as_str().starts_with(key.as_str()))
49            .max_by(|(a, _), (b, _)| a.as_str().len().cmp(&b.as_str().len()))
50            .map_or_else(
51                || {
52                    tracing::warn!(document_url = %url, "using detached workspace");
53                    self.0.get(&*DEFAULT_WORKSPACE_URL).unwrap()
54                },
55                |(_, ws)| ws,
56            )
57    }
58
59    pub fn by_document_mut(&mut self, url: &Url) -> &mut WorkspaceState<E> {
60        self.0
61            .iter_mut()
62            .filter(|(key, _)| {
63                url.as_str().starts_with(key.as_str()) || *key == &*DEFAULT_WORKSPACE_URL
64            })
65            .max_by(|(a, _), (b, _)| a.as_str().len().cmp(&b.as_str().len()))
66            .map(|(k, ws)| {
67                if k == &*DEFAULT_WORKSPACE_URL {
68                    tracing::warn!(document_url = %url, "using detached workspace");
69                }
70
71                ws
72            })
73            .unwrap()
74    }
75}
76
77pub struct WorldState<E: Environment> {
78    pub(crate) init_config: ArcSwap<InitConfig>,
79    pub(crate) env: E,
80    pub(crate) workspaces: AsyncRwLock<Workspaces<E>>,
81    pub(crate) default_config: ArcSwap<Config>,
82}
83
84pub static DEFAULT_WORKSPACE_URL: Lazy<Url> = Lazy::new(|| Url::parse("root:///").unwrap());
85
86impl<E: Environment> WorldState<E> {
87    pub fn new(env: E) -> Self {
88        Self {
89            init_config: Default::default(),
90            workspaces: {
91                let mut m = IndexMap::default();
92                m.insert(
93                    DEFAULT_WORKSPACE_URL.clone(),
94                    WorkspaceState::new(env.clone(), DEFAULT_WORKSPACE_URL.clone()),
95                );
96                AsyncRwLock::new(Workspaces(m))
97            },
98            default_config: Default::default(),
99            env,
100        }
101    }
102
103    /// Set the world state's default config.
104    pub fn set_default_config(&self, default_config: Arc<Config>) {
105        self.default_config.store(default_config);
106    }
107}
108
109pub struct WorkspaceState<E: Environment> {
110    pub(crate) root: Url,
111    pub(crate) documents: HashMap<lsp_types::Url, DocumentState>,
112    pub(crate) taplo_config: Config,
113    pub(crate) schemas: Schemas<E>,
114    pub(crate) config: LspConfig,
115}
116
117impl<E: Environment> WorkspaceState<E> {
118    pub(crate) fn new(env: E, root: Url) -> Self {
119        let client;
120        #[cfg(target_arch = "wasm32")]
121        {
122            client = reqwest::Client::builder().build().unwrap();
123        }
124
125        #[cfg(not(target_arch = "wasm32"))]
126        {
127            client = taplo_common::util::get_reqwest_client(Duration::from_secs(10)).unwrap();
128        }
129
130        Self {
131            root,
132            documents: Default::default(),
133            taplo_config: Default::default(),
134            schemas: Schemas::new(env, client),
135            config: LspConfig::default(),
136        }
137    }
138}
139
140impl<E: Environment> WorkspaceState<E> {
141    pub(crate) fn document(&self, url: &Url) -> Result<&DocumentState, rpc::Error> {
142        self.documents
143            .get(url)
144            .ok_or_else(rpc::Error::invalid_params)
145    }
146
147    #[tracing::instrument(skip_all, fields(%self.root))]
148    pub(crate) async fn initialize(
149        &mut self,
150        context: Context<World<E>>,
151        env: &impl Environment,
152    ) -> Result<(), anyhow::Error> {
153        if let Err(error) = self
154            .load_config(env, &context.world().default_config.load())
155            .await
156        {
157            tracing::warn!(%error, "failed to load workspace configuration");
158        }
159
160        if !self.config.schema.enabled {
161            return Ok(());
162        }
163
164        self.schemas.cache().set_expiration_times(
165            Duration::from_secs(self.config.schema.cache.memory_expiration),
166            Duration::from_secs(self.config.schema.cache.disk_expiration),
167        );
168
169        self.schemas
170            .associations()
171            .add_from_config(&self.taplo_config);
172
173        for (pattern, schema_url) in &self.config.schema.associations {
174            let pattern = match Regex::new(pattern) {
175                Ok(p) => p,
176                Err(error) => {
177                    tracing::error!(%error, "invalid association pattern");
178                    continue;
179                }
180            };
181
182            let url = if schema_url.starts_with("./") {
183                self.root.join(schema_url)
184            } else {
185                schema_url.parse()
186            };
187
188            let url = match url {
189                Ok(u) => u,
190                Err(error) => {
191                    tracing::error!(%error, url = %schema_url, "invalid schema url");
192                    continue;
193                }
194            };
195
196            self.schemas.associations().add(
197                AssociationRule::Regex(pattern),
198                SchemaAssociation {
199                    url,
200                    meta: json!({
201                        "source": source::LSP_CONFIG,
202                    }),
203                    priority: priority::LSP_CONFIG,
204                },
205            );
206        }
207
208        for catalog in &self.config.schema.catalogs {
209            if let Err(error) = self.schemas.associations().add_from_catalog(catalog).await {
210                tracing::error!(%error, "failed to add schemas from catalog");
211            }
212        }
213
214        self.emit_associations(context).await;
215        Ok(())
216    }
217
218    pub(crate) async fn load_config(
219        &mut self,
220        env: &impl Environment,
221        default_config: &Config,
222    ) -> Result<(), anyhow::Error> {
223        self.taplo_config = default_config.clone();
224
225        let root_path = env
226            .to_file_path_normalized(&self.root)
227            .ok_or_else(|| anyhow!("invalid root URL"))?;
228
229        if self.config.taplo.config_file.enabled {
230            let config_path = if let Some(p) = &self.config.taplo.config_file.path {
231                tracing::debug!(path = ?p, "using config file at specified path");
232
233                if env.is_absolute(p) {
234                    Some(p.clone())
235                } else if self.root != *DEFAULT_WORKSPACE_URL {
236                    Some(root_path.join(p))
237                } else {
238                    tracing::debug!("relative config path is not valid for detached workspace");
239                    None
240                }
241            } else if self.root != *DEFAULT_WORKSPACE_URL {
242                tracing::debug!("discovering config file in workspace");
243                env.find_config_file_normalized(&root_path).await
244            } else {
245                None
246            };
247
248            if let Some(config_path) = config_path {
249                tracing::info!(path = ?config_path, "using config file");
250                self.taplo_config =
251                    toml::from_str(str::from_utf8(&env.read_file(&config_path).await?)?)?;
252            }
253        }
254
255        self.taplo_config.rule.extend(self.config.rules.clone());
256        self.taplo_config.prepare(env, &root_path)?;
257
258        tracing::debug!("using config: {:#?}", self.taplo_config);
259
260        Ok(())
261    }
262
263    pub(crate) async fn emit_associations(&self, mut context: Context<World<E>>) {
264        for document_url in self.documents.keys() {
265            if let Some(assoc) = self.schemas.associations().association_for(document_url) {
266                if let Err(error) = context
267                    .write_notification::<DidChangeSchemaAssociation, _>(Some(
268                        DidChangeSchemaAssociationParams {
269                            document_uri: document_url.clone(),
270                            schema_uri: Some(assoc.url.clone()),
271                            meta: Some(assoc.meta.clone()),
272                        },
273                    ))
274                    .await
275                {
276                    tracing::error!(%error, "failed to write notification");
277                }
278            } else if let Err(error) = context
279                .write_notification::<DidChangeSchemaAssociation, _>(Some(
280                    DidChangeSchemaAssociationParams {
281                        document_uri: document_url.clone(),
282                        schema_uri: None,
283                        meta: None,
284                    },
285                ))
286                .await
287            {
288                tracing::error!(%error, "failed to write notification");
289            }
290        }
291    }
292}
293
294#[derive(Debug, Clone)]
295pub struct DocumentState {
296    pub(crate) parse: Parse,
297    pub(crate) dom: Node,
298    pub(crate) mapper: Mapper,
299}