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