aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view')
-rw-r--r--helix-view/src/document.rs128
-rw-r--r--helix-view/src/editor.rs81
2 files changed, 185 insertions, 24 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 0daa983f..d6480b32 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -3,6 +3,7 @@ use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use helix_core::auto_pairs::AutoPairs;
use helix_core::Range;
+use log::debug;
use serde::de::{self, Deserialize, Deserializer};
use serde::Serialize;
use std::borrow::Cow;
@@ -13,6 +14,8 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
+use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
+use tokio::sync::Mutex;
use helix_core::{
encoding,
@@ -83,6 +86,16 @@ impl Serialize for Mode {
}
}
+/// A snapshot of the text of a document that we want to write out to disk
+#[derive(Debug, Clone)]
+pub struct DocumentSaveEvent {
+ pub revision: usize,
+ pub doc_id: DocumentId,
+}
+
+pub type DocumentSaveEventResult = Result<DocumentSaveEvent, anyhow::Error>;
+pub type DocumentSaveEventFuture = BoxFuture<'static, DocumentSaveEventResult>;
+
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
@@ -118,6 +131,9 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
pub(crate) modified_since_accessed: bool,
+ save_sender: Option<UnboundedSender<DocumentSaveEventFuture>>,
+ save_receiver: Option<UnboundedReceiver<DocumentSaveEventFuture>>,
+ current_save: Arc<Mutex<Option<DocumentSaveEventFuture>>>,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@@ -338,6 +354,7 @@ impl Document {
let encoding = encoding.unwrap_or(encoding::UTF_8);
let changes = ChangeSet::new(&text);
let old_state = None;
+ let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
Self {
id: DocumentId::default(),
@@ -358,6 +375,9 @@ impl Document {
savepoint: None,
last_saved_revision: 0,
modified_since_accessed: false,
+ save_sender: Some(save_sender),
+ save_receiver: Some(save_receiver),
+ current_save: Arc::new(Mutex::new(None)),
language_server: None,
}
}
@@ -492,29 +512,34 @@ impl Document {
Some(fut.boxed())
}
- pub fn save(&mut self, force: bool) -> impl Future<Output = Result<(), anyhow::Error>> {
+ pub fn save(&mut self, force: bool) -> Result<(), anyhow::Error> {
self.save_impl::<futures_util::future::Ready<_>>(None, force)
}
pub fn format_and_save(
&mut self,
- formatting: Option<impl Future<Output = Result<Transaction, FormatterError>>>,
+ formatting: Option<
+ impl Future<Output = Result<Transaction, FormatterError>> + 'static + Send,
+ >,
force: bool,
- ) -> impl Future<Output = anyhow::Result<()>> {
+ ) -> anyhow::Result<()> {
self.save_impl(formatting, force)
}
- // TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
- // or is that handled by the OS/async layer
+ // TODO: impl Drop to handle ensuring writes when closed
/// The `Document`'s text is encoded according to its encoding and written to the file located
/// at its `path()`.
///
/// If `formatting` is present, it supplies some changes that we apply to the text before saving.
- fn save_impl<F: Future<Output = Result<Transaction, FormatterError>>>(
+ fn save_impl<F: Future<Output = Result<Transaction, FormatterError>> + 'static + Send>(
&mut self,
formatting: Option<F>,
force: bool,
- ) -> impl Future<Output = Result<(), anyhow::Error>> {
+ ) -> Result<(), anyhow::Error> {
+ if self.save_sender.is_none() {
+ bail!("saves are closed for this document!");
+ }
+
// we clone and move text + path into the future so that we asynchronously save the current
// state without blocking any further edits.
@@ -525,12 +550,13 @@ impl Document {
let language_server = self.language_server.clone();
// mark changes up to now as saved
- self.reset_modified();
+ let current_rev = self.get_current_revision();
+ let doc_id = self.id();
let encoding = self.encoding;
// We encode the file according to the `Document`'s encoding.
- async move {
+ let save_event = async move {
use tokio::fs::File;
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
@@ -563,9 +589,14 @@ impl Document {
let mut file = File::create(path).await?;
to_writer(&mut file, encoding, &text).await?;
+ let event = DocumentSaveEvent {
+ revision: current_rev,
+ doc_id,
+ };
+
if let Some(language_server) = language_server {
if !language_server.is_initialized() {
- return Ok(());
+ return Ok(event);
}
if let Some(notification) =
language_server.text_document_did_save(identifier, &text)
@@ -574,8 +605,70 @@ impl Document {
}
}
- Ok(())
+ Ok(event)
+ };
+
+ self.save_sender
+ .as_mut()
+ .unwrap()
+ .send(Box::pin(save_event))
+ .map_err(|err| anyhow!("failed to send save event: {}", err))
+ }
+
+ pub async fn await_save(&mut self) -> Option<DocumentSaveEventResult> {
+ let mut current_save = self.current_save.lock().await;
+ if let Some(ref mut save) = *current_save {
+ let result = save.await;
+ *current_save = None;
+ debug!("save of '{:?}' result: {:?}", self.path(), result);
+ return Some(result);
+ }
+
+ // return early if the receiver is closed
+ self.save_receiver.as_ref()?;
+
+ let save = match self.save_receiver.as_mut().unwrap().recv().await {
+ Some(save) => save,
+ None => {
+ self.save_receiver = None;
+ return None;
+ }
+ };
+
+ // save a handle to the future so that when a poll on this
+ // function gets cancelled, we don't lose it
+ *current_save = Some(save);
+ debug!("awaiting save of '{:?}'", self.path());
+
+ let result = (*current_save).as_mut().unwrap().await;
+ *current_save = None;
+
+ debug!("save of '{:?}' result: {:?}", self.path(), result);
+
+ Some(result)
+ }
+
+ /// Prepares the Document for being closed by stopping any new writes
+ /// and flushing through the queue of pending writes. If any fail,
+ /// it stops early before emptying the rest of the queue. Callers
+ /// should keep calling until it returns None.
+ pub async fn close(&mut self) -> Option<DocumentSaveEventResult> {
+ if self.save_sender.is_some() {
+ self.save_sender = None;
}
+
+ let mut final_result = None;
+
+ while let Some(save_event) = self.await_save().await {
+ let is_err = save_event.is_err();
+ final_result = Some(save_event);
+
+ if is_err {
+ break;
+ }
+ }
+
+ final_result
}
/// Detect the programming language based on the file type.
@@ -941,6 +1034,19 @@ impl Document {
self.last_saved_revision = current_revision;
}
+ /// Set the document's latest saved revision to the given one.
+ pub fn set_last_saved_revision(&mut self, rev: usize) {
+ self.last_saved_revision = rev;
+ }
+
+ /// Get the current revision number
+ pub fn get_current_revision(&mut self) -> usize {
+ let history = self.history.take();
+ let current_revision = history.current_revision();
+ self.history.set(history);
+ current_revision
+ }
+
/// Corresponding language scope name. Usually `source.<lang>`.
pub fn language_scope(&self) -> Option<&str> {
self.language
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index e9a3c639..ec6119a4 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,6 +1,6 @@
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
- document::Mode,
+ document::{DocumentSaveEventResult, Mode},
graphics::{CursorKind, Rect},
info::Info,
input::KeyEvent,
@@ -9,8 +9,9 @@ use crate::{
Document, DocumentId, View, ViewId,
};
-use futures_util::future;
-use futures_util::stream::select_all::SelectAll;
+use futures_util::stream::{select_all::SelectAll, FuturesUnordered};
+use futures_util::{future, StreamExt};
+use helix_lsp::Call;
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
@@ -65,7 +66,7 @@ where
)
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig {
/// IgnoreOptions
@@ -172,7 +173,7 @@ pub struct Config {
pub color_modes: bool,
}
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig {
pub command: String,
@@ -225,7 +226,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
None
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig {
/// Display LSP progress messages below statusline
@@ -246,7 +247,7 @@ impl Default for LspConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct SearchConfig {
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
@@ -255,7 +256,7 @@ pub struct SearchConfig {
pub wrap_around: bool,
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>,
@@ -279,7 +280,7 @@ impl Default for StatusLineConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct ModeConfig {
pub normal: String,
@@ -458,7 +459,7 @@ impl std::str::FromStr for GutterType {
}
}
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceConfig {
pub render: WhitespaceRender,
@@ -688,6 +689,15 @@ pub struct Editor {
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
}
+#[derive(Debug)]
+pub enum EditorEvent {
+ DocumentSave(DocumentSaveEventResult),
+ ConfigEvent(ConfigEvent),
+ LanguageServerMessage((usize, Call)),
+ DebuggerEvent(dap::Payload),
+ IdleTimer,
+}
+
#[derive(Debug, Clone)]
pub enum ConfigEvent {
Refresh,
@@ -719,6 +729,8 @@ pub enum CloseError {
DoesNotExist,
/// Buffer is modified
BufferModified(String),
+ /// Document failed to save
+ SaveError(anyhow::Error),
}
impl Editor {
@@ -1079,8 +1091,12 @@ impl Editor {
self._refresh();
}
- pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
- let doc = match self.documents.get(&doc_id) {
+ pub async fn close_document(
+ &mut self,
+ doc_id: DocumentId,
+ force: bool,
+ ) -> Result<(), CloseError> {
+ let doc = match self.documents.get_mut(&doc_id) {
Some(doc) => doc,
None => return Err(CloseError::DoesNotExist),
};
@@ -1089,8 +1105,19 @@ impl Editor {
return Err(CloseError::BufferModified(doc.display_name().into_owned()));
}
+ if let Some(Err(err)) = doc.close().await {
+ return Err(CloseError::SaveError(err));
+ }
+
+ // Don't fail the whole write because the language server could not
+ // acknowledge the close
if let Some(language_server) = doc.language_server() {
- tokio::spawn(language_server.text_document_did_close(doc.identifier()));
+ if let Err(err) = language_server
+ .text_document_did_close(doc.identifier())
+ .await
+ {
+ log::error!("Error closing doc in language server: {}", err);
+ }
}
enum Action {
@@ -1269,4 +1296,32 @@ impl Editor {
.await
.map(|_| ())
}
+
+ pub async fn wait_event(&mut self) -> EditorEvent {
+ let mut saves: FuturesUnordered<_> = self
+ .documents
+ .values_mut()
+ .map(Document::await_save)
+ .collect();
+
+ tokio::select! {
+ biased;
+
+ Some(Some(event)) = saves.next() => {
+ EditorEvent::DocumentSave(event)
+ }
+ Some(config_event) = self.config_events.1.recv() => {
+ EditorEvent::ConfigEvent(config_event)
+ }
+ Some(message) = self.language_servers.incoming.next() => {
+ EditorEvent::LanguageServerMessage(message)
+ }
+ Some(event) = self.debugger_events.next() => {
+ EditorEvent::DebuggerEvent(event)
+ }
+ _ = &mut self.idle_timer => {
+ EditorEvent::IdleTimer
+ }
+ }
+ }
}