diff --git a/pylight/src/index/symbol_index.rs b/pylight/src/index/symbol_index.rs index cc37a54..12d30dd 100644 --- a/pylight/src/index/symbol_index.rs +++ b/pylight/src/index/symbol_index.rs @@ -227,13 +227,20 @@ impl SymbolIndex { Ok(()) } - /// Parse and index a list of Python files in parallel + /// Parse and index a list of Python files in parallel with optional progress tracking /// Returns (number of files parsed, total symbols, elapsed time) - pub fn parse_and_index_files( + pub fn parse_and_index_files_with_progress( self: Arc, python_files: Vec, - ) -> Result<(usize, usize, std::time::Duration)> { + progress_callback: F, + ) -> Result<(usize, usize, std::time::Duration)> + where + F: Fn(usize, usize) + Send + Sync, + { let start_time = std::time::Instant::now(); + let total_files = python_files.len(); + let processed_count = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let progress_callback = Arc::new(progress_callback); // Process files in parallel and collect all results let all_file_symbols: Vec<(PathBuf, Vec)> = python_files @@ -256,7 +263,7 @@ impl SymbolIndex { }; // Read and parse the file - match std::fs::read_to_string(path) { + let result = match std::fs::read_to_string(path) { Ok(content) => match parser.parse_file(path, &content) { Ok(symbols) => { tracing::debug!( @@ -273,16 +280,18 @@ impl SymbolIndex { } }, Err(e) => { - // Only warn for errors other than "file not found" since that's expected - // during rapid file system changes (e.g., git operations) - if e.kind() != std::io::ErrorKind::NotFound { - tracing::warn!("Failed to read {}: {}", path.display(), e); - } else { + if e.kind() == std::io::ErrorKind::NotFound { tracing::debug!("File no longer exists: {}", path.display()); } None } - } + }; + + // Update progress counter and call callback + let current = processed_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; + progress_callback(current, total_files); + + result }) .collect(); @@ -301,29 +310,58 @@ impl SymbolIndex { Ok((file_count, total_symbols, elapsed)) } + /// Parse and index a list of Python files in parallel + /// Returns (number of files parsed, total symbols, elapsed time) + pub fn parse_and_index_files( + self: Arc, + python_files: Vec, + ) -> Result<(usize, usize, std::time::Duration)> { + // Call the progress version with a no-op callback + self.parse_and_index_files_with_progress(python_files, |_, _| {}) + } + /// Index all Python files in a workspace directory pub fn index_workspace(self: Arc, root: &PathBuf) -> Result<()> { + // Call the progress version with a no-op callback + self.index_workspace_with_progress(root, |_, _| {}) + } + + /// Index all Python files in a workspace directory with progress callback + pub fn index_workspace_with_progress( + self: Arc, + root: &PathBuf, + progress_callback: F, + ) -> Result<()> + where + F: Fn(usize, usize) + Send + Sync, + { tracing::info!("Starting workspace indexing for: {}", root.display()); // Collect all Python files first let file_collection_start = std::time::Instant::now(); let python_files = files::collect_python_files(root); let file_collection_elapsed = file_collection_start.elapsed(); + let total_files = python_files.len(); + tracing::info!( "Found {} Python files to index in {:.2}s (using {} threads)", - python_files.len(), + total_files, file_collection_elapsed.as_secs_f64(), num_cpus::get().saturating_sub(1).max(1) ); + // Call progress callback for file discovery phase + progress_callback(0, total_files); + // Log thread pool info tracing::info!( "Starting parallel processing with {} concurrent tasks", rayon::current_num_threads() ); - // Parse and index files - let (file_count, total_symbols, elapsed) = self.parse_and_index_files(python_files)?; + // Parse and index files with progress tracking + let (file_count, total_symbols, elapsed) = + self.parse_and_index_files_with_progress(python_files, progress_callback)?; tracing::info!( "Parallel parsing completed in {:.2}s ({:.0} files/sec)", diff --git a/pylight/src/lsp/mod.rs b/pylight/src/lsp/mod.rs index 398a39f..015fb7a 100644 --- a/pylight/src/lsp/mod.rs +++ b/pylight/src/lsp/mod.rs @@ -1,6 +1,7 @@ //! LSP server implementation pub mod handlers; +pub mod progress; pub mod server; pub use server::LspServer; diff --git a/pylight/src/lsp/progress.rs b/pylight/src/lsp/progress.rs new file mode 100644 index 0000000..14f66a7 --- /dev/null +++ b/pylight/src/lsp/progress.rs @@ -0,0 +1,187 @@ +//! Progress reporting utilities for LSP + +use lsp_server::{Connection, Message, Notification}; +use lsp_types::{ + notification::{Notification as NotificationTrait, Progress}, + request::{Request as LspRequest, WorkDoneProgressCreate}, + ProgressParams, ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin, + WorkDoneProgressCreateParams, WorkDoneProgressEnd, WorkDoneProgressReport, +}; +use std::sync::atomic::{AtomicU32, Ordering}; + +/// Unique token generator for progress reporting +static PROGRESS_TOKEN_COUNTER: AtomicU32 = AtomicU32::new(0); + +/// Helper for managing LSP progress reporting +pub struct ProgressReporter { + connection: Connection, + token: ProgressToken, +} + +impl ProgressReporter { + /// Create a new progress reporter + pub fn new(connection: Connection) -> Result> { + let token_id = PROGRESS_TOKEN_COUNTER.fetch_add(1, Ordering::SeqCst); + let token = ProgressToken::Number(token_id as i32); + + // Request permission to create progress + let create_params = WorkDoneProgressCreateParams { + token: token.clone(), + }; + + let request = lsp_server::Request { + id: lsp_server::RequestId::from(token_id as i32), + method: WorkDoneProgressCreate::METHOD.to_string(), + params: serde_json::to_value(create_params)?, + }; + + connection.sender.send(Message::Request(request))?; + + // Wait for response (with timeout) + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + if std::time::Instant::now() > deadline { + return Err("Timeout waiting for progress create response".into()); + } + + if let Ok(Message::Response(resp)) = connection + .receiver + .recv_timeout(std::time::Duration::from_millis(100)) + { + if resp.id == lsp_server::RequestId::from(token_id as i32) { + if resp.error.is_some() { + return Err("Client rejected progress creation".into()); + } + break; + } + } + } + + Ok(Self { connection, token }) + } + + /// Begin progress reporting + pub fn begin( + &self, + title: impl Into, + message: Option, + percentage: Option, + cancellable: bool, + ) -> Result<(), Box> { + let begin = WorkDoneProgressBegin { + title: title.into(), + cancellable: Some(cancellable), + message, + percentage, + }; + + self.send_progress(WorkDoneProgress::Begin(begin)) + } + + /// Report progress update + pub fn report( + &self, + message: Option, + percentage: Option, + ) -> Result<(), Box> { + let report = WorkDoneProgressReport { + cancellable: None, + message, + percentage, + }; + + self.send_progress(WorkDoneProgress::Report(report)) + } + + /// End progress reporting + pub fn end(&self, message: Option) -> Result<(), Box> { + let end = WorkDoneProgressEnd { message }; + self.send_progress(WorkDoneProgress::End(end)) + } + + /// Send a progress notification + fn send_progress(&self, progress: WorkDoneProgress) -> Result<(), Box> { + let params = ProgressParams { + token: self.token.clone(), + value: ProgressParamsValue::WorkDone(progress), + }; + + let notification = Notification { + method: ::METHOD.to_string(), + params: serde_json::to_value(params)?, + }; + + self.connection + .sender + .send(Message::Notification(notification))?; + + Ok(()) + } +} + +/// Simple progress reporter that sends notifications without waiting for client permission +pub struct SimpleProgressReporter { + sender: crossbeam_channel::Sender, + token: ProgressToken, +} + +impl SimpleProgressReporter { + /// Create a new simple progress reporter + pub fn new(sender: crossbeam_channel::Sender, token: ProgressToken) -> Self { + Self { sender, token } + } + + /// Begin progress reporting + pub fn begin( + &self, + title: impl Into, + message: Option, + percentage: Option, + ) -> Result<(), Box> { + let begin = WorkDoneProgressBegin { + title: title.into(), + cancellable: Some(false), + message, + percentage, + }; + + self.send_progress(WorkDoneProgress::Begin(begin)) + } + + /// Report progress update + pub fn report( + &self, + message: Option, + percentage: Option, + ) -> Result<(), Box> { + let report = WorkDoneProgressReport { + cancellable: None, + message, + percentage, + }; + + self.send_progress(WorkDoneProgress::Report(report)) + } + + /// End progress reporting + pub fn end(&self, message: Option) -> Result<(), Box> { + let end = WorkDoneProgressEnd { message }; + self.send_progress(WorkDoneProgress::End(end)) + } + + /// Send a progress notification + fn send_progress(&self, progress: WorkDoneProgress) -> Result<(), Box> { + let params = ProgressParams { + token: self.token.clone(), + value: ProgressParamsValue::WorkDone(progress), + }; + + let notification = Notification { + method: ::METHOD.to_string(), + params: serde_json::to_value(params)?, + }; + + self.sender.send(Message::Notification(notification))?; + Ok(()) + } +} diff --git a/pylight/src/lsp/server.rs b/pylight/src/lsp/server.rs index cf5854c..bd63e78 100644 --- a/pylight/src/lsp/server.rs +++ b/pylight/src/lsp/server.rs @@ -5,13 +5,15 @@ use crate::parser::ParserBackend; use crate::watcher::{FileWatcher, WatcherConfig}; use crate::{Error, Result, SearchEngine, SymbolIndex}; use lsp_server::{Connection, Message, RequestId, Response}; -use lsp_types::{InitializeParams, ServerCapabilities, WorkspaceSymbolParams}; +use lsp_types::{InitializeParams, ProgressToken, ServerCapabilities, WorkspaceSymbolParams}; use parking_lot::Mutex; use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use std::thread; +use super::progress::SimpleProgressReporter; + pub struct LspServer { connection: Connection, index: Arc, @@ -19,6 +21,7 @@ pub struct LspServer { workspace_root: Option, cancelled_requests: Arc>>, _file_watcher: Option, + client_supports_progress: bool, } impl LspServer { @@ -32,6 +35,7 @@ impl LspServer { workspace_root: None, cancelled_requests: Arc::new(Mutex::new(HashSet::new())), _file_watcher: None, + client_supports_progress: false, }) } @@ -47,8 +51,16 @@ impl LspServer { .initialize(serde_json::to_value(server_capabilities).unwrap()) .map_err(|e| Error::Lsp(format!("Failed to initialize: {e}")))?; - // Extract workspace root + // Extract workspace root and client capabilities if let Ok(params) = serde_json::from_value::(initialization_params) { + // Check if client supports work done progress + if let Some(capabilities) = params.capabilities.window.as_ref() { + self.client_supports_progress = capabilities.work_done_progress.unwrap_or(false); + tracing::info!( + "Client work done progress support: {}", + self.client_supports_progress + ); + } #[allow(deprecated)] if let Some(root_uri) = params.root_uri { if let Ok(url) = url::Url::parse(root_uri.as_str()) { @@ -85,11 +97,70 @@ impl LspServer { } } + // Clone sender for progress reporting + let sender = self.connection.sender.clone(); + let supports_progress = self.client_supports_progress; + thread::spawn(move || { - if let Err(e) = index.index_workspace(&root) { - tracing::error!("Failed to index workspace: {}", e); + if supports_progress { + // Create a progress reporter for indexing + let progress_token = + ProgressToken::String("pylight-indexing".to_string()); + let progress = SimpleProgressReporter::new(sender, progress_token); + + // Begin progress + if let Err(e) = progress.begin( + "Indexing Python workspace", + Some("Discovering Python files...".to_string()), + None, + ) { + tracing::warn!("Failed to send progress begin: {}", e); + } + + // Run indexing with progress updates + match index.clone().index_workspace_with_progress( + &root, + |current, total| { + let percentage = if total > 0 { + Some(((current as f64 / total as f64) * 100.0) as u32) + } else { + None + }; + + let message = + Some(format!("Indexing file {current} of {total}")); + + if let Err(e) = progress.report(message, percentage) { + tracing::warn!("Failed to send progress update: {}", e); + } + }, + ) { + Ok(_) => { + // End progress with success + if let Err(e) = + progress.end(Some("Indexing complete".to_string())) + { + tracing::warn!("Failed to send progress end: {}", e); + } + tracing::info!("Initial workspace indexing completed"); + } + Err(e) => { + // End progress with error + if let Err(e) = + progress.end(Some(format!("Indexing failed: {e}"))) + { + tracing::warn!("Failed to send progress end: {}", e); + } + tracing::error!("Failed to index workspace: {}", e); + } + } } else { - tracing::info!("Initial workspace indexing completed"); + // Run indexing without progress updates + if let Err(e) = index.index_workspace(&root) { + tracing::error!("Failed to index workspace: {}", e); + } else { + tracing::info!("Initial workspace indexing completed"); + } } }); }