aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src/document.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view/src/document.rs')
-rw-r--r--helix-view/src/document.rs246
1 files changed, 118 insertions, 128 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 76b19a07..c0186ee5 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,5 +1,6 @@
-use anyhow::{anyhow, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
+use serde::Serialize;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
@@ -9,7 +10,8 @@ use std::str::FromStr;
use std::sync::Arc;
use helix_core::{
- history::History,
+ encoding,
+ history::{History, UndoKind},
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
syntax::{self, LanguageConfiguration},
@@ -18,7 +20,7 @@ use helix_core::{
};
use helix_lsp::util::LspFormatting;
-use crate::{DocumentId, Theme, ViewId};
+use crate::{DocumentId, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
@@ -29,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
- Normal,
- Select,
- Insert,
+ Normal = 0,
+ Select = 1,
+ Insert = 2,
}
impl Display for Mode {
@@ -52,7 +54,7 @@ impl FromStr for Mode {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
- _ => Err(anyhow!("Invalid mode '{}'", s)),
+ _ => bail!("Invalid mode '{}'", s),
}
}
}
@@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode {
}
}
+impl Serialize for Mode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
pub(crate) selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
- encoding: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
/// Current editing mode.
pub mode: Mode,
@@ -104,6 +115,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
+ pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +139,7 @@ impl fmt::Debug for Document {
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
+ .field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
@@ -141,8 +154,8 @@ impl fmt::Debug for Document {
/// be used to override encoding auto-detection.
pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R,
- encoding: Option<&'static encoding_rs::Encoding>,
-) -> Result<(Rope, &'static encoding_rs::Encoding), Error> {
+ encoding: Option<&'static encoding::Encoding>,
+) -> Result<(Rope, &'static encoding::Encoding), Error> {
// These two buffers are 8192 bytes in size each and are used as
// intermediaries during the decoding process. Text read into `buf`
// from `reader` is decoded into `buf_out` as UTF-8. Once either
@@ -210,11 +223,11 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(slice.len(), total_read);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(slice.len() > total_read);
builder.append(&buf_str[..total_written]);
total_written = 0;
@@ -249,7 +262,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
/// replacement characters may appear in the encoded text.
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
writer: &'a mut W,
- encoding: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
rope: &'a Rope,
) -> Result<(), Error> {
// Text inside a `Rope` is stored as non-contiguous blocks of data called
@@ -284,12 +297,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(chunk.len(), total_read);
debug_assert!(buf.len() >= total_written);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(chunk.len() > total_read);
writer.write_all(&buf[..total_written]).await?;
total_written = 0;
@@ -320,8 +333,8 @@ use helix_lsp::lsp;
use url::Url;
impl Document {
- pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
let changes = ChangeSet::new(&text);
let old_state = None;
@@ -344,6 +357,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
+ modified_since_accessed: false,
language_server: None,
}
}
@@ -353,9 +367,8 @@ impl Document {
/// overwritten with the `encoding` parameter.
pub fn open(
path: &Path,
- encoding: Option<&'static encoding_rs::Encoding>,
- theme: Option<&Theme>,
- config_loader: Option<&syntax::Loader>,
+ encoding: Option<&'static encoding::Encoding>,
+ config_loader: Option<Arc<syntax::Loader>>,
) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding) = if path.exists() {
@@ -363,7 +376,7 @@ impl Document {
std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)?
} else {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
(Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
};
@@ -372,7 +385,7 @@ impl Document {
// set the path and try detecting the language
doc.set_path(Some(path))?;
if let Some(loader) = config_loader {
- doc.detect_language(theme, loader);
+ doc.detect_language(loader);
}
doc.detect_indent_and_line_ending();
@@ -383,7 +396,7 @@ impl Document {
/// The same as [`format`], but only returns formatting changes if auto-formatting
/// is configured.
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if self.language_config().map(|c| c.auto_format) == Some(true) {
+ if self.language_config()?.auto_format {
self.format()
} else {
None
@@ -393,30 +406,27 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if let Some(language_server) = self.language_server() {
- let text = self.text.clone();
- let offset_encoding = language_server.offset_encoding();
- let request = language_server.text_document_formatting(
- self.identifier(),
- lsp::FormattingOptions::default(),
- None,
- )?;
-
- let fut = async move {
- let edits = request.await.unwrap_or_else(|e| {
- log::warn!("LSP formatting failed: {}", e);
- Default::default()
- });
- LspFormatting {
- doc: text,
- edits,
- offset_encoding,
- }
- };
- Some(fut)
- } else {
- None
- }
+ let language_server = self.language_server()?;
+ let text = self.text.clone();
+ let offset_encoding = language_server.offset_encoding();
+ let request = language_server.text_document_formatting(
+ self.identifier(),
+ lsp::FormattingOptions::default(),
+ None,
+ )?;
+
+ let fut = async move {
+ let edits = request.await.unwrap_or_else(|e| {
+ log::warn!("LSP formatting failed: {}", e);
+ Default::default()
+ });
+ LspFormatting {
+ doc: text,
+ edits,
+ offset_encoding,
+ }
+ };
+ Some(fut)
}
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -460,9 +470,7 @@ impl Document {
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
- return Err(Error::msg(
- "can't save file, parent directory does not exist",
- ));
+ bail!("can't save file, parent directory does not exist");
}
}
@@ -494,12 +502,12 @@ impl Document {
}
/// Detect the programming language based on the file type.
- pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
+ pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) {
if let Some(path) = &self.path {
let language_config = config_loader
.language_config_for_file_name(path)
.or_else(|| config_loader.language_config_for_shebang(self.text()));
- self.set_language(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
}
@@ -509,8 +517,7 @@ impl Document {
/// line ending.
pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
});
@@ -524,7 +531,7 @@ impl Document {
// If there is no path or the path no longer exists.
if path.is_none() {
- return Err(anyhow!("can't find file to reload from"));
+ bail!("can't find file to reload from");
}
let mut file = std::fs::File::open(path.unwrap())?;
@@ -545,15 +552,13 @@ impl Document {
/// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
- match encoding_rs::Encoding::for_label(label.as_bytes()) {
- Some(encoding) => self.encoding = encoding,
- None => return Err(anyhow::anyhow!("unknown encoding")),
- }
+ self.encoding = encoding::Encoding::for_label(label.as_bytes())
+ .ok_or_else(|| anyhow!("unknown encoding"))?;
Ok(())
}
/// Returns the [`Document`]'s current encoding.
- pub fn encoding(&self) -> &'static encoding_rs::Encoding {
+ pub fn encoding(&self) -> &'static encoding::Encoding {
self.encoding
}
@@ -573,15 +578,13 @@ impl Document {
/// if it exists.
pub fn set_language(
&mut self,
- theme: Option<&Theme>,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
+ loader: Option<Arc<helix_core::syntax::Loader>>,
) {
- if let Some(language_config) = language_config {
- let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
- if let Some(highlight_config) = language_config.highlight_config(scopes) {
- let syntax = Syntax::new(&self.text, highlight_config);
+ if let (Some(language_config), Some(loader)) = (language_config, loader) {
+ if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
+ let syntax = Syntax::new(&self.text, highlight_config, loader);
self.syntax = Some(syntax);
- // TODO: config.configure(scopes) is now delayed, is that ok?
}
self.language = Some(language_config);
@@ -593,15 +596,10 @@ impl Document {
/// Set the programming language for the file if you know the name (scope) but don't have the
/// [`syntax::LanguageConfiguration`] for it.
- pub fn set_language2(
- &mut self,
- scope: &str,
- theme: Option<&Theme>,
- config_loader: Arc<syntax::Loader>,
- ) {
+ pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) {
let language_config = config_loader.language_config_for_scope(scope);
- self.set_language(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
/// Set the LSP.
@@ -639,6 +637,8 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
+
+ self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {
@@ -680,7 +680,7 @@ impl Document {
if let Some(notify) = notify {
tokio::spawn(notify);
- } //.expect("failed to emit textDocument/didChange");
+ }
}
}
success
@@ -708,11 +708,11 @@ impl Document {
success
}
- /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
- pub fn undo(&mut self, view_id: ViewId) -> bool {
+ fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
let mut history = self.history.take();
- let success = if let Some(transaction) = history.undo() {
- self.apply_impl(transaction, view_id)
+ let txn = if undo { history.undo() } else { history.redo() };
+ let success = if let Some(txn) = txn {
+ self.apply_impl(txn, view_id)
} else {
false
};
@@ -725,21 +725,14 @@ impl Document {
success
}
+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
+ pub fn undo(&mut self, view_id: ViewId) -> bool {
+ self.undo_redo_impl(view_id, true)
+ }
+
/// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
pub fn redo(&mut self, view_id: ViewId) -> bool {
- let mut history = self.history.take();
- let success = if let Some(transaction) = history.redo() {
- self.apply_impl(transaction, view_id)
- } else {
- false
- };
- self.history.set(history);
-
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ self.undo_redo_impl(view_id, false)
}
pub fn savepoint(&mut self) {
@@ -752,9 +745,12 @@ impl Document {
}
}
- /// Undo modifications to the [`Document`] according to `uk`.
- pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().earlier(uk);
+ fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
+ let txns = if earlier {
+ self.history.get_mut().earlier(uk)
+ } else {
+ self.history.get_mut().later(uk)
+ };
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view_id) {
@@ -768,20 +764,14 @@ impl Document {
success
}
+ /// Undo modifications to the [`Document`] according to `uk`.
+ pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, true)
+ }
+
/// Redo modifications to the [`Document`] according to `uk`.
- pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().later(uk);
- let mut success = false;
- for txn in txns {
- if self.apply_impl(&txn, view_id) {
- success = true;
- }
- }
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, false)
}
/// Commit pending changes to history
@@ -837,6 +827,16 @@ impl Document {
.map(|language| language.scope.as_str())
}
+ /// Language ID for the document. Either the `language-id` from the
+ /// `language-server` configuration, or the document language if no
+ /// `language-id` has been specified.
+ pub fn language_id(&self) -> Option<&str> {
+ self.language_config()
+ .and_then(|config| config.language_server.as_ref())
+ .and_then(|lsp_config| lsp_config.language_id.as_deref())
+ .or_else(|| Some(self.language()?.rsplit_once('.')?.1))
+ }
+
/// Corresponding [`LanguageConfiguration`].
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
self.language.as_deref()
@@ -847,18 +847,10 @@ impl Document {
self.version
}
+ /// Language server if it has been initialized.
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
- let server = self.language_server.as_deref();
- let initialized = server
- .map(|server| server.is_initialized())
- .unwrap_or(false);
-
- // only resolve language_server if it's initialized
- if initialized {
- server
- } else {
- None
- }
+ let server = self.language_server.as_deref()?;
+ server.is_initialized().then(|| server)
}
#[inline]
@@ -869,8 +861,7 @@ impl Document {
/// Tab size in columns.
pub fn tab_width(&self) -> usize {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(4, |config| config.tab_width) // fallback to 4 columns
}
@@ -883,6 +874,10 @@ impl Document {
self.indent_style.as_str()
}
+ pub fn changes(&self) -> &ChangeSet {
+ &self.changes
+ }
+
#[inline]
/// File path on disk.
pub fn path(&self) -> Option<&PathBuf> {
@@ -891,7 +886,7 @@ impl Document {
/// File path as a URL.
pub fn url(&self) -> Option<Url> {
- self.path().map(|path| Url::from_file_path(path).unwrap())
+ Url::from_file_path(self.path()?).ok()
}
#[inline]
@@ -914,10 +909,6 @@ impl Document {
.map(helix_core::path::get_relative_path)
}
- // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
- // self.state.doc.slice
- // }
-
// transact(Fn) ?
// -- LSP methods
@@ -938,7 +929,6 @@ impl Document {
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
- // sort by range
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
}
@@ -1113,7 +1103,7 @@ mod test {
macro_rules! test_decode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_in.txt", $label));
let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
@@ -1132,7 +1122,7 @@ mod test {
macro_rules! test_encode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_out.txt", $label));
let ref_path = base_path.join(format!("{}_out_ref.txt", $label));