Skip to main content

wdl_lsp/
server.rs

1//! Implementation of the LSP server.
2
3use std::ffi::OsStr;
4use std::fmt::Formatter;
5use std::mem;
6use std::path::Component;
7use std::path::PathBuf;
8use std::path::Prefix;
9use std::str::FromStr;
10use std::sync::Arc;
11use std::sync::OnceLock;
12
13use anyhow::Result;
14use notification::Progress;
15use parking_lot::RwLock;
16use request::WorkDoneProgressCreate;
17use serde::Deserialize;
18use serde::Deserializer;
19use serde_json::to_value;
20use struct_patch::Patch;
21use tower_lsp::Client;
22use tower_lsp::LanguageServer;
23use tower_lsp::LspService;
24use tower_lsp::jsonrpc::Error as RpcError;
25use tower_lsp::jsonrpc::ErrorCode;
26use tower_lsp::jsonrpc::Result as RpcResult;
27use tower_lsp::lsp_types::request::WorkspaceConfiguration;
28use tower_lsp::lsp_types::*;
29use tracing::debug;
30use tracing::error;
31use tracing::info;
32use uuid::Uuid;
33use wdl_analysis::Analyzer;
34use wdl_analysis::Config as AnalysisConfig;
35use wdl_analysis::DiagnosticsConfig;
36use wdl_analysis::FeatureFlags;
37use wdl_analysis::IncrementalChange;
38use wdl_analysis::SourceEdit;
39use wdl_analysis::SourcePosition;
40use wdl_analysis::SourcePositionEncoding;
41use wdl_analysis::Validator;
42use wdl_analysis::handlers::WDL_SEMANTIC_TOKEN_MODIFIERS;
43use wdl_analysis::handlers::WDL_SEMANTIC_TOKEN_TYPES;
44use wdl_analysis::path_to_uri;
45use wdl_lint::Linter;
46use wdl_lint::Rule;
47
48use crate::proto;
49
50/// Normalizes the path of a URI.
51///
52/// If the path contains percent encoded sequences, the sequences are decoded.
53///
54/// Additionally, on Windows, this will normalize the drive letter to uppercase.
55fn normalize_uri_path(uri: &mut Url) {
56    if uri.scheme() != "file" {
57        return;
58    }
59
60    // Call `to_file_path` which will automatically decode any encoded sequences
61    if let Ok(path) = uri.to_file_path() {
62        // On windows we need to normalize any drive letter prefixes to uppercase
63        let path = if cfg!(windows) {
64            let mut comps = path.components();
65            match comps.next() {
66                Some(Component::Prefix(prefix)) => match prefix.kind() {
67                    Prefix::Disk(d) => {
68                        let mut path = PathBuf::new();
69                        path.push(format!("{}:", d.to_ascii_uppercase() as char));
70                        path.extend(comps);
71                        path
72                    }
73                    Prefix::VerbatimDisk(d) => {
74                        let mut path = PathBuf::new();
75                        path.push(format!(r"\\?\{}:", d.to_ascii_uppercase() as char));
76                        path.extend(comps);
77                        path
78                    }
79                    _ => path,
80                },
81                _ => path,
82            }
83        } else {
84            path
85        };
86
87        if let Ok(u) = Url::from_file_path(path) {
88            *uri = u;
89        }
90    }
91}
92
93/// LSP features supported by the client.
94#[derive(Clone, Copy, Debug, Default)]
95struct ClientSupport {
96    /// Whether or not the client supports dynamic registration of watched
97    /// files.
98    pub watched_files: bool,
99    /// Whether or not the client supports pull diagnostics (workspace and text
100    /// document).
101    pub pull_diagnostics: bool,
102    /// Whether or not the client supports registering work done progress
103    /// tokens.
104    pub work_done_progress: bool,
105    /// Whether or not the client supports configuration change notifications.
106    pub did_change_configuration: bool,
107}
108
109impl ClientSupport {
110    /// Creates a new client features from the given client capabilities.
111    pub fn new(capabilities: &ClientCapabilities) -> Self {
112        Self {
113            watched_files: capabilities
114                .workspace
115                .as_ref()
116                .map(|c| {
117                    c.did_change_watched_files
118                        .as_ref()
119                        .map(|c| c.dynamic_registration == Some(true))
120                        .unwrap_or(false)
121                })
122                .unwrap_or(false),
123            pull_diagnostics: capabilities
124                .text_document
125                .as_ref()
126                .map(|c| c.diagnostic.is_some())
127                .unwrap_or(false),
128            work_done_progress: capabilities
129                .window
130                .as_ref()
131                .map(|c| c.work_done_progress == Some(true))
132                .unwrap_or(false),
133            did_change_configuration: capabilities
134                .workspace
135                .as_ref()
136                .map(|c| {
137                    c.did_change_configuration
138                        .as_ref()
139                        .map(|c| c.dynamic_registration == Some(true))
140                        .unwrap_or(false)
141                })
142                .unwrap_or(false),
143        }
144    }
145}
146
147/// Represents a progress token for displaying work progress in the client.
148#[derive(Debug, Clone, Default)]
149struct ProgressToken(Option<String>);
150
151impl ProgressToken {
152    /// Constructs a new progress token.
153    ///
154    /// If progress tokens aren't supported by the client, this will return a
155    /// no-op token.
156    pub async fn new(client: &Client, client_supported: bool) -> Self {
157        if !client_supported {
158            return Self(None);
159        }
160
161        let token = Uuid::new_v4().to_string();
162        if client
163            .send_request::<WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
164                token: NumberOrString::String(token.clone()),
165            })
166            .await
167            .is_err()
168        {
169            return Self(None);
170        }
171
172        Self(Some(token))
173    }
174
175    /// Starts the work progress.
176    pub async fn start(
177        &self,
178        client: &Client,
179        title: impl Into<String>,
180        message: impl Into<String>,
181    ) {
182        if let Some(token) = &self.0 {
183            client
184                .send_notification::<Progress>(ProgressParams {
185                    token: NumberOrString::String(token.clone()),
186                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
187                        WorkDoneProgressBegin {
188                            title: title.into(),
189                            cancellable: None,
190                            message: Some(message.into()),
191                            percentage: Some(0),
192                        },
193                    )),
194                })
195                .await;
196        }
197    }
198
199    /// Updates the work progress.
200    pub async fn update(&self, client: &Client, message: impl Into<String>, percentage: u32) {
201        if let Some(token) = &self.0 {
202            client
203                .send_notification::<Progress>(ProgressParams {
204                    token: NumberOrString::String(token.clone()),
205                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
206                        WorkDoneProgressReport {
207                            cancellable: None,
208                            message: Some(message.into()),
209                            percentage: Some(percentage),
210                        },
211                    )),
212                })
213                .await;
214        }
215    }
216
217    /// Completes the work progress.
218    pub async fn complete(self, client: &Client, message: impl Into<String>) {
219        if let Some(token) = self.0 {
220            client
221                .send_notification::<Progress>(ProgressParams {
222                    token: NumberOrString::String(token),
223                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
224                        WorkDoneProgressEnd {
225                            message: Some(message.into()),
226                        },
227                    )),
228                })
229                .await;
230        }
231    }
232}
233
234// NOTE: Renamed camelCase to make it play nicely with the vscode extension.
235/// Represents options for running the LSP server.
236#[derive(Debug, Clone, Patch)]
237#[patch(attribute(derive(Debug, Default, Deserialize)))]
238#[patch(attribute(serde(rename_all = "camelCase")))]
239#[patch(attribute(allow(missing_docs)))]
240pub struct ServerOptions {
241    /// The name of the server.
242    ///
243    /// Defaults to `wdl-lsp` crate name.
244    #[patch(skip)]
245    pub name: String,
246
247    /// The version of the server.
248    ///
249    /// Defaults to the version of the `wdl-lsp` crate.
250    #[patch(skip)]
251    pub version: String,
252
253    /// The verbosity level of the server.
254    pub log_level: LevelFilter,
255
256    /// The options for linting.
257    #[patch(nesting)]
258    pub lint: LintOptions,
259
260    /// Analysis or lint rule IDs to except (ignore).
261    pub exceptions: Vec<String>,
262
263    /// Basename for any ignorefiles which should be respected.
264    pub ignore_filename: Option<String>,
265
266    /// Feature flags for enabling experimental features.
267    #[patch(skip)]
268    pub feature_flags: FeatureFlags,
269
270    /// The diagnostic baseline for suppressing known diagnostics.
271    #[patch(skip)]
272    pub baseline: Option<wdl_lint::Baseline>,
273}
274
275impl Default for ServerOptions {
276    fn default() -> Self {
277        Self {
278            name: String::from(env!("CARGO_CRATE_NAME")),
279            version: String::from(env!("CARGO_PKG_VERSION")),
280            log_level: LevelFilter(tracing::metadata::LevelFilter::ERROR),
281            lint: Default::default(),
282            exceptions: Vec::new(),
283            ignore_filename: None,
284            feature_flags: Default::default(),
285            baseline: None,
286        }
287    }
288}
289
290/// Options for the external linter.
291#[derive(Debug, Default, Clone, PartialEq, Patch)]
292#[patch(attribute(derive(Debug, Default, Deserialize)))]
293#[patch(attribute(allow(missing_docs)))]
294pub struct LintOptions {
295    /// Whether or not linting is enabled.
296    pub enabled: bool,
297    /// The lint rule configuration.
298    #[patch(skip)]
299    pub config: Arc<wdl_lint::Config>,
300}
301
302/// Wrapper for [`tracing::metadata::LevelFilter`] to support deserialization.
303#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
304#[serde(transparent)]
305pub struct LevelFilter(
306    #[serde(deserialize_with = "deserialize_level_filter")] tracing::metadata::LevelFilter,
307);
308
309impl From<tracing::metadata::LevelFilter> for LevelFilter {
310    fn from(level: tracing::metadata::LevelFilter) -> Self {
311        Self(level)
312    }
313}
314
315/// Deserializer for [`tracing::metadata::LevelFilter`].
316fn deserialize_level_filter<'de, D>(
317    deserializer: D,
318) -> Result<tracing::metadata::LevelFilter, D::Error>
319where
320    D: Deserializer<'de>,
321{
322    struct LevelFilterVisitor;
323
324    impl<'de> serde::de::Visitor<'de> for LevelFilterVisitor {
325        type Value = tracing::metadata::LevelFilter;
326
327        fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
328            write!(formatter, "a level filter string")
329        }
330
331        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
332        where
333            E: serde::de::Error,
334        {
335            tracing::metadata::LevelFilter::from_str(v).map_err(serde::de::Error::custom)
336        }
337    }
338
339    deserializer.deserialize_str(LevelFilterVisitor)
340}
341
342/// Reload handle for dynamic level filter setting.
343pub type FilterReloadHandle<S> =
344    tracing_subscriber::reload::Handle<tracing::metadata::LevelFilter, S>;
345
346/// Represents an LSP server for analyzing WDL documents.
347#[derive(Debug)]
348pub struct Server<S> {
349    /// The LSP client connected to the server.
350    client: Client,
351    /// The features supported by the LSP client.
352    client_support: OnceLock<ClientSupport>,
353    /// The current set of workspace folders.
354    folders: Arc<RwLock<Vec<WorkspaceFolder>>>,
355    /// Mutable configuration fields.
356    config: Arc<tokio::sync::RwLock<ServerConfig>>,
357    /// Level filter reload handle.
358    log_handle: Option<FilterReloadHandle<S>>,
359}
360
361/// The server config and dependent fields.
362#[derive(Debug)]
363struct ServerConfig {
364    /// The options for the server.
365    options: ServerOptions,
366    /// The analyzer used to analyze documents.
367    analyzer: Analyzer<ProgressToken>,
368}
369
370impl ServerOptions {
371    /// Create an [`Analyzer`] based on this config.
372    fn analyzer(&self, client: Client) -> Analyzer<ProgressToken> {
373        let linting_enabled = self.lint.enabled;
374        let exceptions = self.exceptions.clone();
375        let ignore_name = self.ignore_filename.clone();
376        let analyzer_client = client.clone();
377
378        let mut all_rules: Vec<_> = wdl_analysis::ALL_RULE_IDS
379            .iter()
380            .chain(wdl_lint::ALL_RULE_IDS.iter())
381            .map(ToString::to_string)
382            .collect();
383        all_rules.sort_unstable();
384        all_rules.dedup();
385
386        // TODO ACF 2025-07-07: add configurability around the fallback behavior; see
387        // https://github.com/stjude-rust-labs/wdl/issues/517
388        let analyzer_config = AnalysisConfig::default()
389            .with_fallback_version(Some(Default::default()))
390            .with_diagnostics_config(DiagnosticsConfig::new(
391                wdl_analysis::rules()
392                    .iter()
393                    .filter(|r| !exceptions.contains(&r.id().into())),
394            ))
395            .with_ignore_filename(ignore_name)
396            .with_all_rules(all_rules)
397            .with_feature_flags(self.feature_flags);
398
399        let wdl_lint_config = self.lint.config.clone();
400        Analyzer::<ProgressToken>::new_with_validator(
401            analyzer_config,
402            move |token, kind, current, total| {
403                let client = analyzer_client.clone();
404                async move {
405                    let message = format!(
406                        "{kind} {current}/{total} file{s}",
407                        s = if total > 1 { "s" } else { "" }
408                    );
409                    let percentage = ((current * 100) as f64 / total as f64) as u32;
410                    token.update(&client, message, percentage).await
411                }
412            },
413            move || {
414                let mut validator = Validator::default();
415                if linting_enabled {
416                    validator.add_visitor(Linter::new(
417                        wdl_lint::rules(&wdl_lint_config)
418                            .into_iter()
419                            .filter(|r| !exceptions.contains(&r.id().into()))
420                            .map(|r| r as Box<dyn Rule>),
421                    ));
422                }
423                validator
424            },
425        )
426    }
427}
428
429impl<S: 'static> Server<S> {
430    /// Creates a new WDL language server.
431    ///
432    /// `log_handle` can be provided to enable dynamic log level setting.
433    pub fn new(
434        client: Client,
435        options: ServerOptions,
436        log_handle: Option<FilterReloadHandle<S>>,
437    ) -> Self {
438        let analyzer = options.analyzer(client.clone());
439        Self {
440            client,
441            client_support: Default::default(),
442            folders: Default::default(),
443            config: Arc::new(tokio::sync::RwLock::new(ServerConfig { options, analyzer })),
444            log_handle,
445        }
446    }
447
448    /// Patch the config with the new values from the client.
449    async fn apply_config_patch(&self, patch: ServerOptionsPatch) {
450        let mut config = self.config.write().await;
451        if let Some(log_level) = patch.log_level
452            && let Some(reload_handle) = self.log_handle.as_ref()
453            && let Err(e) = reload_handle.modify(|filter| *filter = log_level.0)
454        {
455            error!("failed to set log level: {e:?}");
456        }
457
458        config.options.apply(patch);
459        config.analyzer = config.options.analyzer(self.client.clone());
460    }
461
462    /// Runs the server until a request is received to shut down.
463    ///
464    /// See also: [`Self::new()`]
465    pub async fn run(
466        options: ServerOptions,
467        log_handle: Option<FilterReloadHandle<S>>,
468    ) -> Result<()> {
469        debug!("running LSP server: {options:#?}");
470
471        let (service, socket) = LspService::new(|client| Self::new(client, options, log_handle));
472
473        let stdin = tokio::io::stdin();
474        let stdout = tokio::io::stdout();
475        tower_lsp::Server::new(stdin, stdout, socket)
476            .serve(service)
477            .await;
478
479        Ok(())
480    }
481
482    /// Get info about the server.
483    async fn info(&self) -> ServerInfo {
484        let config = self.config.read().await;
485
486        ServerInfo {
487            name: config.options.name.clone(),
488            version: Some(config.options.version.clone()),
489        }
490    }
491
492    /// Registers a generic watcher for all files/directories in the workspace.
493    async fn register_capabilities(&self, client_support: &ClientSupport) {
494        let mut registrations = Vec::new();
495        if client_support.watched_files {
496            registrations.push(Registration {
497                id: Uuid::new_v4().to_string(),
498                method: "workspace/didChangeWatchedFiles".into(),
499                register_options: Some(
500                    to_value(DidChangeWatchedFilesRegistrationOptions {
501                        watchers: vec![FileSystemWatcher {
502                            // We use a generic glob so we can be notified for when directories,
503                            // which might contain WDL documents, are deleted
504                            glob_pattern: GlobPattern::String("**/*".to_string()),
505                            kind: None,
506                        }],
507                    })
508                    .expect("should convert to value"),
509                ),
510            });
511        }
512
513        if client_support.did_change_configuration {
514            registrations.push(Registration {
515                id: Uuid::new_v4().to_string(),
516                method: "workspace/didChangeConfiguration".into(),
517                register_options: None,
518            });
519        }
520
521        if registrations.is_empty() {
522            return;
523        }
524
525        self.client
526            .register_capability(registrations)
527            .await
528            .expect("failed to register capabilities with client");
529    }
530}
531
532#[tower_lsp::async_trait]
533impl<S: 'static> LanguageServer for Server<S> {
534    async fn initialize(&self, params: InitializeParams) -> RpcResult<InitializeResult> {
535        debug!("received `initialize` request: {params:#?}");
536
537        if let Some(folders) = params.workspace_folders {
538            let config = self.config.read().await;
539            for mut folder in folders {
540                normalize_uri_path(&mut folder.uri);
541                self.folders.write().push(folder.clone());
542                if let Ok(path) = folder.uri.to_file_path()
543                    && let Err(e) = config.analyzer.add_directory(path).await
544                {
545                    error!(
546                        "failed to add initial workspace directory {uri}: {e}",
547                        uri = folder.uri
548                    );
549                }
550            }
551        }
552
553        {
554            let client_support = ClientSupport::new(&params.capabilities);
555
556            if !client_support.pull_diagnostics {
557                return Err(RpcError {
558                    code: ErrorCode::ServerError(0),
559                    message: "LSP server currently requires support for pulling diagnostics".into(),
560                    data: None,
561                });
562            }
563
564            // This is guaranteed to be called once anyway
565            let _ = self.client_support.set(client_support);
566        }
567
568        Ok(InitializeResult {
569            capabilities: ServerCapabilities {
570                text_document_sync: Some(TextDocumentSyncCapability::Options(
571                    TextDocumentSyncOptions {
572                        open_close: Some(true),
573                        change: Some(TextDocumentSyncKind::INCREMENTAL),
574                        ..Default::default()
575                    },
576                )),
577                workspace: Some(WorkspaceServerCapabilities {
578                    workspace_folders: Some(WorkspaceFoldersServerCapabilities {
579                        supported: Some(true),
580                        change_notifications: Some(OneOf::Left(true)),
581                    }),
582                    ..Default::default()
583                }),
584                workspace_symbol_provider: Some(OneOf::Left(true)),
585                diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
586                    DiagnosticOptions {
587                        inter_file_dependencies: true,
588                        workspace_diagnostics: true,
589                        // Intentionally disabled as currently VS code doesn't send a work done
590                        // token on the diagnostic requests, only one for partial results; instead,
591                        // we'll use a token created by the server to report progress.
592                        // work_done_progress_options: WorkDoneProgressOptions {
593                        //     work_done_progress: Some(true),
594                        // },
595                        ..Default::default()
596                    },
597                )),
598                document_symbol_provider: Some(OneOf::Left(true)),
599                document_formatting_provider: Some(OneOf::Left(true)),
600                definition_provider: Some(OneOf::Left(true)),
601                references_provider: Some(OneOf::Left(true)),
602                completion_provider: Some(CompletionOptions {
603                    resolve_provider: Some(false),
604                    trigger_characters: Some(vec![
605                        ".".to_string(),
606                        "[".to_string(),
607                        "#".to_string(),
608                    ]),
609                    ..Default::default()
610                }),
611                hover_provider: Some(HoverProviderCapability::Simple(true)),
612                rename_provider: Some(OneOf::Left(true)),
613                semantic_tokens_provider: Some(
614                    SemanticTokensServerCapabilities::SemanticTokensOptions(
615                        SemanticTokensOptions {
616                            work_done_progress_options: Default::default(),
617                            legend: SemanticTokensLegend {
618                                token_types: WDL_SEMANTIC_TOKEN_TYPES.to_vec(),
619                                token_modifiers: WDL_SEMANTIC_TOKEN_MODIFIERS.to_vec(),
620                            },
621                            range: Some(false),
622                            full: Some(SemanticTokensFullOptions::Bool(true)),
623                        },
624                    ),
625                ),
626                signature_help_provider: Some(SignatureHelpOptions {
627                    trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
628                    retrigger_characters: None,
629                    work_done_progress_options: WorkDoneProgressOptions {
630                        work_done_progress: Some(false),
631                    },
632                }),
633                inlay_hint_provider: Some(OneOf::Left(true)),
634                ..Default::default()
635            },
636            server_info: Some(self.info().await),
637        })
638    }
639
640    async fn initialized(&self, _: InitializedParams) {
641        let client_support = self.client_support.get().expect("should exist");
642        self.register_capabilities(client_support).await;
643
644        let info = self.info().await;
645        info!(
646            "{name} (v{version}) server initialized",
647            name = info.name,
648            version = info.version.expect("should exist")
649        );
650    }
651
652    async fn shutdown(&self) -> RpcResult<()> {
653        Ok(())
654    }
655
656    async fn did_open(&self, mut params: DidOpenTextDocumentParams) {
657        normalize_uri_path(&mut params.text_document.uri);
658
659        debug!("received `textDocument/didOpen` request: {params:#?}");
660
661        let config = self.config.read().await;
662        if let Err(e) = config
663            .analyzer
664            .add_document(params.text_document.uri.clone())
665            .await
666        {
667            error!(
668                "failed to add document {uri}: {e}",
669                uri = params.text_document.uri
670            );
671            return;
672        }
673
674        if let Err(e) = config.analyzer.notify_incremental_change(
675            params.text_document.uri,
676            IncrementalChange {
677                version: params.text_document.version,
678                start: Some(params.text_document.text),
679                edits: Vec::new(),
680            },
681        ) {
682            error!("failed to notify incremental change: {e}");
683        }
684    }
685
686    async fn did_change(&self, mut params: DidChangeTextDocumentParams) {
687        let config = self.config.read().await;
688
689        normalize_uri_path(&mut params.text_document.uri);
690
691        debug!("received `textDocument/didChange` request: {params:#?}");
692
693        debug!(
694            "document `{uri}` is now client version {version}",
695            uri = params.text_document.uri,
696            version = params.text_document.version
697        );
698
699        // Look for the last full change (one without a range) and start there
700        let (start, changes) = match params
701            .content_changes
702            .iter()
703            .rposition(|change| change.range.is_none())
704        {
705            Some(idx) => (
706                Some(mem::take(&mut params.content_changes[idx].text)),
707                &mut params.content_changes[idx + 1..],
708            ),
709            None => (None, &mut params.content_changes[..]),
710        };
711
712        // Notify the analyzer that the document has changed
713        if let Err(e) = config.analyzer.notify_incremental_change(
714            params.text_document.uri,
715            IncrementalChange {
716                version: params.text_document.version,
717                start,
718                edits: changes
719                    .iter_mut()
720                    .map(|e| {
721                        let range = e.range.expect("edit should be after the last full change");
722                        SourceEdit::new(
723                            SourcePosition::new(range.start.line, range.start.character)
724                                ..SourcePosition::new(range.end.line, range.end.character),
725                            SourcePositionEncoding::UTF16,
726                            mem::take(&mut e.text),
727                        )
728                    })
729                    .collect(),
730            },
731        ) {
732            error!("failed to notify incremental change: {e}");
733        }
734    }
735
736    async fn did_close(&self, mut params: DidCloseTextDocumentParams) {
737        let config = self.config.read().await;
738
739        normalize_uri_path(&mut params.text_document.uri);
740
741        debug!("received `textDocument/didClose` request: {params:#?}");
742        if let Err(e) = config
743            .analyzer
744            .notify_change(params.text_document.uri, true)
745        {
746            error!("failed to notify change: {e}");
747        }
748    }
749
750    async fn diagnostic(
751        &self,
752        mut params: DocumentDiagnosticParams,
753    ) -> RpcResult<DocumentDiagnosticReportResult> {
754        let config = self.config.read().await;
755
756        normalize_uri_path(&mut params.text_document.uri);
757
758        debug!("received `textDocument/diagnostic` request: {params:#?}");
759
760        let results: Vec<wdl_analysis::AnalysisResult> = config
761            .analyzer
762            .analyze_document(ProgressToken::default(), params.text_document.uri.clone())
763            .await
764            .map_err(|e| RpcError {
765                code: ErrorCode::InternalError,
766                message: e.to_string().into(),
767                data: None,
768            })?;
769
770        drop(config);
771        let name = self.info().await.name;
772        let config = self.config.read().await;
773        let mut matcher = config.options.baseline.as_ref().map(|b| b.matcher());
774        proto::document_diagnostic_report(params, results, &name, matcher.as_mut())
775            .ok_or_else(RpcError::request_cancelled)
776    }
777
778    async fn workspace_diagnostic(
779        &self,
780        params: WorkspaceDiagnosticParams,
781    ) -> RpcResult<WorkspaceDiagnosticReportResult> {
782        let config = self.config.read().await;
783
784        debug!("received `workspace/diagnostic` request: {params:#?}");
785
786        let name = self.info().await.name;
787
788        let client_support = self.client_support.get().expect("should exist");
789        let progress = ProgressToken::new(&self.client, client_support.work_done_progress).await;
790        progress
791            .start(&self.client, name.clone(), "analyzing...")
792            .await;
793        let results = config
794            .analyzer
795            .analyze(progress.clone())
796            .await
797            .map_err(|e| RpcError {
798                code: ErrorCode::InternalError,
799                message: e.to_string().into(),
800                data: None,
801            })?;
802        progress.complete(&self.client, "analysis complete").await;
803
804        let mut matcher = config.options.baseline.as_ref().map(|b| b.matcher());
805        Ok(proto::workspace_diagnostic_report(
806            params,
807            results,
808            &name,
809            matcher.as_mut(),
810        ))
811    }
812
813    async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) {
814        let config = self.config.read().await;
815
816        debug!("received `workspace/didChangeWorkspaceFolders` request: {params:#?}");
817
818        // Process the removed folders
819        if !params.event.removed.is_empty()
820            && let Err(e) = config
821                .analyzer
822                .remove_documents(
823                    params
824                        .event
825                        .removed
826                        .into_iter()
827                        .map(|mut f| {
828                            normalize_uri_path(&mut f.uri);
829                            f.uri
830                        })
831                        .collect(),
832                )
833                .await
834        {
835            error!("failed to remove documents from analyzer: {e}");
836        }
837
838        // Progress the added folders
839        if !params.event.added.is_empty() {
840            for folder in &params.event.added {
841                if let Err(e) = config
842                    .analyzer
843                    .add_directory(folder.uri.to_file_path().expect("should be a file path"))
844                    .await
845                {
846                    error!("failed to add documents from directory to analyzer: {e}");
847                }
848            }
849        }
850    }
851
852    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
853        debug!("received `workspace/didChangeConfiguration` notification: {params:#?}");
854
855        let workspace_configs = self
856            .client
857            .send_request::<WorkspaceConfiguration>(ConfigurationParams {
858                items: vec![ConfigurationItem {
859                    scope_uri: None,
860                    section: Some(String::from("sprocket.server")),
861                }],
862            })
863            .await;
864
865        match workspace_configs {
866            Ok(mut configs) if !configs.is_empty() => {
867                match serde_json::from_value::<ServerOptionsPatch>(configs.remove(0)) {
868                    Ok(patch) => self.apply_config_patch(patch).await,
869                    Err(e) => error!("failed to deserialize `ServerOptionsPatch`: {e:?}"),
870                }
871            }
872            Ok(_) => error!("client returned no configuration"),
873            Err(e) => error!("failed to fetch workspace configuration: {e}"),
874        }
875    }
876
877    async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
878        let config = self.config.read().await;
879
880        debug!("received `workspace/didChangeWatchedFiles` request: {params:#?}");
881
882        /// Converts a URI into a WDL file path.
883        fn to_wdl_file_path(uri: &Url) -> Option<PathBuf> {
884            if let Ok(path) = uri.to_file_path()
885                && path.is_file()
886                && path.extension().and_then(OsStr::to_str) == Some("wdl")
887            {
888                return Some(path);
889            }
890
891            None
892        }
893
894        let mut added = Vec::new();
895        let mut deleted = Vec::new();
896
897        for mut event in params.changes {
898            normalize_uri_path(&mut event.uri);
899
900            match event.typ {
901                FileChangeType::CREATED => {
902                    if let Some(path) = to_wdl_file_path(&event.uri) {
903                        debug!("document `{uri}` has been created", uri = event.uri);
904                        added.push(path);
905                    }
906                }
907                FileChangeType::CHANGED => {
908                    if to_wdl_file_path(&event.uri).is_some() {
909                        debug!("document `{uri}` has been changed", uri = event.uri);
910                        if let Err(e) = config.analyzer.notify_change(event.uri, false) {
911                            error!("failed to notify change: {e}");
912                        }
913                    }
914                }
915                FileChangeType::DELETED => {
916                    if to_wdl_file_path(&event.uri).is_some() {
917                        debug!("document `{uri}` has been deleted", uri = event.uri);
918                        deleted.push(event.uri);
919                    }
920                }
921                _ => continue,
922            }
923        }
924
925        // Add any documents to the analyzer
926        if !added.is_empty() {
927            for file in added {
928                if let Err(e) = config
929                    .analyzer
930                    .add_document(path_to_uri(&file).expect("should convert to uri"))
931                    .await
932                {
933                    error!("failed to add documents to analyzer: {e}");
934                }
935            }
936        }
937
938        // Remove any documents from the analyzer
939        if !deleted.is_empty()
940            && let Err(e) = config.analyzer.remove_documents(deleted).await
941        {
942            error!("failed to remove documents from analyzer: {e}");
943        }
944    }
945
946    async fn formatting(
947        &self,
948        mut params: DocumentFormattingParams,
949    ) -> RpcResult<Option<Vec<TextEdit>>> {
950        let config = self.config.read().await;
951
952        normalize_uri_path(&mut params.text_document.uri);
953
954        debug!("received `textDocument/formatting` request: {params:#?}");
955
956        let result = config
957            .analyzer
958            .format_document(params.text_document.uri)
959            .await
960            .map_err(|e| RpcError {
961                code: ErrorCode::InternalError,
962                message: e.to_string().into(),
963                data: None,
964            })?
965            .map(|(end_line, end_col, formatted)| {
966                vec![TextEdit {
967                    range: Range {
968                        // NOTE: always replace the full set of text starting at the
969                        // very first position.
970                        start: Position {
971                            line: 0,
972                            character: 0,
973                        },
974                        end: Position {
975                            line: end_line,
976                            character: end_col,
977                        },
978                    },
979                    new_text: formatted,
980                }]
981            });
982
983        Ok(result)
984    }
985
986    async fn goto_definition(
987        &self,
988        mut params: GotoDefinitionParams,
989    ) -> RpcResult<Option<GotoDefinitionResponse>> {
990        let config = self.config.read().await;
991
992        normalize_uri_path(&mut params.text_document_position_params.text_document.uri);
993
994        debug!("received `textDocument/gotoDefinition` request: {params:#?}");
995
996        let position = SourcePosition::new(
997            params.text_document_position_params.position.line,
998            params.text_document_position_params.position.character,
999        );
1000
1001        let result = config
1002            .analyzer
1003            .goto_definition(
1004                params.text_document_position_params.text_document.uri,
1005                position,
1006                SourcePositionEncoding::UTF16,
1007            )
1008            .await
1009            .map_err(|e| RpcError {
1010                code: ErrorCode::InternalError,
1011                message: e.to_string().into(),
1012                data: None,
1013            })?;
1014
1015        Ok(result)
1016    }
1017
1018    async fn references(&self, mut params: ReferenceParams) -> RpcResult<Option<Vec<Location>>> {
1019        let config = self.config.read().await;
1020
1021        normalize_uri_path(&mut params.text_document_position.text_document.uri);
1022
1023        debug!("received `textDocument/references` request: {params:#?}");
1024
1025        let position = SourcePosition::new(
1026            params.text_document_position.position.line,
1027            params.text_document_position.position.character,
1028        );
1029
1030        let result = config
1031            .analyzer
1032            .find_all_references(
1033                params.text_document_position.text_document.uri,
1034                position,
1035                SourcePositionEncoding::UTF16,
1036                params.context.include_declaration,
1037            )
1038            .await
1039            .map_err(|e| RpcError {
1040                code: ErrorCode::InternalError,
1041                message: e.to_string().into(),
1042                data: None,
1043            })?;
1044
1045        Ok(Some(result))
1046    }
1047
1048    async fn completion(
1049        &self,
1050        mut params: CompletionParams,
1051    ) -> RpcResult<Option<CompletionResponse>> {
1052        let config = self.config.read().await;
1053
1054        normalize_uri_path(&mut params.text_document_position.text_document.uri);
1055
1056        debug!("received `textDocument/completion` request: {params:#?}");
1057
1058        let position = SourcePosition::new(
1059            params.text_document_position.position.line,
1060            params.text_document_position.position.character,
1061        );
1062
1063        let result = config
1064            .analyzer
1065            .completion(
1066                ProgressToken::default(),
1067                params.text_document_position.text_document.uri,
1068                position,
1069                SourcePositionEncoding::UTF16,
1070            )
1071            .await
1072            .map_err(|e| RpcError {
1073                code: ErrorCode::InternalError,
1074                message: e.to_string().into(),
1075                data: None,
1076            })?;
1077
1078        Ok(result)
1079    }
1080
1081    async fn hover(&self, mut params: HoverParams) -> RpcResult<Option<Hover>> {
1082        let config = self.config.read().await;
1083
1084        normalize_uri_path(&mut params.text_document_position_params.text_document.uri);
1085
1086        debug!("received `textDocument/hover` request: {params:#?}");
1087
1088        let position = SourcePosition::new(
1089            params.text_document_position_params.position.line,
1090            params.text_document_position_params.position.character,
1091        );
1092
1093        let result = config
1094            .analyzer
1095            .hover(
1096                params.text_document_position_params.text_document.uri,
1097                position,
1098                SourcePositionEncoding::UTF16,
1099            )
1100            .await
1101            .map_err(|e| RpcError {
1102                code: ErrorCode::InternalError,
1103                message: e.to_string().into(),
1104                data: None,
1105            })?;
1106        Ok(result)
1107    }
1108
1109    async fn rename(&self, mut params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
1110        let config = self.config.read().await;
1111
1112        normalize_uri_path(&mut params.text_document_position.text_document.uri);
1113
1114        debug!("received `textDocument/rename` request: {params:#?}");
1115
1116        let position = SourcePosition::new(
1117            params.text_document_position.position.line,
1118            params.text_document_position.position.character,
1119        );
1120
1121        let result = config
1122            .analyzer
1123            .rename(
1124                params.text_document_position.text_document.uri,
1125                position,
1126                SourcePositionEncoding::UTF16,
1127                params.new_name,
1128            )
1129            .await
1130            .map_err(|e| RpcError {
1131                code: ErrorCode::InternalError,
1132                message: e.to_string().into(),
1133                data: None,
1134            })?;
1135
1136        Ok(result)
1137    }
1138
1139    async fn semantic_tokens_full(
1140        &self,
1141        mut params: SemanticTokensParams,
1142    ) -> RpcResult<Option<SemanticTokensResult>> {
1143        let config = self.config.read().await;
1144
1145        normalize_uri_path(&mut params.text_document.uri);
1146
1147        debug!("received `textDocument/semanticTokens/full` request: {params:#?}");
1148
1149        let result = config
1150            .analyzer
1151            .semantic_tokens(params.text_document.uri)
1152            .await
1153            .map_err(|e| RpcError {
1154                code: ErrorCode::InternalError,
1155                message: e.to_string().into(),
1156                data: None,
1157            })?;
1158
1159        Ok(result)
1160    }
1161
1162    async fn document_symbol(
1163        &self,
1164        mut params: DocumentSymbolParams,
1165    ) -> RpcResult<Option<DocumentSymbolResponse>> {
1166        let config = self.config.read().await;
1167
1168        normalize_uri_path(&mut params.text_document.uri);
1169
1170        debug!("received `textDocument/documentSymbol` request: {params:#?}");
1171
1172        let result = config
1173            .analyzer
1174            .document_symbol(params.text_document.uri)
1175            .await
1176            .map_err(|e| RpcError {
1177                code: ErrorCode::InternalError,
1178                message: e.to_string().into(),
1179                data: None,
1180            })?;
1181
1182        Ok(result)
1183    }
1184
1185    async fn symbol(
1186        &self,
1187        params: WorkspaceSymbolParams,
1188    ) -> RpcResult<Option<Vec<SymbolInformation>>> {
1189        let config = self.config.read().await;
1190
1191        debug!("received `workspace/symbol` request: {params:#?}");
1192
1193        let result = config
1194            .analyzer
1195            .workspace_symbol(params.query)
1196            .await
1197            .map_err(|e| RpcError {
1198                code: ErrorCode::InternalError,
1199                message: e.to_string().into(),
1200                data: None,
1201            })?;
1202
1203        Ok(result)
1204    }
1205
1206    async fn signature_help(
1207        &self,
1208        mut params: SignatureHelpParams,
1209    ) -> RpcResult<Option<SignatureHelp>> {
1210        let config = self.config.read().await;
1211
1212        normalize_uri_path(&mut params.text_document_position_params.text_document.uri);
1213
1214        debug!("received `textDocument/signatureHelp` request: {params:#?}");
1215
1216        let position = SourcePosition::new(
1217            params.text_document_position_params.position.line,
1218            params.text_document_position_params.position.character,
1219        );
1220
1221        let result = config
1222            .analyzer
1223            .signature_help(
1224                params.text_document_position_params.text_document.uri,
1225                position,
1226                SourcePositionEncoding::UTF16,
1227            )
1228            .await
1229            .map_err(|e| RpcError {
1230                code: ErrorCode::InternalError,
1231                message: e.to_string().into(),
1232                data: None,
1233            })?;
1234
1235        Ok(result)
1236    }
1237
1238    async fn inlay_hint(&self, mut params: InlayHintParams) -> RpcResult<Option<Vec<InlayHint>>> {
1239        let config = self.config.read().await;
1240
1241        normalize_uri_path(&mut params.text_document.uri);
1242
1243        debug!("received `textDocument/inlayHint` request: {params:#?}");
1244
1245        // Analyze the document first to ensure we have up-to-date information
1246        config
1247            .analyzer
1248            .analyze(ProgressToken(None))
1249            .await
1250            .map_err(|e| RpcError {
1251                code: ErrorCode::InternalError,
1252                message: e.to_string().into(),
1253                data: None,
1254            })?;
1255
1256        let result = config
1257            .analyzer
1258            .inlay_hints(params.text_document.uri, params.range)
1259            .await
1260            .map_err(|e| RpcError {
1261                code: ErrorCode::InternalError,
1262                message: e.to_string().into(),
1263                data: None,
1264            })?;
1265
1266        Ok(result)
1267    }
1268}