Refactor textDocument/build request

This commit is contained in:
Patrick Förster 2022-10-15 15:47:27 +02:00
parent ab36f6f712
commit 2323ef7f85
9 changed files with 217 additions and 297 deletions

10
Cargo.lock generated
View file

@ -1522,7 +1522,6 @@ dependencies = [
"typed-builder",
"unicode-normalization",
"url",
"uuid",
]
[[package]]
@ -1685,15 +1684,6 @@ dependencies = [
"serde",
]
[[package]]
name = "uuid"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.4"

View file

@ -70,7 +70,6 @@ titlecase = "2.2.0"
typed-builder = "0.10.0"
unicode-normalization = "0.1.22"
url = { version = "2.3.1", features = ["serde"] }
uuid = { version = "1.1.2", features = ["v4"] }
[dependencies.derive_more]
version = "0.99.17"

View file

@ -11,6 +11,7 @@ mod hover;
mod inlay_hint;
mod link;
mod lsp_kinds;
pub(crate) mod progress;
mod reference;
mod rename;
mod symbol;
@ -22,7 +23,7 @@ use lsp_types::Url;
use crate::{Document, Workspace};
pub use self::{
build::{BuildEngine, BuildParams, BuildResult, BuildStatus},
build::{BuildParams, BuildResult, BuildRunner, BuildStatus},
completion::{complete, CompletionItemData, COMPLETION_LIMIT},
definition::goto_definition,
execute_command::execute_command,

View file

@ -2,27 +2,19 @@ use std::{
io::{BufRead, BufReader, Read},
path::Path,
process::{Command, Stdio},
sync::{Arc, Mutex},
thread::{self, JoinHandle},
thread::JoinHandle,
};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender};
use dashmap::DashMap;
use anyhow::{Ok, Result};
use encoding_rs_io::DecodeReaderBytesBuilder;
use lsp_types::{
notification::{LogMessage, Progress},
LogMessageParams, NumberOrString, Position, ProgressParams, ProgressParamsValue,
TextDocumentIdentifier, Url, WorkDoneProgress, WorkDoneProgressBegin,
WorkDoneProgressCreateParams, WorkDoneProgressEnd,
};
use lsp_types::{notification::LogMessage, LogMessageParams, TextDocumentIdentifier, Url};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use uuid::Uuid;
use typed_builder::TypedBuilder;
use crate::{client::LspClient, ClientCapabilitiesExt, DocumentLanguage};
use crate::{client::LspClient, DocumentLanguage, Workspace};
use super::{FeatureRequest, ForwardSearch};
use super::progress::ProgressReporter;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -45,196 +37,93 @@ pub struct BuildResult {
pub status: BuildStatus,
}
struct ProgressReporter<'a> {
supports_progress: bool,
client: LspClient,
token: &'a str,
#[derive(TypedBuilder)]
pub struct BuildRunner<'a> {
executable: &'a str,
args: &'a [String],
workspace: &'a Workspace,
tex_uri: &'a Url,
client: &'a LspClient,
report_progress: bool,
}
impl<'a> ProgressReporter<'a> {
pub fn start(&self, uri: &Url) -> Result<()> {
if self.supports_progress {
self.client
.send_request::<lsp_types::request::WorkDoneProgressCreate>(
WorkDoneProgressCreateParams {
token: NumberOrString::String(self.token.to_string()),
},
)?;
self.client.send_notification::<Progress>(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::<Progress>(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<Arc<Url>, Position>,
}
impl BuildEngine {
pub fn build(
&self,
request: FeatureRequest<BuildParams>,
client: LspClient,
) -> Result<BuildResult> {
let lock = self.lock.lock().unwrap();
let document = request
impl<'a> BuildRunner<'a> {
pub fn run(self) -> Result<BuildStatus> {
let path = match self
.workspace
.iter()
.find(|document| {
if let Some(data) = document.data().as_latex() {
data.extras.has_document_environment
} else {
false
}
document
.data()
.as_latex()
.map_or(false, |document| document.extras.can_be_built)
})
.unwrap_or_else(|| request.main_document());
if document.data().language() != DocumentLanguage::Latex {
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,
.or_else(|| self.workspace.get(self.tex_uri))
.filter(|document| document.data().language() == DocumentLanguage::Latex)
.filter(|document| document.uri().scheme() == "file")
.and_then(|document| document.uri().to_file_path().ok())
{
Some(path) => path,
None => return Ok(BuildStatus::FAILURE),
};
progress_reporter.start(document.uri())?;
let options = &request.workspace.environment.options;
let reporter = if self.report_progress {
Some(ProgressReporter::new(
self.client,
"Building".to_string(),
path.display().to_string(),
)?)
} else {
None
};
let build_dir = options
.root_directory
.as_ref()
.map(AsRef::as_ref)
.or_else(|| path.parent())
.unwrap();
let args: Vec<_> = options
.build
let args: Vec<_> = self
.args
.0
.iter()
.map(|arg| replace_placeholder(arg.clone(), &path))
.collect();
let mut process = Command::new(&options.build.executable.0)
let mut process = Command::new(self.executable)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.current_dir(path.parent().unwrap())
.stderr(Stdio::piped())
.current_dir(build_dir)
.stdout(Stdio::piped())
.stdin(Stdio::null())
.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);
self.track_output(process.stdout.take().unwrap());
self.track_output(process.stderr.take().unwrap());
log_handle.join().unwrap();
let status = if success {
let status = if process.wait()?.success() {
BuildStatus::SUCCESS
} else {
BuildStatus::ERROR
BuildStatus::FAILURE
};
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 })
drop(reporter);
Ok(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::<LogMessage>(
LogMessageParams {
message,
typ: lsp_types::MessageType::LOG,
},
)
fn track_output(&self, output: impl Read + Send + 'static) -> JoinHandle<()> {
let client = self.client.clone();
let reader = BufReader::new(
DecodeReaderBytesBuilder::new()
.encoding(Some(encoding_rs::UTF_8))
.utf8_passthru(true)
.strip_bom(true)
.build(output),
);
std::thread::spawn(move || {
for line in reader.lines() {
let message = line.unwrap();
let typ = lsp_types::MessageType::LOG;
client
.send_notification::<LogMessage>(LogMessageParams { message, typ })
.unwrap();
}
},
recv(&exit_receiver) -> _ => break,
};
})
}
})
}
}
fn replace_placeholder(arg: String, file: &Path) -> String {
@ -244,19 +133,3 @@ fn replace_placeholder(arg: String, file: &Path) -> String {
arg.replace("%f", &file.to_string_lossy())
}
}
fn track_output(output: impl Read + Send + 'static, sender: Sender<String>) -> 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();
}
})
}

