diff options
Diffstat (limited to 'support/texlab/src/protocol')
-rw-r--r-- | support/texlab/src/protocol/capabilities.rs | 145 | ||||
-rw-r--r-- | support/texlab/src/protocol/client.rs | 27 | ||||
-rw-r--r-- | support/texlab/src/protocol/codec.rs | 144 | ||||
-rw-r--r-- | support/texlab/src/protocol/edit.rs | 12 | ||||
-rw-r--r-- | support/texlab/src/protocol/mod.rs | 71 | ||||
-rw-r--r-- | support/texlab/src/protocol/options.rs | 104 | ||||
-rw-r--r-- | support/texlab/src/protocol/range.rs | 97 | ||||
-rw-r--r-- | support/texlab/src/protocol/uri.rs | 97 |
8 files changed, 697 insertions, 0 deletions
diff --git a/support/texlab/src/protocol/capabilities.rs b/support/texlab/src/protocol/capabilities.rs new file mode 100644 index 0000000000..c83b205aa8 --- /dev/null +++ b/support/texlab/src/protocol/capabilities.rs @@ -0,0 +1,145 @@ +use lsp_types::{ClientCapabilities, MarkupKind}; + +pub trait ClientCapabilitiesExt { + fn has_definition_link_support(&self) -> bool; + + fn has_hierarchical_document_symbol_support(&self) -> bool; + + fn has_work_done_progress_support(&self) -> bool; + + fn has_hover_markdown_support(&self) -> bool; + + fn has_pull_configuration_support(&self) -> bool; + + fn has_push_configuration_support(&self) -> bool; +} + +impl ClientCapabilitiesExt for ClientCapabilities { + fn has_definition_link_support(&self) -> bool { + self.text_document + .as_ref() + .and_then(|cap| cap.definition.as_ref()) + .and_then(|cap| cap.link_support) + == Some(true) + } + + fn has_hierarchical_document_symbol_support(&self) -> bool { + self.text_document + .as_ref() + .and_then(|cap| cap.document_symbol.as_ref()) + .and_then(|cap| cap.hierarchical_document_symbol_support) + == Some(true) + } + + fn has_work_done_progress_support(&self) -> bool { + self.window.as_ref().and_then(|cap| cap.work_done_progress) == Some(true) + } + + fn has_hover_markdown_support(&self) -> bool { + self.text_document + .as_ref() + .and_then(|cap| cap.hover.as_ref()) + .and_then(|cap| cap.content_format.as_ref()) + .filter(|formats| formats.contains(&MarkupKind::Markdown)) + .is_some() + } + + fn has_pull_configuration_support(&self) -> bool { + self.workspace.as_ref().and_then(|cap| cap.configuration) == Some(true) + } + + fn has_push_configuration_support(&self) -> bool { + self.workspace + .as_ref() + .and_then(|cap| cap.did_change_configuration) + .and_then(|cap| cap.dynamic_registration) + == Some(true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp_types::*; + + #[test] + fn has_definition_link_support_true() { + let capabilities = ClientCapabilities { + text_document: Some(TextDocumentClientCapabilities { + definition: Some(GotoCapability { + link_support: Some(true), + ..GotoCapability::default() + }), + ..TextDocumentClientCapabilities::default() + }), + ..ClientCapabilities::default() + }; + assert!(capabilities.has_definition_link_support()); + } + + #[test] + fn has_definition_link_support_false() { + let capabilities = ClientCapabilities::default(); + assert!(!capabilities.has_definition_link_support()); + } + + #[test] + fn has_hierarchical_document_symbol_support_true() { + let capabilities = ClientCapabilities { + text_document: Some(TextDocumentClientCapabilities { + document_symbol: Some(DocumentSymbolCapability { + hierarchical_document_symbol_support: Some(true), + ..DocumentSymbolCapability::default() + }), + ..TextDocumentClientCapabilities::default() + }), + ..ClientCapabilities::default() + }; + assert!(capabilities.has_hierarchical_document_symbol_support()); + } + + #[test] + fn has_hierarchical_document_symbol_support_false() { + let capabilities = ClientCapabilities::default(); + assert!(!capabilities.has_hierarchical_document_symbol_support()); + } + + #[test] + fn has_work_done_progress_support_true() { + let capabilities = ClientCapabilities { + window: Some(WindowClientCapabilities { + work_done_progress: Some(true), + ..WindowClientCapabilities::default() + }), + ..ClientCapabilities::default() + }; + assert!(capabilities.has_work_done_progress_support()); + } + + #[test] + fn has_work_done_progress_support_false() { + let capabilities = ClientCapabilities::default(); + assert!(!capabilities.has_work_done_progress_support()); + } + + #[test] + fn has_hover_markdown_support_true() { + let capabilities = ClientCapabilities { + text_document: Some(TextDocumentClientCapabilities { + hover: Some(HoverCapability { + content_format: Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]), + ..HoverCapability::default() + }), + ..TextDocumentClientCapabilities::default() + }), + ..ClientCapabilities::default() + }; + assert!(capabilities.has_hover_markdown_support()); + } + + #[test] + fn has_hover_markdown_support_false() { + let capabilities = ClientCapabilities::default(); + assert!(!capabilities.has_hover_markdown_support()); + } +} diff --git a/support/texlab/src/protocol/client.rs b/support/texlab/src/protocol/client.rs new file mode 100644 index 0000000000..471f1fa41d --- /dev/null +++ b/support/texlab/src/protocol/client.rs @@ -0,0 +1,27 @@ +use jsonrpc::client::Result; +use jsonrpc_derive::{jsonrpc_client, jsonrpc_method}; +use lsp_types::*; + +#[jsonrpc_client(LatexLspClient)] +pub trait LspClient { + #[jsonrpc_method("workspace/configuration", kind = "request")] + async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value>; + + #[jsonrpc_method("window/showMessage", kind = "notification")] + async fn show_message(&self, params: ShowMessageParams); + + #[jsonrpc_method("client/registerCapability", kind = "request")] + async fn register_capability(&self, params: RegistrationParams) -> Result<()>; + + #[jsonrpc_method("textDocument/publishDiagnostics", kind = "notification")] + async fn publish_diagnostics(&self, params: PublishDiagnosticsParams); + + #[jsonrpc_method("$/progress", kind = "notification")] + async fn progress(&self, params: ProgressParams); + + #[jsonrpc_method("window/workDoneProgress/create", kind = "request")] + async fn work_done_progress_create(&self, params: WorkDoneProgressCreateParams) -> Result<()>; + + #[jsonrpc_method("window/logMessage", kind = "notification")] + async fn log_message(&self, params: LogMessageParams); +} diff --git a/support/texlab/src/protocol/codec.rs b/support/texlab/src/protocol/codec.rs new file mode 100644 index 0000000000..980ae1307e --- /dev/null +++ b/support/texlab/src/protocol/codec.rs @@ -0,0 +1,144 @@ +use bytes::{BufMut, BytesMut}; +use log::trace; +use std::io::{Error, ErrorKind}; +use tokio_util::codec::{Decoder, Encoder}; + +pub struct LspCodec; + +impl Decoder for LspCodec { + type Item = String; + type Error = Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { + match parser::parse(src) { + Ok((remaining, content)) => { + trace!("Received message:\n{}\n", content); + + let offset = src.len() - remaining.len(); + let _ = src.split_to(offset); + Ok(Some(content)) + } + Err(error) => { + if error.is_incomplete() { + Ok(None) + } else { + Err(ErrorKind::InvalidData.into()) + } + } + } + } +} + +impl Encoder<String> for LspCodec { + type Error = Error; + + fn encode(&mut self, item: String, dst: &mut BytesMut) -> Result<(), Self::Error> { + let message = format!("Content-Length: {}\r\n\r\n{}", item.len(), item); + trace!("Sent message:\n{}\n", message); + + dst.reserve(message.len()); + dst.put(message.as_bytes()); + Ok(()) + } +} + +mod parser { + use nom::{ + bytes::streaming::{tag, take, take_while}, + character::{is_digit, streaming::line_ending}, + combinator::{map_res, opt}, + IResult, + }; + use std::str; + + pub fn parse(input: &[u8]) -> IResult<&[u8], String> { + let (input, _) = opt(content_type)(input)?; + let (input, length) = content_length(input)?; + let (input, _) = opt(content_type)(input)?; + let (input, _) = line_ending(input)?; + let (input, content) = map_res(take(length), str::from_utf8)(input)?; + Ok((input, content.to_owned())) + } + + fn content_type(input: &[u8]) -> IResult<&[u8], &[u8]> { + let (input, _) = tag("Content-Type: application/vscode-jsonrpc;charset=utf")(input)?; + let (input, _) = opt(tag("-"))(input)?; + let (input, _) = tag("8")(input)?; + line_ending(input) + } + + fn content_length(input: &[u8]) -> IResult<&[u8], usize> { + let (input, _) = tag("Content-Length: ")(input)?; + let (input, length) = map_res(take_while(is_digit), from_bytes)(input)?; + let (input, _) = line_ending(input)?; + Ok((input, length)) + } + + fn from_bytes(input: &[u8]) -> Result<usize, std::num::ParseIntError> { + usize::from_str_radix(str::from_utf8(input).unwrap(), 10) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn parse_content_type() { + let result = + content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf-8\r\n"); + assert!(result.is_ok()); + } + + #[test] + fn parse_content_type_utf8() { + let result = content_type(b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\n"); + assert!(result.is_ok()); + } + + #[test] + fn parse_content_length() { + let result = content_length(b"Content-Length: 42\r\n"); + assert_eq!(result.unwrap().1, 42usize); + } + + #[test] + fn parse_message_full() { + let result = parse( + b"Content-Length: 2\r\nContent-Type: application/vscode-jsonrpc;charset=utf8\r\n\r\n{}", + ); + assert_eq!(result.unwrap().1, "{}"); + } + + #[test] + fn parse_message_type_first() { + let result = parse( + b"Content-Type: application/vscode-jsonrpc;charset=utf8\r\nContent-Length: 2\r\n\r\n{}", + ); + assert_eq!(result.unwrap().1, "{}"); + } + + #[test] + fn parse_message_without_type() { + let result = parse(b"Content-Length: 2\r\n\r\n{}"); + assert_eq!(result.unwrap().1, "{}"); + } + + #[test] + fn parse_message_incomplete() { + let result = parse(b"Content-Length:"); + assert!(result.unwrap_err().is_incomplete()); + } + + #[test] + fn parse_message_invalid() { + let error = parse(b"foo").unwrap_err(); + assert!(!error.is_incomplete()); + } + + #[test] + fn parse_message_overflow() { + let result = parse(b"Content-Length: 4\r\n\r\n{}"); + assert!(result.unwrap_err().is_incomplete()); + } + } +} diff --git a/support/texlab/src/protocol/edit.rs b/support/texlab/src/protocol/edit.rs new file mode 100644 index 0000000000..9091f2d393 --- /dev/null +++ b/support/texlab/src/protocol/edit.rs @@ -0,0 +1,12 @@ +use lsp_types::{CompletionTextEdit, TextEdit}; + +pub trait CompletionTextEditExt { + fn text_edit(&self) -> Option<&TextEdit>; +} + +impl CompletionTextEditExt for CompletionTextEdit { + fn text_edit(&self) -> Option<&TextEdit> { + let CompletionTextEdit::Edit(edit) = self; + Some(edit) + } +} diff --git a/support/texlab/src/protocol/mod.rs b/support/texlab/src/protocol/mod.rs new file mode 100644 index 0000000000..c08a7d82f5 --- /dev/null +++ b/support/texlab/src/protocol/mod.rs @@ -0,0 +1,71 @@ +cfg_if::cfg_if! { + if #[cfg(feature = "server")] { + mod client; + mod codec; + + pub use self::{ + client::{LatexLspClient, LspClient}, + codec::LspCodec, + }; + } +} + +mod capabilities; +mod edit; +mod options; +mod range; +mod uri; + +pub use self::{ + capabilities::ClientCapabilitiesExt, + edit::*, + options::*, + range::RangeExt, + uri::{AsUri, Uri}, +}; +pub use lsp_types::*; + +use serde::{Deserialize, Serialize}; +use serde_repr::*; + +#[serde(untagged)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub enum DefinitionResponse { + Locations(Vec<Location>), + LocationLinks(Vec<LocationLink>), +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[repr(i32)] +pub enum ForwardSearchStatus { + Success = 0, + Error = 1, + Failure = 2, + Unconfigured = 3, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct ForwardSearchResult { + pub status: ForwardSearchStatus, +} + +#[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, +} diff --git a/support/texlab/src/protocol/options.rs b/support/texlab/src/protocol/options.rs new file mode 100644 index 0000000000..86718d1e72 --- /dev/null +++ b/support/texlab/src/protocol/options.rs @@ -0,0 +1,104 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum BibtexFormatter { + Texlab, + Latexindent, +} + +impl Default for BibtexFormatter { + fn default() -> Self { + Self::Texlab + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexFormattingOptions { + pub line_length: Option<i32>, + pub formatter: Option<BibtexFormatter>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +pub struct LatexForwardSearchOptions { + pub executable: Option<String>, + pub args: Option<Vec<String>>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct LatexLintOptions { + pub on_change: Option<bool>, + pub on_save: Option<bool>, +} + +impl LatexLintOptions { + pub fn on_change(&self) -> bool { + self.on_change.unwrap_or(false) + } + + pub fn on_save(&self) -> bool { + self.on_save.unwrap_or(false) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LatexBuildOptions { + pub executable: Option<String>, + pub args: Option<Vec<String>>, + pub on_save: Option<bool>, + pub output_directory: Option<PathBuf>, + pub forward_search_after: Option<bool>, +} + +impl LatexBuildOptions { + pub fn executable(&self) -> String { + self.executable + .as_ref() + .map(Clone::clone) + .unwrap_or_else(|| "latexmk".to_owned()) + } + + pub fn args(&self) -> Vec<String> { + self.args.as_ref().map(Clone::clone).unwrap_or_else(|| { + vec![ + "-pdf".to_owned(), + "-interaction=nonstopmode".to_owned(), + "-synctex=1".to_owned(), + ] + }) + } + + pub fn on_save(&self) -> bool { + self.on_save.unwrap_or(false) + } + + pub fn forward_search_after(&self) -> bool { + self.forward_search_after.unwrap_or(false) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LatexOptions { + pub forward_search: Option<LatexForwardSearchOptions>, + pub lint: Option<LatexLintOptions>, + pub build: Option<LatexBuildOptions>, + pub root_directory: Option<PathBuf>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexOptions { + pub formatting: Option<BibtexFormattingOptions>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Options { + pub latex: Option<LatexOptions>, + pub bibtex: Option<BibtexOptions>, +} diff --git a/support/texlab/src/protocol/range.rs b/support/texlab/src/protocol/range.rs new file mode 100644 index 0000000000..774df1e699 --- /dev/null +++ b/support/texlab/src/protocol/range.rs @@ -0,0 +1,97 @@ +use lsp_types::{Position, Range}; + +pub trait RangeExt { + fn new_simple(start_line: u64, start_character: u64, end_line: u64, end_character: u64) + -> Self; + + fn contains(&self, pos: Position) -> bool; + + fn contains_exclusive(&self, pos: Position) -> bool; +} + +impl RangeExt for Range { + fn new_simple( + start_line: u64, + start_character: u64, + end_line: u64, + end_character: u64, + ) -> Self { + Self { + start: Position::new(start_line, start_character), + end: Position::new(end_line, end_character), + } + } + + fn contains(&self, pos: Position) -> bool { + pos >= self.start && pos <= self.end + } + + fn contains_exclusive(&self, pos: Position) -> bool { + pos > self.start && pos < self.end + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contains_inside() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(range.contains(Position::new(2, 5))); + } + + #[test] + fn contains_begin() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(range.contains(range.start)); + } + + #[test] + fn contains_end() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(range.contains(range.end)); + } + + #[test] + fn contains_outside_left() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains(Position::new(0, 5))); + } + + #[test] + fn contains_outside_right() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains(Position::new(5, 1))); + } + + #[test] + fn contains_exclusive_inside() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(range.contains_exclusive(Position::new(2, 5))); + } + + #[test] + fn contains_exclusive_begin() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains_exclusive(range.start)); + } + + #[test] + fn contains_exclusive_end() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains_exclusive(range.end)); + } + + #[test] + fn contains_exclusive_outside_left() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains_exclusive(Position::new(0, 5))); + } + + #[test] + fn contains_exclusive_outside_right() { + let range = Range::new_simple(1, 2, 3, 4); + assert!(!range.contains_exclusive(Position::new(5, 1))); + } +} diff --git a/support/texlab/src/protocol/uri.rs b/support/texlab/src/protocol/uri.rs new file mode 100644 index 0000000000..e8dd1ba160 --- /dev/null +++ b/support/texlab/src/protocol/uri.rs @@ -0,0 +1,97 @@ +use lsp_types::{TextDocumentIdentifier, TextDocumentPositionParams}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt, + hash::{Hash, Hasher}, + ops::Deref, + path::Path, +}; +use url::{ParseError, Url}; + +#[derive(Eq, Clone, Serialize, Deserialize)] +pub struct Uri(Url); + +impl Uri { + pub fn with_extension(&self, extension: &str) -> Option<Self> { + let file_name = self.path_segments()?.last()?; + let file_stem = match file_name.rfind('.') { + Some(index) => &file_name[..index], + None => file_name, + }; + self.join(&format!("{}.{}", file_stem, extension)) + .ok() + .map(Into::into) + } + + pub fn parse(input: &str) -> Result<Self, ParseError> { + Url::parse(input).map(|url| url.into()) + } + + pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + Url::from_file_path(path).map(|url| url.into()) + } +} + +impl PartialEq for Uri { + fn eq(&self, other: &Self) -> bool { + if cfg!(windows) { + self.as_str().to_lowercase() == other.as_str().to_lowercase() + } else { + self.as_str() == other.as_str() + } + } +} + +impl Hash for Uri { + fn hash<H: Hasher>(&self, state: &mut H) { + self.as_str().to_lowercase().hash(state); + } +} + +impl Deref for Uri { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<Url> for Uri { + fn from(url: Url) -> Self { + Uri(url) + } +} + +impl Into<Url> for Uri { + fn into(self) -> Url { + self.0 + } +} + +impl fmt::Debug for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub trait AsUri { + fn as_uri(&self) -> Uri; +} + +impl AsUri for TextDocumentIdentifier { + fn as_uri(&self) -> Uri { + self.uri.clone().into() + } +} + +impl AsUri for TextDocumentPositionParams { + fn as_uri(&self) -> Uri { + self.text_document.as_uri() + } +} |