Add support for push-based configuration

This commit is contained in:
Patrick Förster 2020-01-04 21:09:31 +01:00
parent eb315e5330
commit b8b201deee
10 changed files with 312 additions and 84 deletions

View file

@ -8,6 +8,10 @@ pub trait ClientCapabilitiesExt {
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 {
@ -39,6 +43,18 @@ impl ClientCapabilitiesExt for ClientCapabilities {
.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)]

View file

@ -59,3 +59,24 @@ impl LatexBuildOptions {
self.on_save.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>,
}
#[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>,
}

View file

@ -11,12 +11,18 @@ async fn create_scenario(
let scenario = Scenario::new("build", true).await;
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
let options = LatexBuildOptions {
executable: Some(executable.into()),
args: None,
on_save: Some(build_on_save),
*scenario.client.options.lock().await = Options {
latex: Some(LatexOptions {
forward_search: None,
lint: None,
build: Some(LatexBuildOptions {
executable: Some(executable.into()),
args: None,
on_save: Some(build_on_save),
}),
}),
bibtex: None,
};
scenario.client.options.lock().await.latex_build = Some(options);
scenario.open(file).await;
scenario

View file

@ -5,17 +5,10 @@ use serde::Serialize;
use std::collections::HashMap;
use texlab_protocol::*;
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct MockLspClientOptions {
pub bibtex_formatting: Option<BibtexFormattingOptions>,
pub latex_lint: Option<LatexLintOptions>,
pub latex_build: Option<LatexBuildOptions>,
}
#[derive(Debug, Default)]
pub struct MockLspClient {
pub messages: Mutex<Vec<ShowMessageParams>>,
pub options: Mutex<MockLspClientOptions>,
pub options: Mutex<Options>,
pub diagnostics_by_uri: Mutex<HashMap<Uri, Vec<Diagnostic>>>,
pub log_messages: Mutex<Vec<LogMessageParams>>,
}
@ -52,10 +45,9 @@ impl LspClient for MockLspClient {
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!"),
"latex" => serialize(&options.latex),
"bibtex" => serialize(&options.bibtex),
_ => panic!("invalid language configuration"),
}
}

View file

@ -11,7 +11,12 @@ pub async fn run_bibtex(
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
scenario.open(file).await;
{
scenario.client.options.lock().await.bibtex_formatting = options;
*scenario.client.options.lock().await = Options {
bibtex: Some(BibtexOptions {
formatting: options,
}),
latex: None,
};
}
let params = DocumentFormattingParams {

View file

@ -8,9 +8,11 @@ pub enum LintReason {
Save,
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub enum Action {
RegisterCapabilities,
LoadDistribution,
UpdateConfiguration(serde_json::Value),
DetectRoot(Uri),
PublishDiagnostics,
RunLinter(Uri, LintReason),

129
src/config.rs Normal file
View file

@ -0,0 +1,129 @@
use futures::lock::Mutex;
use futures_boxed::boxed;
use log::*;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use texlab_protocol::*;
pub trait ConfigStrategy: Send + Sync {
#[boxed]
async fn get(&self) -> Options;
#[boxed()]
async fn set(&self, settings: serde_json::Value);
}
impl dyn ConfigStrategy {
pub fn select<C: LspClient + Send + Sync + 'static>(
capabilities: &ClientCapabilities,
client: Arc<C>,
) -> Box<Self> {
if capabilities.has_pull_configuration_support() {
Box::new(PullConfigStrategy::new(client))
} else if capabilities.has_push_configuration_support() {
Box::new(PushConfigStrategy::new())
} else {
Box::new(NoConfigStrategy::new())
}
}
}
#[derive(Debug)]
struct PullConfigStrategy<C> {
client: Arc<C>,
}
impl<C: LspClient> PullConfigStrategy<C> {
pub fn new(client: Arc<C>) -> Self {
Self { client }
}
async fn configuration<T>(&self, section: &'static str) -> T
where
T: DeserializeOwned + Default,
{
let params = ConfigurationParams {
items: vec![ConfigurationItem {
section: Some(section.into()),
scope_uri: None,
}],
};
match self.client.configuration(params).await {
Ok(json) => match serde_json::from_value::<Vec<T>>(json) {
Ok(config) => config.into_iter().next().unwrap(),
Err(_) => {
warn!("Invalid configuration: {}", section);
T::default()
}
},
Err(why) => {
error!(
"Retrieving configuration for {} failed: {}",
section, why.message
);
T::default()
}
}
}
}
impl<C: LspClient + Send + Sync> ConfigStrategy for PullConfigStrategy<C> {
#[boxed]
async fn get(&self) -> Options {
Options {
latex: Some(self.configuration("latex").await),
bibtex: Some(self.configuration("bibtex").await),
}
}
#[boxed]
async fn set(&self, _settings: serde_json::Value) {}
}
#[derive(Debug, Default)]
struct PushConfigStrategy {
options: Mutex<Options>,
}
impl PushConfigStrategy {
pub fn new() -> Self {
Self::default()
}
}
impl ConfigStrategy for PushConfigStrategy {
#[boxed]
async fn get(&self) -> Options {
let options = self.options.lock().await;
options.clone()
}
#[boxed()]
async fn set(&self, settings: serde_json::Value) {
let mut options = self.options.lock().await;
match serde_json::from_value(settings) {
Ok(settings) => *options = settings,
Err(why) => warn!("Invalid configuration: {}", why),
}
}
}
#[derive(Debug, Default)]
struct NoConfigStrategy;
impl NoConfigStrategy {
pub fn new() -> Self {
Self::default()
}
}
impl ConfigStrategy for NoConfigStrategy {
#[boxed]
async fn get(&self) -> Options {
Options::default()
}
#[boxed()]
async fn set(&self, _settings: serde_json::Value) {}
}

View file

@ -2,6 +2,7 @@
pub mod action;
pub mod build;
pub mod config;
pub mod definition;
pub mod diagnostics;
pub mod folding;

View file

@ -1,5 +1,6 @@
use crate::action::{Action, ActionManager, LintReason};
use crate::build::*;
use crate::config::ConfigStrategy;
use crate::definition::DefinitionProvider;
use crate::diagnostics::DiagnosticsManager;
use crate::folding::FoldingProvider;
@ -15,7 +16,6 @@ use jsonrpc::server::{Middleware, Result};
use jsonrpc_derive::{jsonrpc_method, jsonrpc_server};
use log::*;
use once_cell::sync::{Lazy, OnceCell};
use serde::de::DeserializeOwned;
use std::ffi::OsStr;
use std::fs;
use std::future::Future;
@ -34,6 +34,7 @@ pub struct LatexLspServer<C> {
client: Arc<C>,
client_capabilities: OnceCell<Arc<ClientCapabilities>>,
distribution: Arc<Box<dyn Distribution>>,
config_strategy: OnceCell<Box<dyn ConfigStrategy>>,
build_manager: BuildManager<C>,
workspace_manager: WorkspaceManager,
action_manager: ActionManager,
@ -57,6 +58,7 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
client: Arc::clone(&client),
client_capabilities: OnceCell::new(),
distribution: Arc::clone(&distribution),
config_strategy: OnceCell::new(),
build_manager: BuildManager::new(client),
workspace_manager: WorkspaceManager::new(distribution),
action_manager: ActionManager::default(),
@ -97,6 +99,10 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
#[jsonrpc_method("initialize", kind = "request")]
pub async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
let client = Arc::clone(&self.client);
let config_strategy = ConfigStrategy::select(&params.capabilities, client);
let _ = self.config_strategy.set(config_strategy);
self.client_capabilities
.set(Arc::new(params.capabilities))
.unwrap();
@ -156,6 +162,7 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
#[jsonrpc_method("initialized", kind = "notification")]
pub fn initialized(&self, _params: InitializedParams) {
self.action_manager.push(Action::RegisterCapabilities);
self.action_manager.push(Action::PublishDiagnostics);
self.action_manager.push(Action::LoadDistribution);
}
@ -209,6 +216,12 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
#[jsonrpc_method("textDocument/didClose", kind = "notification")]
pub fn did_close(&self, _params: DidCloseTextDocumentParams) {}
#[jsonrpc_method("workspace/didChangeConfiguration", kind = "notification")]
pub fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
self.action_manager
.push(Action::UpdateConfiguration(params.settings));
}
#[jsonrpc_method("window/workDoneProgress/cancel", kind = "notification")]
pub fn work_done_progress_cancel(&self, params: WorkDoneProgressCancelParams) {
self.action_manager.push(Action::CancelBuild(params.token));
@ -334,12 +347,17 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
let request = self.make_feature_request(params.text_document.as_uri(), params)?;
let mut edits = Vec::new();
if let SyntaxTree::Bibtex(tree) = &request.document().tree {
let options = self
.configuration()
.await
.bibtex
.and_then(|opts| opts.formatting)
.unwrap_or_default();
let params = BibtexFormattingParams {
tab_size: request.params.options.tab_size as usize,
insert_spaces: request.params.options.insert_spaces,
options: self
.configuration::<BibtexFormattingOptions>("bibtex.formatting")
.await,
options,
};
for declaration in &tree.root.children {
@ -384,7 +402,12 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
#[jsonrpc_method("textDocument/build", kind = "request")]
pub async fn build(&self, params: BuildParams) -> Result<BuildResult> {
let request = self.make_feature_request(params.text_document.as_uri(), params)?;
let options = self.configuration::<LatexBuildOptions>("latex.build").await;
let options = self
.configuration()
.await
.latex
.and_then(|opts| opts.build)
.unwrap_or_default();
let result = self.build_manager.build(request, options).await;
Ok(result)
}
@ -396,8 +419,11 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
) -> Result<ForwardSearchResult> {
let request = self.make_feature_request(params.text_document.as_uri(), params)?;
let options = self
.configuration::<LatexForwardSearchOptions>("latex.forwardSearch")
.await;
.configuration()
.await
.latex
.and_then(|opts| opts.forward_search)
.unwrap_or_default();
match request.document().uri.to_file_path() {
Ok(tex_file) => {
@ -416,43 +442,12 @@ impl<C: LspClient + Send + Sync + 'static> LatexLspServer<C> {
}
}
async fn configuration<T>(&self, section: &'static str) -> T
where
T: DeserializeOwned + Default,
{
if !self
.client_capabilities
async fn configuration(&self) -> Options {
self.config_strategy
.get()
.and_then(|cap| cap.workspace.as_ref())
.and_then(|cap| cap.configuration)
.unwrap_or(false)
{
return T::default();
}
let params = ConfigurationParams {
items: vec![ConfigurationItem {
section: Some(section.into()),
scope_uri: None,
}],
};
match self.client.configuration(params).await {
Ok(json) => match serde_json::from_value::<Vec<T>>(json) {
Ok(config) => config.into_iter().next().unwrap(),
Err(_) => {
warn!("Invalid configuration: {}", section);
T::default()
}
},
Err(why) => {
error!(
"Retrieving configuration for {} failed: {}",
section, why.message
);
T::default()
}
}
.expect("initialize needs to be called before using the server")
.get()
.await
}
fn make_feature_request<P>(&self, uri: Uri, params: P) -> Result<FeatureRequest<P>> {
@ -581,6 +576,25 @@ impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> {
self.update_build_diagnostics().await;
for action in self.action_manager.take() {
match action {
Action::RegisterCapabilities => {
let capabilities = self.client_capabilities.get().unwrap();
if !capabilities.has_pull_configuration_support()
&& capabilities.has_push_configuration_support()
{
let registration = Registration {
id: "pull-config".into(),
method: "workspace/didChangeConfiguration".into(),
register_options: None,
};
let params = RegistrationParams {
registrations: vec![registration],
};
self.client
.register_capability(params)
.await
.expect("failed to register \"workspace/didChangeConfiguration\"");
}
}
Action::LoadDistribution => {
info!("Detected TeX distribution: {:?}", self.distribution.kind());
if self.distribution.kind() == DistributionKind::Unknown {
@ -612,6 +626,9 @@ impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> {
self.client.show_message(params).await;
};
}
Action::UpdateConfiguration(settings) => {
self.config_strategy.get().unwrap().set(settings).await;
}
Action::DetectRoot(uri) => {
self.detect_root(uri).await;
}
@ -631,10 +648,16 @@ impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> {
}
}
Action::RunLinter(uri, reason) => {
let config: LatexLintOptions = self.configuration("latex.lint").await;
let options = self
.configuration()
.await
.latex
.and_then(|opts| opts.lint)
.unwrap_or_default();
let should_lint = match reason {
LintReason::Change => config.on_change(),
LintReason::Save => config.on_save(),
LintReason::Change => options.on_change(),
LintReason::Save => options.on_save(),
};
if should_lint {
let workspace = self.workspace_manager.get();
@ -647,8 +670,14 @@ impl<C: LspClient + Send + Sync + 'static> Middleware for LatexLspServer<C> {
}
}
Action::Build(uri) => {
let config: LatexBuildOptions = self.configuration("latex.build").await;
if config.on_save() {
let options = self
.configuration()
.await
.latex
.and_then(|opts| opts.build)
.unwrap_or_default();
if options.on_save() {
let text_document = TextDocumentIdentifier::new(uri.into());
self.build(BuildParams { text_document }).await.unwrap();
}

View file

@ -7,10 +7,17 @@ async fn disabled() {
let scenario = Scenario::new("diagnostics/latex", true).await;
match scenario.distribution.kind() {
Texlive | Miktex => {
scenario.client.options.lock().await.latex_lint = Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(false),
});
*scenario.client.options.lock().await = Options {
latex: Some(LatexOptions {
forward_search: None,
lint: Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(false),
}),
build: None,
}),
bibtex: None,
};
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
scenario.open("disabled.tex").await;
@ -28,10 +35,17 @@ async fn on_open() {
let scenario = Scenario::new("diagnostics/latex", true).await;
match scenario.distribution.kind() {
Texlive | Miktex => {
scenario.client.options.lock().await.latex_lint = Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(true),
});
*scenario.client.options.lock().await = Options {
latex: Some(LatexOptions {
forward_search: None,
lint: Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(true),
}),
build: None,
}),
bibtex: None,
};
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
scenario.open("on_open.tex").await;
@ -51,10 +65,17 @@ async fn on_save() {
let scenario = Scenario::new("diagnostics/latex", true).await;
match scenario.distribution.kind() {
Texlive | Miktex => {
scenario.client.options.lock().await.latex_lint = Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(true),
});
*scenario.client.options.lock().await = Options {
latex: Some(LatexOptions {
forward_search: None,
lint: Some(LatexLintOptions {
on_change: Some(false),
on_save: Some(true),
}),
build: None,
}),
bibtex: None,
};
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
scenario.open("on_save.tex").await;
@ -99,11 +120,17 @@ async fn on_change() {
let scenario = Scenario::new("diagnostics/latex", true).await;
match scenario.distribution.kind() {
Texlive | Miktex => {
scenario.client.options.lock().await.latex_lint = Some(LatexLintOptions {
on_change: Some(true),
on_save: Some(true),
});
*scenario.client.options.lock().await = Options {
latex: Some(LatexOptions {
forward_search: None,
lint: Some(LatexLintOptions {
on_change: Some(true),
on_save: Some(true),
}),
build: None,
}),
bibtex: None,
};
scenario.initialize(&CLIENT_FULL_CAPABILITIES).await;
scenario.open("on_change.tex").await;
let uri = scenario.uri("on_change.tex");