summaryrefslogtreecommitdiff
path: root/support/texlab/src/test/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/test/mod.rs')
-rw-r--r--support/texlab/src/test/mod.rs632
1 files changed, 632 insertions, 0 deletions
diff --git a/support/texlab/src/test/mod.rs b/support/texlab/src/test/mod.rs
new file mode 100644
index 0000000000..5f26fb0dcc
--- /dev/null
+++ b/support/texlab/src/test/mod.rs
@@ -0,0 +1,632 @@
+#![allow(deprecated)]
+
+mod client;
+mod server;
+
+pub use self::{
+ client::{TestLatexLspClient, TestLspClient},
+ server::TestLatexLspServer,
+};
+
+use crate::{
+ protocol::*,
+ server::LatexLspServer,
+ tex::{Distribution, UnknownDistribution},
+};
+use futures::{
+ channel::mpsc,
+ future::{join, AbortHandle, Abortable},
+ lock::Mutex,
+ prelude::*,
+};
+use jsonrpc::MessageHandler;
+use once_cell::sync::Lazy;
+use std::{path::PathBuf, sync::Arc};
+use tempfile::{tempdir, TempDir};
+use tokio::fs;
+
+struct GlobalDistribution {
+ distro: Mutex<Option<Arc<dyn Distribution>>>,
+}
+
+impl GlobalDistribution {
+ fn new() -> Self {
+ Self {
+ distro: Mutex::new(None),
+ }
+ }
+
+ async fn get(&self, use_distro: bool) -> Arc<dyn Distribution> {
+ if use_distro {
+ let mut distro_lock = self.distro.lock().await;
+ match &*distro_lock {
+ Some(distro) => distro.clone(),
+ None => {
+ let distro = Distribution::detect().await;
+ *distro_lock = Some(distro.clone());
+ distro
+ }
+ }
+ } else {
+ Arc::new(UnknownDistribution::default())
+ }
+ }
+}
+
+static DISTRO: Lazy<GlobalDistribution> = Lazy::new(GlobalDistribution::new);
+
+#[derive(Debug)]
+struct Endpoint<S, C> {
+ receiver: mpsc::Receiver<String>,
+ client: Arc<C>,
+ server: Arc<S>,
+ handler: MessageHandler<S, C>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct TestBedBuilder {
+ files: Vec<(PathBuf, String)>,
+ use_distro: bool,
+ root_dir: Option<PathBuf>,
+ latex_build: Option<LatexBuildOptions>,
+ latex_lint: Option<LatexLintOptions>,
+ latex_forward_search: Option<LatexForwardSearchOptions>,
+ bibtex_formatting: Option<BibtexFormattingOptions>,
+}
+
+impl TestBedBuilder {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn file<P: Into<PathBuf>>(&mut self, path: P, text: &str) -> &mut Self {
+ self.files.push((path.into(), text.trim().into()));
+ self
+ }
+
+ pub fn use_distro(&mut self) -> &mut Self {
+ self.use_distro = true;
+ self
+ }
+
+ pub fn root_dir<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
+ self.root_dir = Some(path.into());
+ self
+ }
+
+ pub fn latex_build(&mut self, options: LatexBuildOptions) -> &mut Self {
+ self.latex_build = Some(options);
+ self
+ }
+
+ pub fn latex_lint(&mut self, options: LatexLintOptions) -> &mut Self {
+ self.latex_lint = Some(options);
+ self
+ }
+
+ pub fn latex_forward_search(&mut self, options: LatexForwardSearchOptions) -> &mut Self {
+ self.latex_forward_search = Some(options);
+ self
+ }
+
+ pub fn bibtex_formatting(&mut self, options: BibtexFormattingOptions) -> &mut Self {
+ self.bibtex_formatting = Some(options);
+ self
+ }
+
+ pub async fn build(&self) -> TestBed {
+ let dir = tempdir().expect("failed to create temporary directory");
+ for (path, text) in &self.files {
+ let full_path = dir.path().join(path);
+ fs::create_dir_all(full_path.parent().unwrap())
+ .await
+ .unwrap();
+ fs::write(&full_path, text).await.unwrap();
+ }
+
+ let (tx1, rx1) = mpsc::channel(0);
+ let (tx2, rx2) = mpsc::channel(0);
+
+ let endpoint1 = self.build_endpoint1(&dir, tx2, rx1).await;
+ let endpoint2 = self.build_endpoint2(&dir, tx1, rx2).await;
+ let server = Arc::clone(&endpoint2.server);
+ let client = Arc::clone(&endpoint2.client);
+ TestBed {
+ dir,
+ server,
+ client,
+ endpoint1: Some(endpoint1),
+ endpoint2: Some(endpoint2),
+ handle: None,
+ }
+ }
+
+ async fn build_endpoint1(
+ &self,
+ dir: &TempDir,
+ tx2: mpsc::Sender<String>,
+ rx1: mpsc::Receiver<String>,
+ ) -> Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient> {
+ let client = Arc::new(LatexLspClient::new(tx2.clone()));
+ let server = Arc::new(LatexLspServer::new(
+ DISTRO.get(self.use_distro).await,
+ Arc::clone(&client),
+ Arc::new(dir.path().to_path_buf()),
+ ));
+
+ Endpoint {
+ receiver: rx1,
+ client: Arc::clone(&client),
+ server: Arc::clone(&server),
+ handler: MessageHandler {
+ client,
+ server,
+ output: tx2.clone(),
+ },
+ }
+ }
+
+ async fn build_endpoint2(
+ &self,
+ dir: &TempDir,
+ tx1: mpsc::Sender<String>,
+ rx2: mpsc::Receiver<String>,
+ ) -> Endpoint<TestLatexLspServer, TestLatexLspClient> {
+ let options = Options {
+ latex: Some(LatexOptions {
+ root_directory: self.root_dir.as_ref().map(|path| dir.path().join(path)),
+ build: self.latex_build.clone(),
+ forward_search: self.latex_forward_search.clone(),
+ lint: self.latex_lint.clone(),
+ }),
+ bibtex: Some(BibtexOptions {
+ formatting: self.bibtex_formatting.clone(),
+ }),
+ };
+
+ let test_client = Arc::new(TestLatexLspClient::new(tx1.clone()));
+ let test_server = Arc::new(TestLatexLspServer::new(options));
+
+ Endpoint {
+ receiver: rx2,
+ client: Arc::clone(&test_client),
+ server: Arc::clone(&test_server),
+ handler: MessageHandler {
+ client: test_client,
+ server: test_server,
+ output: tx1,
+ },
+ }
+ }
+}
+
+pub struct TestBed {
+ pub dir: TempDir,
+ pub server: Arc<TestLatexLspServer>,
+ pub client: Arc<TestLatexLspClient>,
+ endpoint1: Option<Endpoint<LatexLspServer<LatexLspClient>, LatexLspClient>>,
+ endpoint2: Option<Endpoint<TestLatexLspServer, TestLatexLspClient>>,
+ handle: Option<AbortHandle>,
+}
+
+impl TestBed {
+ pub fn spawn(&mut self) {
+ let (handle, reg) = AbortHandle::new_pair();
+
+ let endpoint1 = self.endpoint1.take().unwrap();
+ let endpoint2 = self.endpoint2.take().unwrap();
+ let mut rx1 = endpoint1.receiver;
+ let mut rx2 = endpoint2.receiver;
+ let mut handler1 = endpoint1.handler;
+ let mut handler2 = endpoint2.handler;
+
+ tokio::spawn(Abortable::new(
+ async move {
+ let task1 = async move {
+ while let Some(json) = rx2.next().await {
+ handler2.handle(&json).await;
+ }
+ };
+
+ let task2 = async move {
+ while let Some(json) = rx1.next().await {
+ handler1.handle(&json).await;
+ }
+ };
+
+ join(task1, task2).await;
+ },
+ reg,
+ ));
+ self.handle = Some(handle);
+ }
+
+ pub async fn initialize(&self, capabilities: ClientCapabilities) {
+ self.client
+ .initialize(InitializeParams {
+ capabilities,
+ initialization_options: None,
+ process_id: None,
+ root_path: None,
+ root_uri: None,
+ trace: None,
+ workspace_folders: None,
+ client_info: None,
+ })
+ .await
+ .unwrap();
+ self.client.initialized(InitializedParams {}).await;
+ }
+
+ pub fn path(&self, relative_path: &str) -> PathBuf {
+ self.dir.path().join(relative_path)
+ }
+
+ pub fn uri(&self, relative_path: &str) -> Uri {
+ Uri::from_file_path(self.path(relative_path)).unwrap()
+ }
+
+ pub fn identifier(&self, relative_path: &str) -> TextDocumentIdentifier {
+ TextDocumentIdentifier::new(self.uri(relative_path).into())
+ }
+
+ pub async fn open(&self, relative_path: &str) {
+ let full_path = self.path(relative_path);
+ let params = DidOpenTextDocumentParams {
+ text_document: TextDocumentItem {
+ language_id: if relative_path.ends_with("bib") {
+ "bibtex".into()
+ } else {
+ "latex".into()
+ },
+ uri: Uri::from_file_path(&full_path).unwrap().into(),
+ version: 0,
+ text: fs::read_to_string(&full_path).await.unwrap(),
+ },
+ };
+ self.client.did_open(params).await;
+ }
+
+ pub async fn edit<S: Into<String>>(&self, relative_path: &str, text: S) {
+ let uri = self.uri(relative_path).into();
+ let params = DidChangeTextDocumentParams {
+ text_document: VersionedTextDocumentIdentifier::new(uri, 0),
+ content_changes: vec![TextDocumentContentChangeEvent {
+ range: None,
+ range_length: None,
+ text: text.into(),
+ }],
+ };
+ self.client.did_change(params).await;
+ }
+
+ pub async fn push_options(&self) {
+ let options = self.server.options.lock().await.clone();
+ let params = DidChangeConfigurationParams {
+ settings: serde_json::to_value::<Options>(options).unwrap(),
+ };
+ self.client.did_change_configuration(params).await
+ }
+
+ pub async fn definition_link(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Vec<LocationLink>> {
+ let params = TextDocumentPositionParams {
+ text_document: self.identifier(relative_path),
+ position: Position::new(line, character),
+ };
+ let response = self.client.definition(params).await.ok()?;
+ match response {
+ DefinitionResponse::LocationLinks(links) => Some(links),
+ DefinitionResponse::Locations(_) => unreachable!(),
+ }
+ }
+
+ pub async fn definition_location(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Vec<Location>> {
+ let params = TextDocumentPositionParams {
+ text_document: self.identifier(relative_path),
+ position: Position::new(line, character),
+ };
+ let response = self.client.definition(params).await.ok()?;
+ match response {
+ DefinitionResponse::LocationLinks(_) => unreachable!(),
+ DefinitionResponse::Locations(locations) => Some(locations),
+ }
+ }
+
+ pub async fn completion(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Vec<CompletionItem>> {
+ let params = CompletionParams {
+ text_document_position: TextDocumentPositionParams {
+ text_document: self.identifier(relative_path),
+ position: Position::new(line, character),
+ },
+ context: None,
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.client
+ .completion(params)
+ .await
+ .ok()
+ .map(|list| list.items)
+ }
+
+ pub async fn folding_range(&self, relative_path: &str) -> Option<Vec<FoldingRange>> {
+ let params = FoldingRangeParams {
+ text_document: self.identifier(relative_path),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.client.folding_range(params).await.ok()
+ }
+
+ pub async fn document_highlight(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Vec<DocumentHighlight>> {
+ let params = TextDocumentPositionParams {
+ text_document: self.identifier(relative_path),
+ position: Position::new(line, character),
+ };
+ self.client.document_highlight(params).await.ok()
+ }
+
+ pub async fn document_link(&self, relative_path: &str) -> Option<Vec<DocumentLink>> {
+ let params = DocumentLinkParams {
+ text_document: self.identifier(relative_path),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.client.document_link(params).await.ok()
+ }
+
+ pub async fn references(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ include_declaration: bool,
+ ) -> Option<Vec<Location>> {
+ let params = ReferenceParams {
+ text_document_position: TextDocumentPositionParams::new(
+ self.identifier(relative_path),
+ Position::new(line, character),
+ ),
+ context: ReferenceContext {
+ include_declaration,
+ },
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ self.client.references(params).await.ok()
+ }
+
+ pub async fn prepare_rename(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Option<Range>> {
+ let pos = Position::new(line, character);
+ let params = TextDocumentPositionParams::new(self.identifier(relative_path), pos);
+ self.client.prepare_rename(params).await.ok()
+ }
+
+ pub async fn rename<S: Into<String>>(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ new_name: S,
+ ) -> Option<Option<WorkspaceEdit>> {
+ let params = RenameParams {
+ text_document_position: TextDocumentPositionParams::new(
+ self.identifier(relative_path),
+ Position::new(line, character),
+ ),
+ new_name: new_name.into(),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ };
+ self.client.rename(params).await.ok()
+ }
+
+ pub async fn document_symbol_flat(
+ &self,
+ relative_path: &str,
+ ) -> Option<Vec<SymbolInformation>> {
+ let params = DocumentSymbolParams {
+ text_document: self.identifier(relative_path),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ match self.client.document_symbol(params).await.ok()? {
+ DocumentSymbolResponse::Flat(symbols) => Some(symbols),
+ DocumentSymbolResponse::Nested(_) => unreachable!(),
+ }
+ }
+
+ pub async fn document_symbol_nested(&self, relative_path: &str) -> Option<Vec<DocumentSymbol>> {
+ let params = DocumentSymbolParams {
+ text_document: self.identifier(relative_path),
+ work_done_progress_params: WorkDoneProgressParams::default(),
+ partial_result_params: PartialResultParams::default(),
+ };
+ match self.client.document_symbol(params).await.ok()? {
+ DocumentSymbolResponse::Flat(_) => unreachable!(),
+ DocumentSymbolResponse::Nested(symbols) => Some(symbols),
+ }
+ }
+
+ pub async fn hover(
+ &self,
+ relative_path: &str,
+ line: u64,
+ character: u64,
+ ) -> Option<Option<Hover>> {
+ let params = TextDocumentPositionParams {
+ text_document: self.identifier(relative_path),
+ position: Position::new(line, character),
+ };
+ self.client.hover(params).await.ok()
+ }
+
+ pub async fn detect_root(&self, relative_path: &str) {
+ self.client
+ .detect_root(self.identifier(relative_path))
+ .await
+ .unwrap();
+ }
+
+ pub async fn shutdown(&self) {
+ self.client.shutdown(()).await.unwrap();
+ self.client.exit(()).await;
+ }
+}
+
+impl Drop for TestBed {
+ fn drop(&mut self) {
+ if let Some(handle) = self.handle.as_ref() {
+ handle.abort();
+ }
+ }
+}
+
+pub static PULL_CAPABILITIES: ClientCapabilities = {
+ ClientCapabilities {
+ experimental: None,
+ text_document: None,
+ window: None,
+ workspace: Some(WorkspaceClientCapabilities {
+ apply_edit: None,
+ configuration: Some(true),
+ did_change_configuration: None,
+ did_change_watched_files: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ workspace_folders: None,
+ }),
+ }
+};
+
+pub static PUSH_CAPABILITIES: ClientCapabilities = {
+ ClientCapabilities {
+ experimental: None,
+ text_document: None,
+ window: None,
+ workspace: Some(WorkspaceClientCapabilities {
+ apply_edit: None,
+ configuration: None,
+ did_change_configuration: Some(GenericCapability {
+ dynamic_registration: Some(true),
+ }),
+ did_change_watched_files: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ workspace_folders: None,
+ }),
+ }
+};
+
+pub static NESTED_SYMBOL_CAPABILITIES: ClientCapabilities = {
+ ClientCapabilities {
+ experimental: None,
+ text_document: Some(TextDocumentClientCapabilities {
+ code_action: None,
+ code_lens: None,
+ color_provider: None,
+ completion: None,
+ declaration: None,
+ definition: None,
+ document_highlight: None,
+ document_link: None,
+ document_symbol: Some(DocumentSymbolCapability {
+ hierarchical_document_symbol_support: Some(true),
+ dynamic_registration: None,
+ symbol_kind: None,
+ }),
+ folding_range: None,
+ formatting: None,
+ hover: None,
+ implementation: None,
+ on_type_formatting: None,
+ publish_diagnostics: None,
+ range_formatting: None,
+ references: None,
+ rename: None,
+ signature_help: None,
+ synchronization: None,
+ type_definition: None,
+ }),
+ window: None,
+ workspace: Some(WorkspaceClientCapabilities {
+ apply_edit: None,
+ configuration: Some(true),
+ did_change_configuration: None,
+ did_change_watched_files: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ workspace_folders: None,
+ }),
+ }
+};
+
+pub static LOCATION_LINK_CAPABILITIES: ClientCapabilities = {
+ ClientCapabilities {
+ experimental: None,
+ text_document: Some(TextDocumentClientCapabilities {
+ code_action: None,
+ code_lens: None,
+ color_provider: None,
+ completion: None,
+ declaration: None,
+ definition: Some(GotoCapability {
+ dynamic_registration: None,
+ link_support: Some(true),
+ }),
+ document_highlight: None,
+ document_link: None,
+ document_symbol: None,
+ folding_range: None,
+ formatting: None,
+ hover: None,
+ implementation: None,
+ on_type_formatting: None,
+ publish_diagnostics: None,
+ range_formatting: None,
+ references: None,
+ rename: None,
+ signature_help: None,
+ synchronization: None,
+ type_definition: None,
+ }),
+ window: None,
+ workspace: Some(WorkspaceClientCapabilities {
+ apply_edit: None,
+ configuration: Some(true),
+ did_change_configuration: None,
+ did_change_watched_files: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ workspace_folders: None,
+ }),
+ }
+};