View file

@ -37,22 +37,15 @@ pub struct ForwardSearch<'a> {
}
impl<'a> ForwardSearch<'a> {
pub fn execute(self) -> Option<ForwardSearchResult> {
pub fn execute(self) -> Option<ForwardSearchStatus> {
let root_document = self
.workspace
.iter()
.find(|document| {
if let Some(data) = document.data().as_latex() {
data.extras.has_document_environment
&& !data
.extras
.explicit_links
.iter()
.filter_map(|link| link.as_component_name())
.any(|name| name == "subfiles.cls")
} else {
false
}
document
.data()
.as_latex()
.map_or(false, |data| data.extras.can_be_root)
})
.filter(|document| document.uri().scheme() == "file")?;
@ -81,7 +74,7 @@ impl<'a> ForwardSearch<'a> {
}
};
Some(ForwardSearchResult { status })
Some(status)
}
}

51
src/features/progress.rs Normal file
View file

@ -0,0 +1,51 @@
use std::sync::atomic::{AtomicI32, Ordering};
use anyhow::Result;
use lsp_types::{
notification::Progress, NumberOrString, ProgressParams, ProgressParamsValue, WorkDoneProgress,
WorkDoneProgressBegin, WorkDoneProgressCreateParams, WorkDoneProgressEnd,
};
use crate::client::LspClient;
static NEXT_TOKEN: AtomicI32 = AtomicI32::new(1);
pub struct ProgressReporter<'a> {
client: &'a LspClient,
token: i32,
}
impl<'a> ProgressReporter<'a> {
pub fn new(client: &'a LspClient, title: String, message: String) -> Result<Self> {
let token = NEXT_TOKEN.fetch_add(1, Ordering::SeqCst);
client.send_request::<lsp_types::request::WorkDoneProgressCreate>(
WorkDoneProgressCreateParams {
token: NumberOrString::Number(token),
},
)?;
client.send_notification::<Progress>(ProgressParams {
token: NumberOrString::Number(token),
value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin {
title,
message: Some(message),
cancellable: Some(false),
percentage: None,
})),
})?;
Ok(Self { client, token })
}
}
impl<'a> Drop for ProgressReporter<'a> {
fn drop(&mut self) {
let _ = self.client.send_notification::<Progress>(ProgressParams {
token: NumberOrString::String(self.token.to_string()),
value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd {
message: None,
})),
});
}
}

