aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view')
-rw-r--r--helix-view/Cargo.toml4
-rw-r--r--helix-view/src/document.rs341
-rw-r--r--helix-view/src/editor.rs7
3 files changed, 313 insertions, 39 deletions
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 8d93d2d9..cadbbdbd 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -31,9 +31,11 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea
slotmap = "1"
+encoding_rs = "0.8"
+chardetng = "0.1"
+
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"
which = "4.1"
-
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index d3c6cf9e..92778ad7 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -11,7 +11,7 @@ use helix_core::{
history::History,
line_ending::auto_detect_line_ending,
syntax::{self, LanguageConfiguration},
- ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
+ ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
DEFAULT_LINE_ENDING,
};
@@ -19,6 +19,8 @@ use crate::{DocumentId, Theme, ViewId};
use std::collections::HashMap;
+const BUF_SIZE: usize = 8192;
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
Normal,
@@ -39,6 +41,7 @@ pub struct Document {
pub(crate) selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
+ encoding: &'static encoding_rs::Encoding,
/// Current editing mode.
pub mode: Mode,
@@ -78,6 +81,7 @@ impl fmt::Debug for Document {
.field("text", &self.text)
.field("selections", &self.selections)
.field("path", &self.path)
+ .field("encoding", &self.encoding)
.field("mode", &self.mode)
.field("restore_cursor", &self.restore_cursor)
.field("syntax", &self.syntax)
@@ -116,6 +120,173 @@ impl FromStr for Mode {
}
}
+// The documentation and implementation of this function should be up-to-date with
+// its sibling function, `to_writer()`.
+//
+/// Decodes a stream of bytes into UTF-8, returning a `Rope` and the
+/// encoding it was decoded as. The optional `encoding` parameter can
+/// 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> {
+ // 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
+ // `buf_out` is full or the end of the reader was reached, the
+ // contents are appended to `builder`.
+ let mut buf = [0u8; BUF_SIZE];
+ let mut buf_out = [0u8; BUF_SIZE];
+ let mut builder = RopeBuilder::new();
+
+ // By default, the encoding of the text is auto-detected via the
+ // `chardetng` crate which requires sample data from the reader.
+ // As a manual override to this auto-detection is possible, the
+ // same data is read into `buf` to ensure symmetry in the upcoming
+ // loop.
+ let (encoding, mut decoder, mut slice, mut is_empty) = {
+ let read = reader.read(&mut buf)?;
+ let is_empty = read == 0;
+ let encoding = encoding.unwrap_or_else(|| {
+ let mut encoding_detector = chardetng::EncodingDetector::new();
+ encoding_detector.feed(&buf, is_empty);
+ encoding_detector.guess(None, true)
+ });
+ let decoder = encoding.new_decoder();
+
+ // If the amount of bytes read from the reader is less than
+ // `buf.len()`, it is undesirable to read the bytes afterwards.
+ let slice = &buf[..read];
+ (encoding, decoder, slice, is_empty)
+ };
+
+ // `RopeBuilder::append()` expects a `&str`, so this is the "real"
+ // output buffer. When decoding, the number of bytes in the output
+ // buffer will often exceed the number of bytes in the input buffer.
+ // The `result` returned by `decode_to_str()` will state whether or
+ // not that happened. The contents of `buf_str` is appended to
+ // `builder` and it is reused for the next iteration of the decoding
+ // loop.
+ //
+ // As it is possible to read less than the buffer's maximum from `read()`
+ // even when the end of the reader has yet to be reached, the end of
+ // the reader is determined only when a `read()` call returns `0`.
+ //
+ // SAFETY: `buf_out` is a zero-initialized array, thus it will always
+ // contain valid UTF-8.
+ let buf_str = unsafe { std::str::from_utf8_unchecked_mut(&mut buf_out[..]) };
+ let mut total_written = 0usize;
+ loop {
+ let mut total_read = 0usize;
+
+ loop {
+ let (result, read, written, ..) = decoder.decode_to_str(
+ &slice[total_read..],
+ &mut buf_str[total_written..],
+ is_empty,
+ );
+
+ // These variables act as the read and write cursors of `buf` and `buf_str` respectively.
+ // They are necessary in case the output buffer fills before decoding of the entire input
+ // loop is complete. Otherwise, the loop would endlessly iterate over the same `buf` and
+ // the data inside the output buffer would be overwritten.
+ total_read += read;
+ total_written += written;
+ match result {
+ encoding_rs::CoderResult::InputEmpty => {
+ debug_assert_eq!(slice.len(), total_read);
+ break;
+ }
+ encoding_rs::CoderResult::OutputFull => {
+ debug_assert!(slice.len() > total_read);
+ builder.append(&buf_str[..total_written]);
+ total_written = 0;
+ }
+ }
+ }
+ // Once the end of the stream is reached, the output buffer is
+ // flushed and the loop terminates.
+ if is_empty {
+ debug_assert_eq!(reader.read(&mut buf)?, 0);
+ builder.append(&buf_str[..total_written]);
+ break;
+ }
+
+ // Once the previous input has been processed and decoded, the next set of
+ // data is fetched from the reader. The end of the reader is determined to
+ // be when exactly `0` bytes were read from the reader, as per the invariants
+ // of the `Read` trait.
+ let read = reader.read(&mut buf)?;
+ slice = &buf[..read];
+ is_empty = read == 0;
+ }
+ let rope = builder.finish();
+ Ok((rope, encoding))
+}
+
+// The documentation and implementation of this function should be up-to-date with
+// its sibling function, `from_reader()`.
+//
+/// Encodes the text inside `rope` into the given `encoding` and writes the
+/// encoded output into `writer.` As a `Rope` can only contain valid UTF-8,
+/// 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,
+ rope: &'a Rope,
+) -> Result<(), Error> {
+ // Text inside a `Rope` is stored as non-contiguous blocks of data called
+ // chunks. The absolute size of each chunk is unknown, thus it is impossible
+ // to predict the end of the chunk iterator ahead of time. Instead, it is
+ // determined by filtering the iterator to remove all empty chunks and then
+ // appending an empty chunk to it. This is valuable for detecting when all
+ // chunks in the `Rope` have been iterated over in the subsequent loop.
+ let iter = rope
+ .chunks()
+ .filter(|c| !c.is_empty())
+ .chain(std::iter::once(""));
+ let mut buf = [0u8; BUF_SIZE];
+ let mut encoder = encoding.new_encoder();
+ let mut total_written = 0usize;
+ for chunk in iter {
+ let is_empty = chunk.is_empty();
+ let mut total_read = 0usize;
+
+ loop {
+ let (result, read, written, ..) =
+ encoder.encode_from_utf8(&chunk[total_read..], &mut buf[total_written..], is_empty);
+
+ // These variables act as the read and write cursors of `chunk` and `buf` respectively.
+ // They are necessary in case the output buffer fills before encoding of the entire input
+ // loop is complete. Otherwise, the loop would endlessly iterate over the same `chunk` and
+ // the data inside the output buffer would be overwritten.
+ total_read += read;
+ total_written += written;
+ match result {
+ encoding_rs::CoderResult::InputEmpty => {
+ debug_assert_eq!(chunk.len(), total_read);
+ debug_assert!(buf.len() >= total_written);
+ break;
+ }
+ encoding_rs::CoderResult::OutputFull => {
+ debug_assert!(chunk.len() > total_read);
+ writer.write_all(&buf[..total_written]).await?;
+ total_written = 0;
+ }
+ }
+ }
+
+ // Once the end of the iterator is reached, the output buffer is
+ // flushed and the outer loop terminates.
+ if is_empty {
+ writer.write_all(&buf[..total_written]).await?;
+ writer.flush().await?;
+ break;
+ }
+ }
+ Ok(())
+}
+
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -216,13 +387,15 @@ use helix_lsp::lsp;
use url::Url;
impl Document {
- pub fn new(text: Rope) -> Self {
+ pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
+ let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
let changes = ChangeSet::new(&text);
let old_state = None;
Self {
id: DocumentId::default(),
path: None,
+ encoding,
text,
selections: HashMap::default(),
indent_style: IndentStyle::Spaces(4),
@@ -242,29 +415,31 @@ impl Document {
}
// TODO: async fn?
- pub fn load(
+ /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
+ /// overwritten with the `encoding` parameter.
+ pub fn open(
path: PathBuf,
+ encoding: Option<&'static encoding_rs::Encoding>,
theme: Option<&Theme>,
config_loader: Option<&syntax::Loader>,
) -> Result<Self, Error> {
- use std::{fs::File, io::BufReader};
+ if !path.exists() {
+ return Ok(Self::default());
+ }
- let mut doc = if !path.exists() {
- Rope::from(DEFAULT_LINE_ENDING.as_str())
- } else {
- let file = File::open(&path).context(format!("unable to open {:?}", path))?;
- Rope::from_reader(BufReader::new(file))?
- };
+ let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
+ let (mut rope, encoding) = from_reader(&mut file, encoding)?;
// search for line endings
- let line_ending = auto_detect_line_ending(&doc).unwrap_or(DEFAULT_LINE_ENDING);
+ let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
// add missing newline at the end of file
- if doc.len_bytes() == 0 || !char_is_line_ending(doc.char(doc.len_chars() - 1)) {
- doc.insert(doc.len_chars(), line_ending.as_str());
+ if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
+ rope.insert(rope.len_chars(), line_ending.as_str());
}
- let mut doc = Self::new(doc);
+ let mut doc = Self::from(rope, Some(encoding));
+
// set the path and try detecting the language
doc.set_path(&path)?;
doc.detect_indent_style();
@@ -303,6 +478,8 @@ impl Document {
// 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
+ /// The `Document`'s text is encoded according to its encoding and written to the file located
+ /// at its `path()`.
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
// we clone and move text + path into the future so that we asynchronously save the current
// state without blocking any further edits.
@@ -320,8 +497,11 @@ impl Document {
self.last_saved_revision = history.current_revision();
self.history.set(history);
+ let encoding = self.encoding;
+
+ // We encode the file according to the `Document`'s encoding.
async move {
- use tokio::{fs::File, io::AsyncWriteExt};
+ use tokio::fs::File;
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
@@ -330,13 +510,9 @@ impl Document {
));
}
}
- let mut file = File::create(path).await?;
- // write all the rope chunks to file
- for chunk in text.chunks() {
- file.write_all(chunk.as_bytes()).await?;
- }
- // TODO: flush?
+ let mut file = File::create(path).await?;
+ to_writer(&mut file, encoding, &text).await?;
if let Some(language_server) = language_server {
language_server
@@ -531,7 +707,7 @@ impl Document {
self.selections.insert(view_id, selection);
}
- fn _apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
+ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
let old_doc = self.text().clone();
let success = transaction.changes().apply(&mut self.text);
@@ -594,7 +770,7 @@ impl Document {
});
}
- let success = self._apply(transaction, view_id);
+ let success = self.apply_impl(transaction, view_id);
if !transaction.changes().is_empty() {
// Compose this transaction with the previous one
@@ -608,7 +784,7 @@ impl Document {
pub fn undo(&mut self, view_id: ViewId) {
let mut history = self.history.take();
let success = if let Some(transaction) = history.undo() {
- self._apply(&transaction, view_id)
+ self.apply_impl(transaction, view_id)
} else {
false
};
@@ -623,7 +799,7 @@ impl Document {
pub fn redo(&mut self, view_id: ViewId) {
let mut history = self.history.take();
let success = if let Some(transaction) = history.redo() {
- self._apply(&transaction, view_id)
+ self.apply_impl(transaction, view_id)
} else {
false
};
@@ -638,14 +814,14 @@ impl Document {
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
let txns = self.history.get_mut().earlier(uk);
for txn in txns {
- self._apply(&txn, view_id);
+ self.apply_impl(&txn, view_id);
}
}
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
let txns = self.history.get_mut().later(uk);
for txn in txns {
- self._apply(&txn, view_id);
+ self.apply_impl(&txn, view_id);
}
}
@@ -670,12 +846,10 @@ impl Document {
self.history.set(history);
}
- #[inline]
pub fn id(&self) -> DocumentId {
self.id
}
- #[inline]
pub fn is_modified(&self) -> bool {
let history = self.history.take();
let current_revision = history.current_revision();
@@ -683,12 +857,10 @@ impl Document {
current_revision != self.last_saved_revision || !self.changes.is_empty()
}
- #[inline]
pub fn mode(&self) -> Mode {
self.mode
}
- #[inline]
/// Corresponding language scope name. Usually `source.<lang>`.
pub fn language(&self) -> Option<&str> {
self.language
@@ -696,21 +868,21 @@ impl Document {
.map(|language| language.scope.as_str())
}
- #[inline]
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
self.language.as_deref()
}
- #[inline]
/// Current document version, incremented at each change.
pub fn version(&self) -> i32 {
self.version
}
+ #[inline]
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
self.language_server.as_deref()
}
+ #[inline]
/// Tree-sitter AST tree
pub fn syntax(&self) -> Option<&Syntax> {
self.syntax.as_ref()
@@ -756,10 +928,12 @@ impl Document {
self.path().map(|path| Url::from_file_path(path).unwrap())
}
+ #[inline]
pub fn text(&self) -> &Rope {
&self.text
}
+ #[inline]
pub fn selection(&self, view_id: ViewId) -> &Selection {
&self.selections[&view_id]
}
@@ -787,6 +961,7 @@ impl Document {
// -- LSP methods
+ #[inline]
pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
lsp::TextDocumentIdentifier::new(self.url().unwrap())
}
@@ -795,6 +970,7 @@ impl Document {
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
}
+ #[inline]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
@@ -804,6 +980,13 @@ impl Document {
}
}
+impl Default for Document {
+ fn default() -> Self {
+ let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
+ Self::from(text, None)
+ }
+}
+
#[cfg(test)]
mod test {
use super::*;
@@ -812,7 +995,7 @@ mod test {
fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello");
- let mut doc = Document::new(text);
+ let mut doc = Document::from(text, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(5, 5));
@@ -921,4 +1104,94 @@ mod test {
]
);
}
+
+ #[test]
+ fn test_line_ending() {
+ if cfg!(windows) {
+ assert_eq!(Document::default().text().to_string(), "\r\n");
+ } else {
+ assert_eq!(Document::default().text().to_string(), "\n");
+ }
+ }
+
+ macro_rules! test_decode {
+ ($label:expr, $label_override:expr) => {
+ let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
+ let path = base_path.join(format!("{}_in.txt", $label));
+ let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
+ assert!(path.exists());
+ assert!(ref_path.exists());
+
+ let mut file = std::fs::File::open(path).unwrap();
+ let text = from_reader(&mut file, Some(encoding))
+ .unwrap()
+ .0
+ .to_string();
+ let expectation = std::fs::read_to_string(ref_path).unwrap();
+ assert_eq!(text[..], expectation[..]);
+ };
+ }
+
+ macro_rules! test_encode {
+ ($label:expr, $label_override:expr) => {
+ let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
+ let path = base_path.join(format!("{}_out.txt", $label));
+ let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
+ assert!(path.exists());
+ assert!(ref_path.exists());
+
+ let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
+ let mut buf: Vec<u8> = Vec::new();
+ helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
+
+ let expectation = std::fs::read(ref_path).unwrap();
+ assert_eq!(buf, expectation);
+ };
+ }
+
+ macro_rules! test_decode_fn {
+ ($name:ident, $label:expr, $label_override:expr) => {
+ #[test]
+ fn $name() {
+ test_decode!($label, $label_override);
+ }
+ };
+ ($name:ident, $label:expr) => {
+ #[test]
+ fn $name() {
+ test_decode!($label, $label);
+ }
+ };
+ }
+
+ macro_rules! test_encode_fn {
+ ($name:ident, $label:expr, $label_override:expr) => {
+ #[test]
+ fn $name() {
+ test_encode!($label, $label_override);
+ }
+ };
+ ($name:ident, $label:expr) => {
+ #[test]
+ fn $name() {
+ test_encode!($label, $label);
+ }
+ };
+ }
+
+ test_decode_fn!(test_big5_decode, "big5");
+ test_encode_fn!(test_big5_encode, "big5");
+ test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR");
+ test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR");
+ test_decode_fn!(test_gb18030_decode, "gb18030");
+ test_encode_fn!(test_gb18030_encode, "gb18030");
+ test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
+ test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
+ test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP");
+ test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP");
+ test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP");
+ test_decode_fn!(test_shift_jis_decode, "shift_jis");
+ test_encode_fn!(test_shift_jis_encode, "shift_jis");
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 839bcdcd..7f910b80 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -17,7 +17,7 @@ use anyhow::Error;
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
-use helix_core::{Position, DEFAULT_LINE_ENDING};
+use helix_core::Position;
#[derive(Debug)]
pub struct Editor {
@@ -171,8 +171,7 @@ impl Editor {
}
pub fn new_file(&mut self, action: Action) -> DocumentId {
- use helix_core::Rope;
- let doc = Document::new(Rope::from(DEFAULT_LINE_ENDING.as_str()));
+ let doc = Document::default();
let id = self.documents.insert(doc);
self.documents[id].id = id;
self.switch(id, action);
@@ -190,7 +189,7 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
- let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
+ let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
// try to find a language server based on the language name
let language_server = doc