summaryrefslogtreecommitdiff
path: root/support/texlab/tests/support/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/tests/support/mod.rs')
-rw-r--r--support/texlab/tests/support/mod.rs635
1 files changed, 635 insertions, 0 deletions
diff --git a/support/texlab/tests/support/mod.rs b/support/texlab/tests/support/mod.rs
new file mode 100644
index 0000000000..abd1bfe313
--- /dev/null
+++ b/support/texlab/tests/support/mod.rs
@@ -0,0 +1,635 @@
+use copy_dir::copy_dir;
+use futures::lock::Mutex;
+use futures_boxed::boxed;
+use jsonrpc::client::Result;
+use lsp_types::*;
+use serde::Serialize;
+use std::collections::HashMap;
+use std::fs::remove_dir;
+use std::path::PathBuf;
+use std::sync::Arc;
+use tempfile::{tempdir, TempDir};
+use texlab::build::BuildOptions;
+use texlab::client::LspClient;
+use texlab::diagnostics::LatexLintOptions;
+use texlab::formatting::bibtex::BibtexFormattingOptions;
+use texlab::server::LatexLspServer;
+use texlab::workspace::Uri;
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct MockLspClientOptions {
+ pub bibtex_formatting: Option<BibtexFormattingOptions>,
+ pub latex_lint: Option<LatexLintOptions>,
+ pub latex_build: Option<BuildOptions>,
+}
+
+#[derive(Debug, Default)]
+pub struct MockLspClient {
+ pub messages: Mutex<Vec<ShowMessageParams>>,
+ pub options: Mutex<MockLspClientOptions>,
+ pub diagnostics_by_uri: Mutex<HashMap<Uri, Vec<Diagnostic>>>,
+ pub log_messages: Mutex<Vec<LogMessageParams>>,
+}
+
+impl MockLspClient {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl LspClient for MockLspClient {
+ #[boxed]
+ async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value> {
+ fn serialize<T>(options: &Option<T>) -> Result<serde_json::Value>
+ where
+ T: Serialize,
+ {
+ options
+ .as_ref()
+ .map(|options| serde_json::to_value(vec![options]).unwrap())
+ .ok_or_else(|| jsonrpc::Error::internal_error("Internal error".to_owned()))
+ }
+
+ let options = self.options.lock().await;
+ match params.items[0].section.as_ref().unwrap().as_ref() {
+ "bibtex.formatting" => serialize(&options.bibtex_formatting),
+ "latex.lint" => serialize(&options.latex_lint),
+ "latex.build" => serialize(&options.latex_build),
+ _ => panic!("Invalid language configuration!"),
+ }
+ }
+
+ #[boxed]
+ async fn show_message(&self, params: ShowMessageParams) {
+ let mut messages = self.messages.lock().await;
+ messages.push(params);
+ }
+
+ #[boxed]
+ async fn register_capability(&self, _params: RegistrationParams) -> Result<()> {
+ Ok(())
+ }
+
+ #[boxed]
+ async fn publish_diagnostics(&self, params: PublishDiagnosticsParams) {
+ let mut diagnostics_by_uri = self.diagnostics_by_uri.lock().await;
+ diagnostics_by_uri.insert(params.uri.into(), params.diagnostics);
+ }
+
+ #[boxed]
+ async fn work_done_progress_create(&self, _params: WorkDoneProgressCreateParams) -> Result<()> {
+ Ok(())
+ }
+
+ #[boxed]
+ async fn progress(&self, _params: ProgressParams) {}
+
+ #[boxed]
+ async fn log_message(&self, params: LogMessageParams) {
+ let mut messages = self.log_messages.lock().await;
+ messages.push(params);
+ }
+}
+
+pub struct Scenario {
+ pub directory: TempDir,
+ pub server: LatexLspServer<MockLspClient>,
+ pub client: Arc<MockLspClient>,
+}
+
+impl Scenario {
+ pub fn new(name: &str, distribution: Arc<Box<dyn tex::Distribution>>) -> Self {
+ let directory = tempdir().unwrap();
+ remove_dir(directory.path()).unwrap();
+ let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .join("tests")
+ .join("scenarios")
+ .join(name);
+ copy_dir(source, directory.path()).unwrap();
+
+ let client = Arc::new(MockLspClient::new());
+ let server = LatexLspServer::new(distribution, Arc::clone(&client));
+ Self {
+ directory,
+ server,
+ client,
+ }
+ }
+
+ pub async fn initialize(&self, capabilities: &ClientCapabilities) {
+ let root_uri = Uri::from_file_path(self.directory.path()).unwrap();
+ let params = InitializeParams {
+ process_id: None,
+ root_path: Some(self.directory.path().to_string_lossy().into_owned()),
+ root_uri: Some(root_uri.into()),
+ initialization_options: None,
+ capabilities: capabilities.clone(),
+ trace: None,
+ workspace_folders: None,
+ };
+
+ self.server
+ .execute_async(|svr| svr.initialize(params))
+ .await
+ .unwrap();
+
+ self.server
+ .execute(|svr| svr.initialized(InitializedParams {}))
+ .await;
+ }
+
+ pub fn uri(&self, name: &str) -> Uri {
+ let mut path = self.directory.path().to_owned();
+ path.push(name);
+ Uri::from_file_path(path).unwrap()
+ }
+
+ pub async fn read(&self, name: &'static str) -> String {
+ let mut path = self.directory.path().to_owned();
+ path.push(name);
+ let data = tokio::fs::read(path).await.unwrap();
+ let text = String::from_utf8_lossy(&data);
+ text.replace('\r', "")
+ }
+
+ pub async fn open(&self, name: &'static str) {
+ let text = self.read(name).await;
+ let language_id = if name.ends_with(".bib") {
+ "bibtex"
+ } else {
+ "latex"
+ };
+
+ let params = DidOpenTextDocumentParams {
+ text_document: TextDocumentItem {
+ uri: self.uri(name).into(),
+ version: 0,
+ language_id: language_id.to_owned(),
+ text,
+ },
+ };
+ self.server.execute(|svr| svr.did_open(params)).await;
+ }
+}
+
+pub mod capabilities {
+ use lsp_types::*;
+
+ pub static CLIENT_FULL_CAPABILITIES: ClientCapabilities = ClientCapabilities {
+ workspace: Some(WorkspaceClientCapabilities {
+ configuration: Some(true),
+ did_change_watched_files: None,
+ workspace_folders: None,
+ apply_edit: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ did_change_configuration: None,
+ }),
+ text_document: Some(TextDocumentClientCapabilities {
+ synchronization: None,
+ completion: None,
+ hover: None,
+ signature_help: None,
+ references: None,
+ document_highlight: None,
+ document_symbol: Some(DocumentSymbolCapability {
+ dynamic_registration: None,
+ hierarchical_document_symbol_support: Some(true),
+ symbol_kind: None,
+ }),
+ formatting: None,
+ range_formatting: None,
+ on_type_formatting: None,
+ declaration: None,
+ definition: Some(GotoCapability {
+ dynamic_registration: None,
+ link_support: Some(true),
+ }),
+ type_definition: None,
+ implementation: None,
+ code_action: None,
+ code_lens: None,
+ document_link: None,
+ color_provider: None,
+ rename: None,
+ publish_diagnostics: None,
+ folding_range: None,
+ }),
+ experimental: None,
+ window: Some(WindowClientCapabilities {
+ work_done_progress: Some(true),
+ }),
+ };
+
+ pub static CLIENT_NO_LINK_CAPABILITIES: ClientCapabilities = ClientCapabilities {
+ workspace: Some(WorkspaceClientCapabilities {
+ configuration: Some(true),
+ did_change_watched_files: None,
+ workspace_folders: None,
+ apply_edit: None,
+ execute_command: None,
+ symbol: None,
+ workspace_edit: None,
+ did_change_configuration: None,
+ }),
+ text_document: Some(TextDocumentClientCapabilities {
+ synchronization: None,
+ completion: None,
+ hover: None,
+ signature_help: None,
+ references: None,
+ document_highlight: None,
+ document_symbol: None,
+ formatting: None,
+ range_formatting: None,
+ on_type_formatting: None,
+ declaration: None,
+ definition: Some(GotoCapability {
+ dynamic_registration: None,
+ link_support: Some(false),
+ }),
+ type_definition: None,
+ implementation: None,
+ code_action: None,
+ code_lens: None,
+ document_link: None,
+ color_provider: None,
+ rename: None,
+ publish_diagnostics: None,
+ folding_range: None,
+ }),
+ experimental: None,
+ window: Some(WindowClientCapabilities {
+ work_done_progress: Some(true),
+ }),
+ };
+}
+
+pub mod completion {
+ use super::*;
+
+ pub async fn run_list(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ ) -> (Scenario, Vec<CompletionItem>) {
+ let scenario_name = format!("completion/{}", scenario_short_name);
+ let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown)));
+ scenario.open(file).await;
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+
+ let params = CompletionParams {
+ text_document_position: TextDocumentPositionParams {
+ text_document: TextDocumentIdentifier::new(scenario.uri(file).into()),
+ position: Position::new(line, character),
+ },
+ context: None,
+ };
+
+ let items = scenario
+ .server
+ .execute_async(|svr| svr.completion(params))
+ .await
+ .unwrap()
+ .items;
+
+ (scenario, items)
+ }
+
+ pub async fn run_empty(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ ) {
+ assert!(run_list(scenario_short_name, file, line, character)
+ .await
+ .1
+ .is_empty());
+ }
+
+ pub async fn run_item(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ item_name: &'static str,
+ ) -> CompletionItem {
+ let (scenario, items) = run_list(scenario_short_name, file, line, character).await;
+
+ let item = items
+ .into_iter()
+ .find(|item| item.label == item_name)
+ .unwrap();
+
+ scenario
+ .server
+ .execute_async(|svr| svr.completion_resolve(item))
+ .await
+ .unwrap()
+ }
+
+ pub mod verify {
+ use lsp_types::*;
+ use texlab::range::RangeExt;
+
+ pub fn text_edit(
+ item: &CompletionItem,
+ start_line: u64,
+ start_character: u64,
+ end_line: u64,
+ end_character: u64,
+ text: &str,
+ ) {
+ assert_eq!(
+ item.text_edit,
+ Some(TextEdit::new(
+ Range::new_simple(start_line, start_character, end_line, end_character),
+ text.into()
+ ))
+ );
+ }
+
+ pub fn detail(item: &CompletionItem, detail: &str) {
+ assert_eq!(item.detail.as_ref().unwrap(), detail);
+ }
+
+ pub fn labels(items: &[CompletionItem], expected_labels: Vec<&'static str>) {
+ let mut actual_labels: Vec<&str> =
+ items.iter().map(|item| item.label.as_ref()).collect();
+ actual_labels.sort();
+ assert_eq!(actual_labels, expected_labels);
+ }
+ }
+}
+
+pub mod definition {
+ use super::capabilities::*;
+ use super::*;
+ use texlab::definition::DefinitionResponse;
+
+ pub async fn run(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ capabilities: &ClientCapabilities,
+ ) -> (Scenario, DefinitionResponse) {
+ let scenario_name = format!("definition/{}", scenario_short_name);
+ let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown)));
+ scenario.initialize(capabilities).await;
+ scenario.open(file).await;
+
+ let params = TextDocumentPositionParams {
+ text_document: TextDocumentIdentifier::new(scenario.uri(file).into()),
+ position: Position::new(line, character),
+ };
+
+ let response = scenario
+ .server
+ .execute_async(|svr| svr.definition(params))
+ .await
+ .unwrap();
+
+ (scenario, response)
+ }
+
+ pub async fn run_link(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ ) -> (Scenario, Vec<LocationLink>) {
+ let (scenario, response) = run(
+ scenario_short_name,
+ file,
+ line,
+ character,
+ &CLIENT_FULL_CAPABILITIES,
+ )
+ .await;
+ match response {
+ DefinitionResponse::LocationLinks(links) => (scenario, links),
+ DefinitionResponse::Locations(_) => unreachable!(),
+ }
+ }
+
+ pub async fn run_location(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ ) -> (Scenario, Vec<Location>) {
+ let (scenario, response) = run(
+ scenario_short_name,
+ file,
+ line,
+ character,
+ &CLIENT_NO_LINK_CAPABILITIES,
+ )
+ .await;
+ match response {
+ DefinitionResponse::LocationLinks(_) => unreachable!(),
+ DefinitionResponse::Locations(locations) => (scenario, locations),
+ }
+ }
+
+ pub mod verify {
+ use super::*;
+ use texlab::range::RangeExt;
+
+ pub fn origin_selection_range(
+ link: &LocationLink,
+ start_line: u64,
+ start_character: u64,
+ end_line: u64,
+ end_character: u64,
+ ) {
+ assert_eq!(
+ link.origin_selection_range,
+ Some(Range::new_simple(
+ start_line,
+ start_character,
+ end_line,
+ end_character
+ ))
+ );
+ }
+ }
+}
+
+pub mod folding {
+ use super::*;
+ use std::cmp::Reverse;
+
+ pub async fn run(file: &'static str) -> Vec<FoldingRange> {
+ let scenario = Scenario::new("folding", Arc::new(Box::new(tex::Unknown)));
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+ scenario.open(file).await;
+ let params = FoldingRangeParams {
+ text_document: TextDocumentIdentifier::new(scenario.uri(file).into()),
+ };
+
+ let mut foldings = scenario
+ .server
+ .execute_async(|svr| svr.folding_range(params))
+ .await
+ .unwrap();
+
+ foldings.sort_by_key(|folding| {
+ let start = Position::new(folding.start_line, folding.start_character.unwrap());
+ let end = Position::new(folding.end_line, folding.end_character.unwrap());
+ (start, Reverse(end))
+ });
+ foldings
+ }
+}
+
+pub mod formatting {
+ use super::*;
+ use texlab::formatting::bibtex::BibtexFormattingOptions;
+
+ pub async fn run_bibtex(
+ file: &'static str,
+ options: Option<BibtexFormattingOptions>,
+ ) -> (Scenario, Vec<TextEdit>) {
+ let scenario = Scenario::new("formatting/bibtex", Arc::new(Box::new(tex::Unknown)));
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+ scenario.open(file).await;
+ {
+ scenario.client.options.lock().await.bibtex_formatting = options;
+ }
+
+ let params = DocumentFormattingParams {
+ text_document: TextDocumentIdentifier::new(scenario.uri(file).into()),
+ options: FormattingOptions {
+ tab_size: 4,
+ insert_spaces: true,
+ properties: HashMap::new(),
+ },
+ };
+
+ let edits = scenario
+ .server
+ .execute_async(|svr| svr.formatting(params))
+ .await
+ .unwrap();
+ (scenario, edits)
+ }
+}
+
+pub mod hover {
+ use super::*;
+
+ pub async fn run(
+ scenario_short_name: &'static str,
+ file: &'static str,
+ line: u64,
+ character: u64,
+ ) -> Option<HoverContents> {
+ let scenario_name = format!("hover/{}", scenario_short_name);
+ let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown)));
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+ scenario.open(file).await;
+ let identifier = TextDocumentIdentifier::new(scenario.uri(file).into());
+ let params = TextDocumentPositionParams::new(identifier, Position::new(line, character));
+ scenario
+ .server
+ .execute_async(|svr| svr.hover(params))
+ .await
+ .unwrap()
+ .map(|hover| hover.contents)
+ }
+}
+
+pub mod symbol {
+ use super::*;
+ use texlab::symbol::SymbolResponse;
+
+ pub async fn run_hierarchical(file: &'static str) -> Vec<DocumentSymbol> {
+ let scenario = Scenario::new("symbol/hierarchical", Arc::new(Box::new(tex::Unknown)));
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+ scenario.open(file).await;
+ let params = DocumentSymbolParams {
+ text_document: TextDocumentIdentifier::new(scenario.uri(file).into()),
+ };
+
+ let response = scenario
+ .server
+ .execute_async(|svr| svr.document_symbol(params))
+ .await
+ .unwrap();
+
+ match response {
+ SymbolResponse::Hierarchical(symbols) => symbols,
+ SymbolResponse::Flat(_) => unreachable!(),
+ }
+ }
+
+ pub async fn run_workspace(query: &'static str) -> (Scenario, Vec<SymbolInformation>) {
+ let scenario = Scenario::new("symbol/workspace", Arc::new(Box::new(tex::Unknown)));
+ scenario
+ .initialize(&capabilities::CLIENT_FULL_CAPABILITIES)
+ .await;
+ scenario.open("foo.tex").await;
+ scenario.open("bar.bib").await;
+ let params = WorkspaceSymbolParams {
+ query: query.into(),
+ };
+ let symbols = scenario
+ .server
+ .execute_async(|svr| svr.workspace_symbol(params))
+ .await
+ .unwrap();
+
+ (scenario, symbols)
+ }
+
+ pub mod verify {
+ use super::*;
+ use texlab::range::RangeExt;
+
+ pub fn symbol(
+ symbol: &DocumentSymbol,
+ name: &str,
+ detail: Option<&str>,
+ selection_range: Range,
+ range: Range,
+ ) {
+ assert_eq!(symbol.name, name);
+ assert_eq!(symbol.detail.as_ref().map(AsRef::as_ref), detail);
+ assert_eq!(symbol.selection_range, selection_range);
+ assert_eq!(symbol.range, range);
+ }
+
+ pub fn symbol_info(
+ symbol: &SymbolInformation,
+ scenario: &Scenario,
+ file: &str,
+ name: &str,
+ start_line: u64,
+ start_character: u64,
+ end_line: u64,
+ end_character: u64,
+ ) {
+ assert_eq!(symbol.name, name);
+ let range = Range::new_simple(start_line, start_character, end_line, end_character);
+ assert_eq!(
+ symbol.location,
+ Location::new(scenario.uri(file).into(), range)
+ );
+ }
+ }
+}