View file

@ -5,6 +5,7 @@ use std::{
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender};
use dashmap::DashMap;
use log::{error, info, warn};
use lsp_server::{Connection, Message, RequestId};
use lsp_types::{notification::*, request::*, *};
@ -23,8 +24,8 @@ use crate::{
features::{
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,
format_source_code, goto_definition, prepare_rename_all, rename_all, BuildParams,
BuildResult, BuildRunner, BuildStatus, CompletionItemData, FeatureRequest, ForwardSearch,
ForwardSearchResult, ForwardSearchStatus,
},
normalize_uri,
@ -33,6 +34,8 @@ use crate::{
LineIndexExt, Options, StartupOptions, Workspace, WorkspaceEvent,
};
static BUILD_LOCK: Mutex<()> = Mutex::new(());
#[derive(Debug)]
enum InternalMessage {
SetDistro(Distribution),
@ -47,7 +50,7 @@ struct ServerSnapshot {
workspace: Workspace,
diagnostic_tx: debouncer::Sender<Workspace>,
diagnostic_manager: DiagnosticManager,
build_engine: Arc<BuildEngine>,
cursor_positions: Arc<DashMap<Arc<Url>, Position>>,
}
impl ServerSnapshot {
@ -111,6 +114,49 @@ impl ServerSnapshot {
Ok(())
}
fn build(&self, uri: &Url) -> BuildResult {
let guard = BUILD_LOCK.lock().unwrap();
let client_capabilities = &self.workspace.environment.client_capabilities;
let options = &self.workspace.environment.options.build;
let status = BuildRunner::builder()
.executable(&options.executable.0)
.args(&options.args.0)
.tex_uri(uri)
.client(&self.client)
.report_progress(client_capabilities.has_work_done_progress_support())
.workspace(&self.workspace)
.build()
.run()
.unwrap_or(BuildStatus::FAILURE);
if options.forward_search_after {
let line = self.cursor_positions.get(uri).map_or(0, |pos| pos.line);
self.forward_search(uri, line);
}
drop(guard);
BuildResult { status }
}
fn forward_search(&self, uri: &Url, line: u32) -> ForwardSearchResult {
let options = &self.workspace.environment.options.forward_search;
let status = match options.executable.as_deref().zip(options.args.as_deref()) {
Some((executable, args)) => ForwardSearch::builder()
.executable(executable)
.args(args)
.line(line)
.workspace(&self.workspace)
.tex_uri(uri)
.build()
.execute()
.unwrap_or(ForwardSearchStatus::ERROR),
None => ForwardSearchStatus::UNCONFIGURED,
};
ForwardSearchResult { status }
}
fn feature_request<P>(&self, uri: Arc<Url>, params: P) -> FeatureRequest<P> {
FeatureRequest {
params,
@ -129,7 +175,7 @@ pub struct Server {
diagnostic_tx: debouncer::Sender<Workspace>,
diagnostic_manager: DiagnosticManager,
pool: Arc<Mutex<ThreadPool>>,
build_engine: Arc<BuildEngine>,
cursor_positions: Arc<DashMap<Arc<Url>, Position>>,
}
impl Server {
@ -147,8 +193,8 @@ impl Server {
workspace,
diagnostic_tx,
diagnostic_manager,
cursor_positions: Arc::default(),
pool: Arc::new(Mutex::new(threadpool::Builder::new().build())),
build_engine: Arc::default(),
}
}
@ -160,7 +206,7 @@ impl Server {
client: self.client.clone(),
diagnostic_tx: self.diagnostic_tx.clone(),
diagnostic_manager: self.diagnostic_manager.clone(),
build_engine: Arc::clone(&self.build_engine),
cursor_positions: Arc::clone(&self.cursor_positions),
};
self.pool.lock().unwrap().execute(move || job(server));
@ -359,7 +405,7 @@ impl Server {
.viewport
.insert(Arc::clone(new_document.uri()));
self.build_engine.positions_by_uri.insert(
self.cursor_positions.insert(
Arc::clone(&uri),
Position::new(
old_document
@ -374,7 +420,7 @@ impl Server {
if self.workspace.environment.options.chktex.on_edit {
self.run_chktex(new_document);
};
}
}
None => match uri.to_file_path() {
Ok(path) => {
@ -390,38 +436,19 @@ impl Server {
fn did_save(&mut self, params: DidSaveTextDocumentParams) -> Result<()> {
let mut uri = params.text_document.uri;
normalize_uri(&mut uri);
let document = match self.workspace.get(&uri) {
Some(document) => document,
_ => return Ok(()),
};
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()),
},
)
})
{
if self.workspace.environment.options.build.on_save {
let uri = Arc::clone(document.uri());
self.spawn(move |server| {
server
.build_engine
.build(request, server.client)
.unwrap_or_else(|why| {
error!("Build failed: {}", why);
BuildResult {
status: BuildStatus::FAILURE,
}
});
server.build(&uri);
});
}
if let Some(document) = self
.workspace
.get(&uri)
.filter(|_| self.workspace.environment.options.chktex.on_open_and_save)
{
if self.workspace.environment.options.chktex.on_open_and_save {
self.run_chktex(document);
}
@ -448,14 +475,6 @@ impl Server {
});
}
fn feature_request<P>(&self, uri: Arc<Url>, params: P) -> FeatureRequest<P> {
FeatureRequest {
params,
workspace: self.workspace.slice(&uri),
uri,
}
}
fn handle_feature_request<P, R, H>(
&self,
id: RequestId,
@ -518,8 +537,7 @@ impl Server {
normalize_uri(&mut params.text_document_position.text_document.uri);
let uri = Arc::new(params.text_document_position.text_document.uri.clone());
self.build_engine
.positions_by_uri
self.cursor_positions
.insert(Arc::clone(&uri), params.text_document_position.position);
self.handle_feature_request(id, params, uri, crate::features::complete)?;
@ -587,7 +605,8 @@ impl Server {
.uri
.clone(),
);
self.build_engine.positions_by_uri.insert(
self.cursor_positions.insert(
Arc::clone(&uri),
params.text_document_position_params.position,
);
@ -684,42 +703,29 @@ impl Server {
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());
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.spawn(move |server| {
let result = server.build(&params.text_document.uri);
server
.connection
.sender
.send(lsp_server::Response::new_ok(id, result).into())
.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,
},
}
})?;
self.spawn(move |server| {
let result = server.forward_search(&params.text_document.uri, params.position.line);
server
.connection
.sender
.send(lsp_server::Response::new_ok(id, result).into())
.unwrap();
});
Ok(())
}

View file

@ -45,9 +45,15 @@ pub fn analyze(context: &mut LatexAnalyzerContext, root: &latex::SyntaxNode) {
.filter_map(ExplicitLink::as_component_name)
.any(|name| name == "subfiles.cls");
context.extras.can_be_built = context
.extras
.explicit_links
.iter()
.any(|link| link.kind == ExplicitLinkKind::Class);
context.extras.can_be_root = context
.extras
.explicit_links
.iter()
.any(|link| link.kind == ExplicitLinkKind::Class && link.stem.as_str() != "subfiles")
.any(|link| link.kind == ExplicitLinkKind::Class && link.stem.as_str() != "subfiles");
}

View file

@ -25,6 +25,7 @@ pub struct Extras {
pub label_numbers_by_name: FxHashMap<String, String>,
pub theorem_environments: Vec<TheoremEnvironment>,
pub graphics_paths: FxHashSet<String>,
pub can_be_built: bool,
pub can_be_root: bool,
pub has_document_environment: bool,
pub has_subfiles_package: bool,