Skip to main content

pytest_language_server/providers/
mod.rs

1//! LSP providers module.
2//!
3//! This module contains the Backend struct and LSP protocol handlers organized by provider type.
4
5pub mod call_hierarchy;
6pub mod code_action;
7pub mod code_lens;
8pub mod completion;
9pub mod definition;
10pub mod diagnostics;
11pub mod document_symbol;
12pub mod hover;
13pub mod implementation;
14pub mod inlay_hint;
15mod language_server;
16pub mod references;
17pub mod workspace_symbol;
18
19use crate::config::Config;
20use crate::fixtures::FixtureDatabase;
21use dashmap::DashMap;
22use std::path::PathBuf;
23use std::sync::Arc;
24use tower_lsp_server::ls_types::*;
25use tower_lsp_server::Client;
26use tracing::warn;
27
28/// The LSP Backend struct containing server state.
29pub struct Backend {
30    pub client: Client,
31    pub fixture_db: Arc<FixtureDatabase>,
32    /// The canonical workspace root path (resolved symlinks)
33    pub workspace_root: Arc<tokio::sync::RwLock<Option<PathBuf>>>,
34    /// The original workspace root path as provided by the client (may contain symlinks)
35    pub original_workspace_root: Arc<tokio::sync::RwLock<Option<PathBuf>>>,
36    /// Handle to the background workspace scan task, used for cancellation on shutdown
37    pub scan_task: Arc<tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>>,
38    /// Cache mapping canonical paths to original URIs from the client
39    /// This ensures we respond with URIs the client recognizes
40    pub uri_cache: Arc<DashMap<PathBuf, Uri>>,
41    /// Configuration loaded from pyproject.toml
42    pub config: Arc<tokio::sync::RwLock<Config>>,
43}
44
45impl Backend {
46    /// Create a new Backend instance
47    pub fn new(client: Client, fixture_db: Arc<FixtureDatabase>) -> Self {
48        Self {
49            client,
50            fixture_db,
51            workspace_root: Arc::new(tokio::sync::RwLock::new(None)),
52            original_workspace_root: Arc::new(tokio::sync::RwLock::new(None)),
53            scan_task: Arc::new(tokio::sync::Mutex::new(None)),
54            uri_cache: Arc::new(DashMap::new()),
55            config: Arc::new(tokio::sync::RwLock::new(Config::default())),
56        }
57    }
58
59    /// Convert URI to PathBuf with error logging
60    /// Canonicalizes the path to handle symlinks (e.g., /var -> /private/var on macOS)
61    pub fn uri_to_path(&self, uri: &Uri) -> Option<PathBuf> {
62        match uri.to_file_path() {
63            Some(path) => {
64                // Canonicalize to match how paths are stored in FixtureDatabase
65                // This handles symlinks like /var -> /private/var on macOS
66                let path = path.to_path_buf();
67                Some(path.canonicalize().unwrap_or(path))
68            }
69            None => {
70                warn!("Failed to convert URI to file path: {:?}", uri);
71                None
72            }
73        }
74    }
75
76    /// Convert PathBuf to URI with error logging
77    /// First checks the URI cache for a previously seen URI, then falls back to creating one
78    pub fn path_to_uri(&self, path: &std::path::Path) -> Option<Uri> {
79        // First, check if we have a cached URI for this path
80        // This ensures we use the same URI format the client originally sent
81        if let Some(cached_uri) = self.uri_cache.get(path) {
82            return Some(cached_uri.clone());
83        }
84
85        // For paths not in cache, we need to handle macOS symlink issue
86        // where /var is a symlink to /private/var
87        // The client sends /var/... but we store /private/var/...
88        // So we need to strip /private prefix when building URIs
89        let path_to_use: Option<PathBuf> = if cfg!(target_os = "macos") {
90            path.to_str().and_then(|path_str| {
91                if path_str.starts_with("/private/var/") || path_str.starts_with("/private/tmp/") {
92                    Some(PathBuf::from(path_str.replacen("/private", "", 1)))
93                } else {
94                    None
95                }
96            })
97        } else if cfg!(target_os = "windows") {
98            // Strip Windows extended-length path prefix (\\?\) which is added by canonicalize()
99            // This prefix causes Uri::from_file_path() to produce malformed URIs
100            path.to_str()
101                .and_then(|path_str| path_str.strip_prefix(r"\\?\"))
102                .map(PathBuf::from)
103        } else {
104            None
105        };
106
107        let final_path = path_to_use.as_deref().unwrap_or(path);
108
109        // Fall back to creating a new URI from the path
110        match Uri::from_file_path(final_path) {
111            Some(uri) => Some(uri),
112            None => {
113                warn!("Failed to convert path to URI: {:?}", path);
114                None
115            }
116        }
117    }
118
119    /// Convert LSP position (0-based line) to internal representation (1-based line)
120    pub fn lsp_line_to_internal(line: u32) -> usize {
121        (line + 1) as usize
122    }
123
124    /// Convert internal line (1-based) to LSP position (0-based)
125    pub fn internal_line_to_lsp(line: usize) -> u32 {
126        line.saturating_sub(1) as u32
127    }
128
129    /// Create a Range from start and end positions
130    pub fn create_range(start_line: u32, start_char: u32, end_line: u32, end_char: u32) -> Range {
131        Range {
132            start: Position {
133                line: start_line,
134                character: start_char,
135            },
136            end: Position {
137                line: end_line,
138                character: end_char,
139            },
140        }
141    }
142
143    /// Create a point Range (start == end) for a single position
144    pub fn create_point_range(line: u32, character: u32) -> Range {
145        Self::create_range(line, character, line, character)
146    }
147
148    /// Format fixture documentation for display (used in both hover and completions)
149    pub fn format_fixture_documentation(
150        fixture: &crate::fixtures::FixtureDefinition,
151        workspace_root: Option<&PathBuf>,
152    ) -> String {
153        let mut content = String::new();
154
155        // Calculate relative path from workspace root
156        let relative_path = if let Some(root) = workspace_root {
157            fixture
158                .file_path
159                .strip_prefix(root)
160                .ok()
161                .and_then(|p| p.to_str())
162                .map(|s| s.to_string())
163                .unwrap_or_else(|| {
164                    fixture
165                        .file_path
166                        .file_name()
167                        .and_then(|f| f.to_str())
168                        .unwrap_or("unknown")
169                        .to_string()
170                })
171        } else {
172            fixture
173                .file_path
174                .file_name()
175                .and_then(|f| f.to_str())
176                .unwrap_or("unknown")
177                .to_string()
178        };
179
180        // Add "from" line with relative path
181        content.push_str(&format!("**from** `{}`\n", relative_path));
182
183        // Add code block with fixture signature
184        let return_annotation = if let Some(ref ret_type) = &fixture.return_type {
185            format!(" -> {}", ret_type)
186        } else {
187            String::new()
188        };
189
190        content.push_str(&format!(
191            "```python\n@pytest.fixture\ndef {}(...){}:\n```",
192            fixture.name, return_annotation
193        ));
194
195        // Add docstring if present
196        if let Some(ref docstring) = fixture.docstring {
197            content.push_str("\n\n---\n\n");
198            content.push_str(docstring);
199        }
200
201        content
202    }
203}