diff --git a/Cargo.lock b/Cargo.lock index 7a4813959..181adba30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,6 +533,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -557,6 +570,18 @@ dependencies = [ "libc", ] +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -583,8 +608,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -934,6 +961,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "notify" version = "5.0.0" @@ -1120,6 +1156,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "plotters" version = "0.3.4" @@ -1458,6 +1514,15 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1549,6 +1614,7 @@ dependencies = [ "env_logger", "fern", "flate2", + "flume", "fuzzy-matcher", "human_name", "insta", @@ -1579,7 +1645,6 @@ dependencies = [ "typed-builder", "unicode-normalization", "url", - "uuid", ] [[package]] @@ -1742,15 +1807,6 @@ dependencies = [ "serde", ] -[[package]] -name = "uuid" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" -dependencies = [ - "getrandom", -] - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 2a0db614a..aabbe8f77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ encoding_rs = "0.8.31" encoding_rs_io = "0.1.7" fern = "0.6.1" flate2 = "1.0.24" +flume = "0.10.14" fuzzy-matcher = { version = "0.3.7" } human_name = { version = "2.0.1", default-features = false } isocountry = "0.3.2" @@ -71,7 +72,6 @@ titlecase = "2.2.1" typed-builder = "0.11.0" unicode-normalization = "0.1.22" url = { version = "2.3.1", features = ["serde"] } -uuid = { version = "1.2.2", features = ["v4"] } [dependencies.salsa] git = "https://github.com/salsa-rs/salsa" diff --git a/src/client.rs b/src/client.rs index 56fdfd2fb..2508bad7a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -70,6 +70,11 @@ impl LspClient { Ok(serde_json::from_value(result)?) } + pub fn send_response(&self, response: lsp_server::Response) -> Result<()> { + self.raw.sender.send(response.into())?; + Ok(()) + } + pub fn recv_response(&self, response: lsp_server::Response) -> Result<()> { let (_, tx) = self .raw diff --git a/src/features.rs b/src/features.rs index e2abcb0f8..20a50aa40 100644 --- a/src/features.rs +++ b/src/features.rs @@ -1,4 +1,4 @@ -mod build; +pub mod building; mod completion; mod cursor; mod definition; @@ -22,7 +22,6 @@ use lsp_types::Url; use crate::{Document, Workspace}; pub use self::{ - build::{BuildEngine, BuildParams, BuildResult, BuildStatus}, completion::{complete, CompletionItemData, COMPLETION_LIMIT}, definition::goto_definition, execute_command::execute_command, diff --git a/src/features/build.rs b/src/features/build.rs deleted file mode 100644 index b8391dbb5..000000000 --- a/src/features/build.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::{ - io::{BufRead, BufReader, Read}, - path::Path, - process::{Command, Stdio}, - sync::{Arc, Mutex}, - thread::{self, JoinHandle}, -}; - -use anyhow::Result; -use crossbeam_channel::{Receiver, Sender}; -use dashmap::DashMap; -use encoding_rs_io::DecodeReaderBytesBuilder; -use lsp_types::{ - notification::{LogMessage, Progress}, - LogMessageParams, NumberOrString, Position, ProgressParams, ProgressParamsValue, - TextDocumentIdentifier, Url, WorkDoneProgress, WorkDoneProgressBegin, - WorkDoneProgressCreateParams, WorkDoneProgressEnd, -}; -use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use uuid::Uuid; - -use crate::{client::LspClient, ClientCapabilitiesExt}; - -use super::{FeatureRequest, ForwardSearch}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildParams { - pub text_document: TextDocumentIdentifier, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] -#[repr(i32)] -pub enum BuildStatus { - SUCCESS = 0, - ERROR = 1, - FAILURE = 2, - CANCELLED = 3, -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildResult { - pub status: BuildStatus, -} - -struct ProgressReporter<'a> { - supports_progress: bool, - client: LspClient, - token: &'a str, -} - -impl<'a> ProgressReporter<'a> { - pub fn start(&self, uri: &Url) -> Result<()> { - if self.supports_progress { - self.client - .send_request::( - WorkDoneProgressCreateParams { - token: NumberOrString::String(self.token.to_string()), - }, - )?; - - self.client.send_notification::(ProgressParams { - token: NumberOrString::String(self.token.to_string()), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin( - WorkDoneProgressBegin { - title: "Building".to_string(), - message: Some(uri.as_str().to_string()), - cancellable: Some(false), - percentage: None, - }, - )), - })?; - }; - Ok(()) - } -} - -impl<'a> Drop for ProgressReporter<'a> { - fn drop(&mut self) { - if self.supports_progress { - drop(self.client.send_notification::(ProgressParams { - token: NumberOrString::String(self.token.to_string()), - value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { - message: None, - })), - })); - } - } -} - -#[derive(Default)] -pub struct BuildEngine { - lock: Mutex<()>, - pub positions_by_uri: DashMap, Position>, -} - -impl BuildEngine { - pub fn build( - &self, - request: FeatureRequest, - client: LspClient, - ) -> Result { - let lock = self.lock.lock().unwrap(); - - let document = request - .workspace - .iter() - .find(|document| { - document - .data() - .as_latex() - .map_or(false, |data| data.extras.has_document_environment) - }) - .unwrap_or_else(|| request.main_document()); - - if !document.can_be_compiled() { - log::info!( - "Document {} cannot be compiled; skipping...", - document.uri() - ); - - return Ok(BuildResult { - status: BuildStatus::SUCCESS, - }); - } - - if document.uri().scheme() != "file" { - return Ok(BuildResult { - status: BuildStatus::FAILURE, - }); - } - - let path = document.uri().to_file_path().unwrap(); - - let supports_progress = request - .workspace - .environment - .client_capabilities - .has_work_done_progress_support(); - - let token = format!("texlab-build-{}", Uuid::new_v4()); - let progress_reporter = ProgressReporter { - supports_progress, - client: client.clone(), - token: &token, - }; - progress_reporter.start(document.uri())?; - - let options = &request.workspace.environment.options; - - let build_dir = options - .root_directory - .as_ref() - .map(AsRef::as_ref) - .or_else(|| path.parent()) - .unwrap(); - - let args: Vec<_> = options - .build - .args - .0 - .iter() - .map(|arg| replace_placeholder(arg.clone(), &path)) - .collect(); - - let mut process = Command::new(&options.build.executable.0) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .current_dir(build_dir) - .spawn()?; - - let (exit_sender, exit_receiver) = crossbeam_channel::bounded(1); - let log_handle = capture_output(&mut process, client, exit_receiver); - let success = process.wait().map(|status| status.success())?; - exit_sender.send(())?; - drop(exit_sender); - - log_handle.join().unwrap(); - let status = if success { - BuildStatus::SUCCESS - } else { - BuildStatus::ERROR - }; - - drop(progress_reporter); - drop(lock); - - if let Some((executable, args)) = options - .forward_search - .executable - .as_deref() - .zip(options.forward_search.args.as_deref()) - .filter(|_| options.build.forward_search_after) - { - let position = self - .positions_by_uri - .get(&request.uri) - .map(|entry| *entry.value()) - .unwrap_or_default(); - - ForwardSearch::builder() - .executable(executable) - .args(args) - .line(position.line) - .workspace(&request.workspace) - .tex_uri(&request.uri) - .build() - .execute(); - } - - Ok(BuildResult { status }) - } -} - -fn capture_output( - process: &mut std::process::Child, - client: LspClient, - exit_receiver: Receiver<()>, -) -> JoinHandle<()> { - let (log_sender, log_receiver) = crossbeam_channel::unbounded(); - track_output(process.stdout.take().unwrap(), log_sender.clone()); - track_output(process.stderr.take().unwrap(), log_sender); - thread::spawn(move || loop { - crossbeam_channel::select! { - recv(&log_receiver) -> message => { - if let Ok(message) = message { - client.send_notification::( - LogMessageParams { - message, - typ: lsp_types::MessageType::LOG, - }, - ) - .unwrap(); - } - }, - recv(&exit_receiver) -> _ => break, - }; - }) -} - -fn replace_placeholder(arg: String, file: &Path) -> String { - if arg.starts_with('"') || arg.ends_with('"') { - arg - } else { - arg.replace("%f", &file.to_string_lossy()) - } -} - -fn track_output(output: impl Read + Send + 'static, sender: Sender) -> JoinHandle<()> { - let reader = BufReader::new( - DecodeReaderBytesBuilder::new() - .encoding(Some(encoding_rs::UTF_8)) - .utf8_passthru(true) - .strip_bom(true) - .build(output), - ); - - thread::spawn(move || { - for line in reader.lines() { - sender.send(line.unwrap()).unwrap(); - } - }) -} diff --git a/src/features/building.rs b/src/features/building.rs new file mode 100644 index 000000000..4e8ca2432 --- /dev/null +++ b/src/features/building.rs @@ -0,0 +1,179 @@ +mod progress; + +use std::{ + io::{BufRead, BufReader, Read}, + path::Path, + process::{Command, Stdio}, + thread::{self, JoinHandle}, +}; + +use encoding_rs_io::DecodeReaderBytesBuilder; +use lsp_types::{notification::LogMessage, LogMessageParams, TextDocumentIdentifier, Url}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::{ + client::LspClient, + db::{document::Location, workspace::Workspace, Distro}, + ClientCapabilitiesExt, Db, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildParams { + pub text_document: TextDocumentIdentifier, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildResult { + pub status: BuildStatus, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum BuildStatus { + SUCCESS = 0, + ERROR = 1, + FAILURE = 2, + CANCELLED = 3, +} + +#[derive(Debug)] +pub struct TexCompiler { + uri: Url, + progress: bool, + command: Command, + client: LspClient, +} + +impl TexCompiler { + pub fn configure(db: &dyn Db, uri: Url, client: LspClient) -> Option { + let workspace = Workspace::get(db); + let location = Location::new(db, uri); + let document = match workspace.lookup(db, location) { + Some(child) => workspace + .parents(db, Distro::get(db), child) + .iter() + .next() + .copied() + .unwrap_or(child), + None => return None, + }; + + let uri = location.uri(db); + if uri.scheme() != "file" { + log::warn!("Document {uri} cannot be compiled; skipping..."); + return None; + } + + let options = &workspace.options(db).build; + let mut command = Command::new(&options.executable.0); + command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(workspace.working_dir(db, location).path(db).as_deref()?); + + let path = location.path(db).as_deref().unwrap(); + for arg in options.args.0.iter() { + command.arg(replace_placeholder(arg, path)); + } + + Some(Self { + uri: document.location(db).uri(db).clone(), + progress: workspace + .client_capabilities(db) + .has_work_done_progress_support(), + command, + client, + }) + } + + pub fn run(mut self) -> BuildStatus { + let reporter = if self.progress { + let inner = progress::Reporter::new(&self.client); + inner.start(&self.uri).expect("report progress"); + Some(inner) + } else { + None + }; + + let mut process = match self.command.spawn() { + Ok(process) => process, + Err(_) => { + log::error!("Failed to spawn process: {:?}", self.command.get_program()); + return BuildStatus::FAILURE; + } + }; + + let (line_sender, line_receiver) = flume::unbounded(); + let (exit_sender, exit_receiver) = flume::unbounded(); + track_output(process.stderr.take().unwrap(), line_sender.clone()); + track_output(process.stdout.take().unwrap(), line_sender); + let client = self.client.clone(); + let handle = std::thread::spawn(move || { + let typ = lsp_types::MessageType::LOG; + + loop { + let done = flume::Selector::new() + .recv(&line_receiver, |line| { + let message = line.unwrap(); + client + .send_notification::(LogMessageParams { message, typ }) + .unwrap(); + false + }) + .recv(&exit_receiver, |_| true) + .wait(); + + if done { + break; + } + } + }); + + let status = process.wait().map_or(BuildStatus::FAILURE, |result| { + if result.success() { + BuildStatus::SUCCESS + } else { + BuildStatus::ERROR + } + }); + + exit_sender + .send(()) + .expect("send exit signal to output reader"); + handle.join().unwrap(); + + drop(reporter); + status + } +} + +fn track_output( + output: impl Read + Send + 'static, + sender: flume::Sender, +) -> JoinHandle<()> { + let reader = BufReader::new( + DecodeReaderBytesBuilder::new() + .encoding(Some(encoding_rs::UTF_8)) + .utf8_passthru(true) + .strip_bom(true) + .build(output), + ); + + thread::spawn(move || { + for line in reader.lines() { + sender.send(line.unwrap()).unwrap(); + } + }) +} + +fn replace_placeholder(arg: &str, file: &Path) -> String { + if arg.starts_with('"') || arg.ends_with('"') { + arg.to_string() + } else { + arg.replace("%f", &file.to_string_lossy()) + } +} diff --git a/src/features/building/progress.rs b/src/features/building/progress.rs new file mode 100644 index 000000000..6f235bebd --- /dev/null +++ b/src/features/building/progress.rs @@ -0,0 +1,54 @@ +use std::sync::atomic::{AtomicI32, Ordering}; + +use anyhow::Result; +use lsp_types::{ + notification::Progress, request::WorkDoneProgressCreate, NumberOrString, ProgressParams, + ProgressParamsValue, Url, WorkDoneProgress, WorkDoneProgressBegin, + WorkDoneProgressCreateParams, WorkDoneProgressEnd, +}; + +use crate::client::LspClient; + +static NEXT_TOKEN: AtomicI32 = AtomicI32::new(1); + +pub struct Reporter<'a> { + client: &'a LspClient, + token: i32, +} + +impl<'a> Reporter<'a> { + pub fn new(client: &'a LspClient) -> Self { + let token = NEXT_TOKEN.fetch_add(1, Ordering::SeqCst); + Self { client, token } + } + + pub fn start(&self, uri: &Url) -> Result<()> { + self.client + .send_request::(WorkDoneProgressCreateParams { + token: NumberOrString::Number(self.token), + })?; + + self.client.send_notification::(ProgressParams { + token: NumberOrString::Number(self.token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin { + title: "Building".to_string(), + message: Some(uri.as_str().to_string()), + cancellable: Some(false), + percentage: None, + })), + })?; + + Ok(()) + } +} + +impl<'a> Drop for Reporter<'a> { + fn drop(&mut self) { + let _ = self.client.send_notification::(ProgressParams { + token: NumberOrString::Number(self.token), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd { + message: None, + })), + }); + } +} diff --git a/src/server.rs b/src/server.rs index bd6ba301b..e75ba59f4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,7 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; use anyhow::Result; use crossbeam_channel::{Receiver, Sender}; @@ -26,11 +29,11 @@ use crate::{ dispatch::{NotificationDispatcher, RequestDispatcher}, distro::Distribution, features::{ + building::{BuildParams, BuildResult, BuildStatus, TexCompiler}, execute_command, find_all_references, find_document_highlights, find_document_links, find_document_symbols, find_foldings, find_hover, find_inlay_hints, find_workspace_symbols, - format_source_code, goto_definition, prepare_rename_all, rename_all, BuildEngine, - BuildParams, BuildResult, BuildStatus, CompletionItemData, FeatureRequest, ForwardSearch, - ForwardSearchResult, ForwardSearchStatus, + format_source_code, goto_definition, prepare_rename_all, rename_all, CompletionItemData, + FeatureRequest, ForwardSearch, ForwardSearchResult, ForwardSearchStatus, }, normalize_uri, syntax::bibtex, @@ -43,17 +46,16 @@ enum InternalMessage { SetDistro(Distribution), SetOptions(Options), FileEvent(notify::Event), + ForwardSearch(Url), } struct ServerFork { connection: Arc, internal_tx: Sender, - client: LspClient, db: salsa::Snapshot, workspace: crate::Workspace, diagnostic_tx: debouncer::Sender, diagnostic_manager: DiagnosticManager, - build_engine: Arc, } impl ServerFork { @@ -77,7 +79,6 @@ pub struct Server { diagnostic_tx: debouncer::Sender, diagnostic_manager: DiagnosticManager, pool: ThreadPool, - build_engine: Arc, } impl Server { @@ -103,7 +104,6 @@ impl Server { diagnostic_tx, diagnostic_manager, pool: threadpool::Builder::new().build(), - build_engine: Arc::default(), } } @@ -116,12 +116,10 @@ impl Server { ServerFork { connection: self.connection.clone(), internal_tx: self.internal_tx.clone(), - client: self.client.clone(), db: self.db.snapshot(), workspace: self.workspace.clone(), diagnostic_tx: self.diagnostic_tx.clone(), diagnostic_manager: self.diagnostic_manager.clone(), - build_engine: self.build_engine.clone(), } } @@ -413,30 +411,8 @@ impl Server { let mut uri = params.text_document.uri; normalize_uri(&mut uri); - if let Some(request) = self - .workspace - .get(&uri) - .filter(|_| self.workspace.environment.options.build.on_save) - .map(|document| { - self.feature_request( - Arc::clone(document.uri()), - BuildParams { - text_document: TextDocumentIdentifier::new(uri.clone()), - }, - ) - }) - { - self.spawn(move |server| { - server - .build_engine - .build(request, server.client) - .unwrap_or_else(|why| { - error!("Build failed: {}", why); - BuildResult { - status: BuildStatus::FAILURE, - } - }); - }); + if Workspace::get(&self.db).options(&self.db).build.on_save { + self.build_internal(uri.clone(), |_| ())?; } if let Some(document) = self @@ -479,14 +455,6 @@ impl Server { }); } - fn feature_request

(&self, uri: Arc, params: P) -> FeatureRequest

{ - FeatureRequest { - params, - workspace: self.workspace.slice(&uri), - uri, - } - } - fn handle_feature_request( &self, id: RequestId, @@ -718,44 +686,110 @@ impl Server { Ok(()) } - fn build(&self, id: RequestId, mut params: BuildParams) -> Result<()> { - normalize_uri(&mut params.text_document.uri); - let uri = Arc::new(params.text_document.uri.clone()); + fn build(&mut self, id: RequestId, params: BuildParams) -> Result<()> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + let client = self.client.clone(); - let build_engine = Arc::clone(&self.build_engine); - self.handle_feature_request(id, params, uri, move |request| { - build_engine.build(request, client).unwrap_or_else(|why| { - error!("Build failed: {}", why); - BuildResult { - status: BuildStatus::FAILURE, - } - }) + self.build_internal(uri, move |status| { + let result = BuildResult { status }; + client + .send_response(lsp_server::Response::new_ok(id, result)) + .unwrap(); })?; + Ok(()) } - fn forward_search(&self, id: RequestId, mut params: TextDocumentPositionParams) -> Result<()> { - normalize_uri(&mut params.text_document.uri); - let uri = Arc::new(params.text_document.uri.clone()); - self.handle_feature_request(id, params, uri, |req| { - let options = &req.workspace.environment.options.forward_search; - match options.executable.as_deref().zip(options.args.as_deref()) { - Some((executable, args)) => ForwardSearch::builder() - .executable(executable) - .args(args) - .line(req.params.position.line) - .workspace(&req.workspace) - .tex_uri(&req.uri) - .build() - .execute() - .unwrap_or(ForwardSearchResult { - status: ForwardSearchStatus::ERROR, - }), - None => ForwardSearchResult { - status: ForwardSearchStatus::UNCONFIGURED, - }, + fn build_internal( + &mut self, + uri: Url, + callback: impl FnOnce(BuildStatus) + Send + 'static, + ) -> Result<()> { + static LOCK: Mutex<()> = Mutex::new(()); + + let compiler = match TexCompiler::configure(&self.db, uri.clone(), self.client.clone()) { + Some(compiler) => compiler, + None => { + callback(BuildStatus::FAILURE); + return Ok(()); + } + }; + + let forward_search_after = Workspace::get(&self.db) + .options(&self.db) + .build + .forward_search_after; + + let sender = self.internal_tx.clone(); + self.pool.execute(move || { + let guard = LOCK.lock().unwrap(); + + let status = compiler.run(); + if forward_search_after { + sender.send(InternalMessage::ForwardSearch(uri)).unwrap(); } + + drop(guard); + callback(status); + }); + + Ok(()) + } + + fn forward_search(&mut self, id: RequestId, params: TextDocumentPositionParams) -> Result<()> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + + let client = self.client.clone(); + self.forward_search_internal(uri, Some(params.position), move |status| { + let result = ForwardSearchResult { status }; + client + .send_response(lsp_server::Response::new_ok(id, result)) + .unwrap(); })?; + + Ok(()) + } + + fn forward_search_internal( + &mut self, + uri: Url, + position: Option, + callback: impl FnOnce(ForwardSearchStatus) + Send + 'static, + ) -> Result<()> { + let workspace = Workspace::get(&self.db); + let location = db::document::Location::new(&self.db, uri.clone()); + let document = match workspace.lookup(&self.db, location) { + Some(document) => document, + None => { + callback(ForwardSearchStatus::FAILURE); + return Ok(()); + } + }; + + let position = position.unwrap_or_else(|| { + document + .contents(&self.db) + .line_index(&self.db) + .line_col_lsp(document.cursor(&self.db)) + }); + + let options = &workspace.options(&self.db).forward_search; + let status = match options.executable.as_deref().zip(options.args.as_deref()) { + Some((executable, args)) => ForwardSearch::builder() + .line(position.line) + .tex_uri(&uri) + .executable(executable) + .args(args) + .workspace(&self.workspace) + .build() + .execute() + .map_or(ForwardSearchStatus::FAILURE, |result| result.status), + None => ForwardSearchStatus::UNCONFIGURED, + }; + + callback(status); Ok(()) } @@ -885,6 +919,9 @@ impl Server { InternalMessage::FileEvent(event) => { self.handle_file_event(event); } + InternalMessage::ForwardSearch(uri) => { + self.forward_search_internal(uri, None, |_| ())?; + } }; } };