diff --git a/README.md b/README.md index 5a3f151..db1aac3 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ Like most language servers, mdpls operates over stdin and stdout. | `markdown.preview.codeTheme` | string | [highlight.js style] to use for syntax highlighting in code blocks. | `github` | `markdown.preview.serveStatic` | boolean | Serve static files like images (this should only be use with trusted documents) | `false` | `markdown.preview.renderer` | array or string | The program to use to render the markdown to html. If not specified, the builtin markdown renderer will be used. | None +| `markdown.preview.deferUpdates.ms_before` | int | After the document changes, how long to wait before updating the preview | 0 +| `markdown.preview.deferUpdates.ms_between` | int | Between two document changes, how long to wait before updating the preview (200ms -> up to 5 updates per second) | 0 + +Setting either `deferUpdates.ms_before` or `deferUpdates.ms_between` to a nonzero value enables enables the deferUpdates mode. Here, the preview is updated slower and less frequently (instead of updating every time any change to the document is made) to preserve battery and improve usability in large documents. This mode requires spawning an additional thread. ### Commands diff --git a/src/lib.rs b/src/lib.rs index fbfe0d5..cd146c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,16 @@ use std::default::Default; -use std::fmt; use std::io::{self, prelude::*}; use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{fmt, thread}; use log::*; use lsp_types::notification::Notification as LspNotification; use lsp_types::request::Request as LspRequest; use lsp_types::{ lsp_notification, lsp_request, ExecuteCommandOptions, InitializeResult, ServerCapabilities, - ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Url, WorkDoneProgressOptions, }; use serde::de::{self, Visitor}; @@ -39,6 +41,12 @@ struct Settings { /// Program and arguments to use to render the markdown. If `None`, use the default renderer. renderer: Option<(String, Vec)>, + + /// If `Some`, don't update every time the document is changed. + /// `Some((ms_before, ms_between))`: + /// - After the first change, wait for `ms_before` milliseconds. + /// - Between two updates, wait at least `ms_between` milliseconds. + defer_updates: Option<(u64, u64)>, } impl Default for Settings { @@ -49,6 +57,7 @@ impl Default for Settings { theme: String::from("github"), serve_static: false, renderer: None, + defer_updates: None, } } } @@ -68,6 +77,12 @@ impl<'de> Deserialize<'de> for Settings { preview: Option, } + #[derive(Deserialize, Default)] + struct DeferUpdates { + ms_before: u64, + ms_between: u64, + } + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Preview { @@ -80,6 +95,8 @@ impl<'de> Deserialize<'de> for Settings { #[serde(deserialize_with = "deserialize_opt_command")] #[serde(default)] renderer: Option<(String, Vec)>, + #[serde(default)] + defer_updates: DeferUpdates, } Settings::deserialize(deserializer).map(|settings| { @@ -103,6 +120,18 @@ impl<'de> Deserialize<'de> for Settings { } settings.renderer = preview_settings.renderer; + + settings.defer_updates = if preview_settings.defer_updates.ms_before > 0 + || preview_settings.defer_updates.ms_between > 0 + { + Some(( + preview_settings.defer_updates.ms_before, + preview_settings.defer_updates.ms_between, + )) + } else { + // when `(0, 0)` (Default), don't spawn a thread. + None + }; } settings @@ -114,7 +143,13 @@ pub struct Server { transport: LspTransport, settings: Settings, shutdown: bool, - markdown_server: aurelius::Server, + markdown_server: Arc>, + defer_control: Option<( + Arc>>, + std::sync::mpsc::Sender, + thread::JoinHandle<()>, + )>, + last_uri: Option, /// True if the server is being run as part of a test. The preview will not be spawned. #[doc(hidden)] pub test: bool, @@ -138,12 +173,24 @@ where transport: LspTransport::new(reader, writer), settings, shutdown: false, - markdown_server: server, + markdown_server: Arc::new(Mutex::new(server)), + last_uri: None, test: false, + defer_control: None, + } + } + + fn is_new_uri(&mut self, uri: &Url) -> bool { + if self.last_uri.as_ref() == Some(uri) { + false + } else { + self.last_uri = Some(uri.clone()); + true } } pub fn serve(mut self) -> io::Result<()> { + self.spawn_or_stop_deferred_update_thread(); loop { let message = match self.transport.decode() { Ok(Some(message)) => message, @@ -179,7 +226,17 @@ where { return Ok(()) } - Message::Notification(not) => self.handle_notification(not), + Message::Notification(not) => { + if let Some(new_doc) = self.handle_notification(not) { + if let Some((current_document, wake_thread, _)) = &self.defer_control { + *current_document.lock().unwrap() = Some(new_doc); + wake_thread.send(DeferEvent::UpdatePreview).unwrap(); + } else { + // update the server directly + self.markdown_server.lock().unwrap().send(new_doc).unwrap(); + } + } + } Message::Response(res) => unimplemented!("unhandled response: {:?}", res), } } @@ -244,7 +301,7 @@ where } } - fn handle_notification(&mut self, not: Notification) { + fn handle_notification(&mut self, not: Notification) -> Option { match not.method.as_str() { ::METHOD => { let params = ::Params::deserialize( @@ -256,8 +313,15 @@ where let old_auto_setting = self.settings.auto; + let update_thread = self.settings.defer_updates != settings.defer_updates; + self.settings = settings; + if update_thread { + // start/stop a thread and/or update its time settings + self.spawn_or_stop_deferred_update_thread(); + } + if self.settings.auto && !old_auto_setting { if let Err(e) = self.open_preview() { error!("could not open browser: {}", e); @@ -265,18 +329,25 @@ where } self.markdown_server + .lock() + .unwrap() .set_highlight_theme(self.settings.theme.clone()); // There is currently no way to unset the static root wihout restarting the browser if self.settings.serve_static { self.markdown_server + .lock() + .unwrap() .set_static_root(std::env::current_dir().unwrap()) } if let Some(renderer) = &self.settings.renderer { let mut command = Command::new(&renderer.0); command.args(&renderer.1); - self.markdown_server.set_external_renderer(command) + self.markdown_server + .lock() + .unwrap() + .set_external_renderer(command) } } } @@ -287,7 +358,18 @@ where ) .unwrap(); + if self.settings.serve_static { + if self.is_new_uri(¶ms.text_document.uri) { + let tfp = params.text_document.uri.to_file_path(); + if let Some(parent) = tfp.as_ref().ok().and_then(|p| p.parent()) { + self.markdown_server.lock().unwrap().set_static_root(parent); + } + } + } + self.markdown_server + .lock() + .unwrap() .send(params.text_document.text) .unwrap(); } @@ -302,13 +384,23 @@ where assert_eq!(content_changes.len(), 1); - self.markdown_server - .send(content_changes.remove(0).text) - .unwrap(); + let new_doc = content_changes.remove(0).text; + + if self.settings.serve_static { + if self.is_new_uri(¶ms.text_document.uri) { + let tfp = params.text_document.uri.to_file_path(); + if let Some(parent) = tfp.as_ref().ok().and_then(|p| p.parent()) { + self.markdown_server.lock().unwrap().set_static_root(parent); + } + } + } + + return Some(new_doc); } ::METHOD => unreachable!("handled by caller"), method => info!("unimplemented notification method: {}", method), } + None } fn open_preview(&mut self) -> io::Result<()> { @@ -319,13 +411,104 @@ where if let Some((name, args)) = &mut self.settings.browser { let mut command = Command::new(name); command.args(args); - self.markdown_server.open_specific_browser(command) + self.markdown_server + .lock() + .unwrap() + .open_specific_browser(command) + } else { + self.markdown_server.lock().unwrap().open_browser() + } + } + + fn stop_deferred_update_thread(&mut self) { + if let Some((_, c, t)) = self.defer_control.take() { + _ = c.send(DeferEvent::StopThread); + _ = t.join(); + } + } + /// If `self.settings.defer_updates.is_some()`: + /// spawn a second thread which will wait a bit before updating the preview after each change. + /// this way, we can update the preview once for multiple changes. + /// this fixes the problem where, with large documents, the preview lags very far behind. + /// NOTE: If a thread is already running, it is updated instead. + /// NOTE: If `self.settings.defer_updates` is `None`, the thread is stopped instead. + fn spawn_or_stop_deferred_update_thread(&mut self) { + if let Some(defer_updates) = self.settings.defer_updates { + fn gen_durations( + ms_before_update: u64, + ms_between_updates: u64, + ) -> (Duration, Duration) { + ( + Duration::from_millis(ms_before_update), + Duration::from_millis(ms_between_updates.saturating_sub(ms_before_update)), + ) + } + if let Some((_, c, _)) = &self.defer_control { + c.send(DeferEvent::SetDelays(defer_updates)).unwrap(); + } else { + self.defer_control = if let Some((ms_before_update, ms_between_updates)) = + self.settings.defer_updates + { + let current_document = Arc::new(Mutex::new(None)); + // used to wake the thread when the document is changed + let (wake_thread, thread_wake) = std::sync::mpsc::channel(); + // for the thread + let current_document_t = Arc::clone(¤t_document); + let markdown_server = Arc::clone(&self.markdown_server); + let thread = thread::spawn(move || { + let mut delays = gen_durations(ms_before_update, ms_between_updates); + let mut keep_running = true; + while keep_running { + let mut update_preview = false; + fn handle( + e: DeferEvent, + keep_running: &mut bool, + update_preview: &mut bool, + delays: &mut (Duration, Duration), + ) { + match e { + DeferEvent::StopThread => *keep_running = true, + DeferEvent::UpdatePreview => *update_preview = true, + DeferEvent::SetDelays((before, between)) => { + *delays = gen_durations(before, between) + } + } + } + match thread_wake.recv() { + Ok(e) => { + handle(e, &mut keep_running, &mut update_preview, &mut delays) + } + Err(_) => break, + } + if update_preview { + std::thread::sleep(delays.0); + match current_document_t.lock().unwrap().take() { + Some(new_doc) => { + markdown_server.lock().unwrap().send(new_doc).unwrap(); + } + None => {} + } + std::thread::sleep(delays.1); + } + } + }); + Some((current_document, wake_thread, thread)) + } else { + None + }; + } } else { - self.markdown_server.open_browser() + self.stop_deferred_update_thread(); } } } +enum DeferEvent { + StopThread, + UpdatePreview, + SetDelays((u64, u64)), +} + fn deserialize_command<'de, D>(deserializer: D) -> Result<(String, Vec), D::Error> where D: Deserializer<'de>,