on master help sign in
Handle non-UTF8 files
fd98e74Shafkath Shuhan
.gitattributesnew file+1
@@ -0,0 +1 @@
1+helix-view/tests/* linguist-generated
Cargo.lock+35−7
@@ -65,12 +65,29 @@ dependencies = [
65 "jobserver",
65 "jobserver",
66]
66]
67
67
68+[[package]]
69+name = "cfg-if"
70+version = "0.1.10"
71+source = "registry+https://github.com/rust-lang/crates.io-index"
72+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
73+
68[[package]]
74[[package]]
69name = "cfg-if"
75name = "cfg-if"
70version = "1.0.0"
76version = "1.0.0"
71source = "registry+https://github.com/rust-lang/crates.io-index"
77source = "registry+https://github.com/rust-lang/crates.io-index"
72checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
78checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
73
79
80+[[package]]
81+name = "chardetng"
82+version = "0.1.13"
83+source = "registry+https://github.com/rust-lang/crates.io-index"
84+checksum = "81a81b0d8f8ee23417182818b4f06312c5f535c2b04eef1773f7c24bbdf8c500"
85+dependencies = [
86+ "cfg-if 0.1.10",
87+ "encoding_rs",
88+ "memchr",
89+]
90+
74[[package]]
91[[package]]
75name = "chrono"
92name = "chrono"
76version = "0.4.19"
93version = "0.4.19"
@@ -89,7 +106,7 @@ version = "0.8.5"
89source = "registry+https://github.com/rust-lang/crates.io-index"
106source = "registry+https://github.com/rust-lang/crates.io-index"
90checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
107checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
91dependencies = [
108dependencies = [
92− "cfg-if",
109+ "cfg-if 1.0.0",
93 "lazy_static",
110 "lazy_static",
94]
111]
95
112
@@ -125,7 +142,7 @@ version = "2.0.0"
125source = "registry+https://github.com/rust-lang/crates.io-index"
142source = "registry+https://github.com/rust-lang/crates.io-index"
126checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
143checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
127dependencies = [
144dependencies = [
128− "cfg-if",
145+ "cfg-if 1.0.0",
129 "dirs-sys-next",
146 "dirs-sys-next",
130]
147]
131
148
@@ -146,13 +163,22 @@ version = "1.6.1"
146source = "registry+https://github.com/rust-lang/crates.io-index"
163source = "registry+https://github.com/rust-lang/crates.io-index"
147checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
164checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
148
165
166+[[package]]
167+name = "encoding_rs"
168+version = "0.8.28"
169+source = "registry+https://github.com/rust-lang/crates.io-index"
170+checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
171+dependencies = [
172+ "cfg-if 1.0.0",
173+]
174+
149[[package]]
175[[package]]
150name = "etcetera"
176name = "etcetera"
151version = "0.3.2"
177version = "0.3.2"
152source = "registry+https://github.com/rust-lang/crates.io-index"
178source = "registry+https://github.com/rust-lang/crates.io-index"
153checksum = "016b04fd1e94fb833d432634245c9bb61cf1c7409668a0e7d4c3ab00c5172dec"
179checksum = "016b04fd1e94fb833d432634245c9bb61cf1c7409668a0e7d4c3ab00c5172dec"
154dependencies = [
180dependencies = [
155− "cfg-if",
181+ "cfg-if 1.0.0",
156 "dirs-next",
182 "dirs-next",
157 "thiserror",
183 "thiserror",
158]
184]
@@ -244,7 +270,7 @@ version = "0.2.3"
244source = "registry+https://github.com/rust-lang/crates.io-index"
270source = "registry+https://github.com/rust-lang/crates.io-index"
245checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
271checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
246dependencies = [
272dependencies = [
247− "cfg-if",
273+ "cfg-if 1.0.0",
248 "libc",
274 "libc",
249 "wasi",
275 "wasi",
250]
276]
@@ -355,7 +381,9 @@ name = "helix-view"
355version = "0.2.0"
381version = "0.2.0"
356dependencies = [
382dependencies = [
357 "anyhow",
383 "anyhow",
384+ "chardetng",
358 "crossterm",
385 "crossterm",
386+ "encoding_rs",
359 "futures-util",
387 "futures-util",
360 "helix-core",
388 "helix-core",
361 "helix-lsp",
389 "helix-lsp",
@@ -414,7 +442,7 @@ version = "0.1.9"
414source = "registry+https://github.com/rust-lang/crates.io-index"
442source = "registry+https://github.com/rust-lang/crates.io-index"
415checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
443checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
416dependencies = [
444dependencies = [
417− "cfg-if",
445+ "cfg-if 1.0.0",
418]
446]
419
447
420[[package]]
448[[package]]
@@ -472,7 +500,7 @@ version = "0.4.14"
472source = "registry+https://github.com/rust-lang/crates.io-index"
500source = "registry+https://github.com/rust-lang/crates.io-index"
473checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
501checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
474dependencies = [
502dependencies = [
475− "cfg-if",
503+ "cfg-if 1.0.0",
476]
504]
477
505
478[[package]]
506[[package]]
@@ -595,7 +623,7 @@ version = "0.8.3"
595source = "registry+https://github.com/rust-lang/crates.io-index"
623source = "registry+https://github.com/rust-lang/crates.io-index"
596checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
624checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
597dependencies = [
625dependencies = [
598− "cfg-if",
626+ "cfg-if 1.0.0",
599 "instant",
627 "instant",
600 "libc",
628 "libc",
601 "redox_syscall",
629 "redox_syscall",
helix-core/src/lib.rs+1−1
@@ -101,7 +101,7 @@ pub use etcetera::home_dir;
101
101
102use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
102use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
103
103
104−pub use ropey::{Rope, RopeSlice};
104+pub use ropey::{Rope, RopeBuilder, RopeSlice};
105
105
106pub use tendril::StrTendril as Tendril;
106pub use tendril::StrTendril as Tendril;
107
107
helix-term/src/commands.rs+2−12
@@ -239,7 +239,6 @@ impl Command {
239 join_selections,
239 join_selections,
240 keep_selections,
240 keep_selections,
241 keep_primary_selection,
241 keep_primary_selection,
242− save,
243 completion,
242 completion,
244 hover,
243 hover,
245 toggle_comments,
244 toggle_comments,
@@ -1080,7 +1079,7 @@ mod cmd {
1080 if autofmt {
1079 if autofmt {
1081 doc.format(view.id); // TODO: merge into save
1080 doc.format(view.id); // TODO: merge into save
1082 }
1081 }
1083− helix_lsp::block_on(doc.save());
1082+ helix_lsp::block_on(tokio::spawn(doc.save()));
1084 Ok(())
1083 Ok(())
1085 }
1084 }
1086
1085
@@ -1256,7 +1255,7 @@ mod cmd {
1256 errors.push_str("cannot write a buffer without a filename\n");
1255 errors.push_str("cannot write a buffer without a filename\n");
1257 continue;
1256 continue;
1258 }
1257 }
1259− helix_lsp::block_on(doc.save());
1258+ helix_lsp::block_on(tokio::spawn(doc.save()));
1260 }
1259 }
1261 editor.set_error(errors);
1260 editor.set_error(errors);
1262
1261
@@ -2950,15 +2949,6 @@ fn keep_primary_selection(cx: &mut Context) {
2950 doc.set_selection(view.id, selection);
2949 doc.set_selection(view.id, selection);
2951}
2950}
2952
2951
2953−//
2954−
2955−fn save(cx: &mut Context) {
2956− // Spawns an async task to actually do the saving. This way we prevent blocking.
2957−
2958− // TODO: handle save errors somehow?
2959− tokio::spawn(doc_mut!(cx.editor).save());
2960−}
2961−
2962fn completion(cx: &mut Context) {
2952fn completion(cx: &mut Context) {
2963 // trigger on trigger char, or if user calls it
2953 // trigger on trigger char, or if user calls it
2964 // (or on word char typing??)
2954 // (or on word char typing??)
helix-view/Cargo.toml+3−1
@@ -31,9 +31,11 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea
31
31
32slotmap = "1"
32slotmap = "1"
33
33
34+encoding_rs = "0.8"
35+chardetng = "0.1"
36+
34serde = { version = "1.0", features = ["derive"] }
37serde = { version = "1.0", features = ["derive"] }
35toml = "0.5"
38toml = "0.5"
36log = "~0.4"
39log = "~0.4"
37
40
38which = "4.1"
41which = "4.1"
39−
helix-view/src/document.rs+307−34
@@ -11,7 +11,7 @@ use helix_core::{
11 history::History,
11 history::History,
12 line_ending::auto_detect_line_ending,
12 line_ending::auto_detect_line_ending,
13 syntax::{self, LanguageConfiguration},
13 syntax::{self, LanguageConfiguration},
14− ChangeSet, Diagnostic, LineEnding, Rope, Selection, State, Syntax, Transaction,
14+ ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
15 DEFAULT_LINE_ENDING,
15 DEFAULT_LINE_ENDING,
16};
16};
17
17
@@ -19,6 +19,8 @@ use crate::{DocumentId, Theme, ViewId};
19
19
20use std::collections::HashMap;
20use std::collections::HashMap;
21
21
22+const BUF_SIZE: usize = 8192;
23+
22#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
24#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
23pub enum Mode {
25pub enum Mode {
24 Normal,
26 Normal,
@@ -39,6 +41,7 @@ pub struct Document {
39 pub(crate) selections: HashMap<ViewId, Selection>,
41 pub(crate) selections: HashMap<ViewId, Selection>,
40
42
41 path: Option<PathBuf>,
43 path: Option<PathBuf>,
44+ encoding: &'static encoding_rs::Encoding,
42
45
43 /// Current editing mode.
46 /// Current editing mode.
44 pub mode: Mode,
47 pub mode: Mode,
@@ -78,6 +81,7 @@ impl fmt::Debug for Document {
78 .field("text", &self.text)
81 .field("text", &self.text)
79 .field("selections", &self.selections)
82 .field("selections", &self.selections)
80 .field("path", &self.path)
83 .field("path", &self.path)
84+ .field("encoding", &self.encoding)
81 .field("mode", &self.mode)
85 .field("mode", &self.mode)
82 .field("restore_cursor", &self.restore_cursor)
86 .field("restore_cursor", &self.restore_cursor)
83 .field("syntax", &self.syntax)
87 .field("syntax", &self.syntax)
@@ -116,6 +120,173 @@ impl FromStr for Mode {
116 }
120 }
117}
121}
118
122
123+// The documentation and implementation of this function should be up-to-date with
124+// its sibling function, `to_writer()`.
125+//
126+/// Decodes a stream of bytes into UTF-8, returning a `Rope` and the
127+/// encoding it was decoded as. The optional `encoding` parameter can
128+/// be used to override encoding auto-detection.
129+pub fn from_reader<R: std::io::Read + ?Sized>(
130+ reader: &mut R,
131+ encoding: Option<&'static encoding_rs::Encoding>,
132+) -> Result<(Rope, &'static encoding_rs::Encoding), Error> {
133+ // These two buffers are 8192 bytes in size each and are used as
134+ // intermediaries during the decoding process. Text read into `buf`
135+ // from `reader` is decoded into `buf_out` as UTF-8. Once either
136+ // `buf_out` is full or the end of the reader was reached, the
137+ // contents are appended to `builder`.
138+ let mut buf = [0u8; BUF_SIZE];
139+ let mut buf_out = [0u8; BUF_SIZE];
140+ let mut builder = RopeBuilder::new();
141+
142+ // By default, the encoding of the text is auto-detected via the
143+ // `chardetng` crate which requires sample data from the reader.
144+ // As a manual override to this auto-detection is possible, the
145+ // same data is read into `buf` to ensure symmetry in the upcoming
146+ // loop.
147+ let (encoding, mut decoder, mut slice, mut is_empty) = {
148+ let read = reader.read(&mut buf)?;
149+ let is_empty = read == 0;
150+ let encoding = encoding.unwrap_or_else(|| {
151+ let mut encoding_detector = chardetng::EncodingDetector::new();
152+ encoding_detector.feed(&buf, is_empty);
153+ encoding_detector.guess(None, true)
154+ });
155+ let decoder = encoding.new_decoder();
156+
157+ // If the amount of bytes read from the reader is less than
158+ // `buf.len()`, it is undesirable to read the bytes afterwards.
159+ let slice = &buf[..read];
160+ (encoding, decoder, slice, is_empty)
161+ };
162+
163+ // `RopeBuilder::append()` expects a `&str`, so this is the "real"
164+ // output buffer. When decoding, the number of bytes in the output
165+ // buffer will often exceed the number of bytes in the input buffer.
166+ // The `result` returned by `decode_to_str()` will state whether or
167+ // not that happened. The contents of `buf_str` is appended to
168+ // `builder` and it is reused for the next iteration of the decoding
169+ // loop.
170+ //
171+ // As it is possible to read less than the buffer's maximum from `read()`
172+ // even when the end of the reader has yet to be reached, the end of
173+ // the reader is determined only when a `read()` call returns `0`.
174+ //
175+ // SAFETY: `buf_out` is a zero-initialized array, thus it will always
176+ // contain valid UTF-8.
177+ let buf_str = unsafe { std::str::from_utf8_unchecked_mut(&mut buf_out[..]) };
178+ let mut total_written = 0usize;
179+ loop {
180+ let mut total_read = 0usize;
181+
182+ loop {
183+ let (result, read, written, ..) = decoder.decode_to_str(
184+ &slice[total_read..],
185+ &mut buf_str[total_written..],
186+ is_empty,
187+ );
188+
189+ // These variables act as the read and write cursors of `buf` and `buf_str` respectively.
190+ // They are necessary in case the output buffer fills before decoding of the entire input
191+ // loop is complete. Otherwise, the loop would endlessly iterate over the same `buf` and
192+ // the data inside the output buffer would be overwritten.
193+ total_read += read;
194+ total_written += written;
195+ match result {
196+ encoding_rs::CoderResult::InputEmpty => {
197+ debug_assert_eq!(slice.len(), total_read);
198+ break;
199+ }
200+ encoding_rs::CoderResult::OutputFull => {
201+ debug_assert!(slice.len() > total_read);
202+ builder.append(&buf_str[..total_written]);
203+ total_written = 0;
204+ }
205+ }
206+ }
207+ // Once the end of the stream is reached, the output buffer is
208+ // flushed and the loop terminates.
209+ if is_empty {
210+ debug_assert_eq!(reader.read(&mut buf)?, 0);
211+ builder.append(&buf_str[..total_written]);
212+ break;
213+ }
214+
215+ // Once the previous input has been processed and decoded, the next set of
216+ // data is fetched from the reader. The end of the reader is determined to
217+ // be when exactly `0` bytes were read from the reader, as per the invariants
218+ // of the `Read` trait.
219+ let read = reader.read(&mut buf)?;
220+ slice = &buf[..read];
221+ is_empty = read == 0;
222+ }
223+ let rope = builder.finish();
224+ Ok((rope, encoding))
225+}
226+
227+// The documentation and implementation of this function should be up-to-date with
228+// its sibling function, `from_reader()`.
229+//
230+/// Encodes the text inside `rope` into the given `encoding` and writes the
231+/// encoded output into `writer.` As a `Rope` can only contain valid UTF-8,
232+/// replacement characters may appear in the encoded text.
233+pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
234+ writer: &'a mut W,
235+ encoding: &'static encoding_rs::Encoding,
236+ rope: &'a Rope,
237+) -> Result<(), Error> {
238+ // Text inside a `Rope` is stored as non-contiguous blocks of data called
239+ // chunks. The absolute size of each chunk is unknown, thus it is impossible
240+ // to predict the end of the chunk iterator ahead of time. Instead, it is
241+ // determined by filtering the iterator to remove all empty chunks and then
242+ // appending an empty chunk to it. This is valuable for detecting when all
243+ // chunks in the `Rope` have been iterated over in the subsequent loop.
244+ let iter = rope
245+ .chunks()
246+ .filter(|c| !c.is_empty())
247+ .chain(std::iter::once(""));
248+ let mut buf = [0u8; BUF_SIZE];
249+ let mut encoder = encoding.new_encoder();
250+ let mut total_written = 0usize;
251+ for chunk in iter {
252+ let is_empty = chunk.is_empty();
253+ let mut total_read = 0usize;
254+
255+ loop {
256+ let (result, read, written, ..) =
257+ encoder.encode_from_utf8(&chunk[total_read..], &mut buf[total_written..], is_empty);
258+
259+ // These variables act as the read and write cursors of `chunk` and `buf` respectively.
260+ // They are necessary in case the output buffer fills before encoding of the entire input
261+ // loop is complete. Otherwise, the loop would endlessly iterate over the same `chunk` and
262+ // the data inside the output buffer would be overwritten.
263+ total_read += read;
264+ total_written += written;
265+ match result {
266+ encoding_rs::CoderResult::InputEmpty => {
267+ debug_assert_eq!(chunk.len(), total_read);
268+ debug_assert!(buf.len() >= total_written);
269+ break;
270+ }
271+ encoding_rs::CoderResult::OutputFull => {
272+ debug_assert!(chunk.len() > total_read);
273+ writer.write_all(&buf[..total_written]).await?;
274+ total_written = 0;
275+ }
276+ }
277+ }
278+
279+ // Once the end of the iterator is reached, the output buffer is
280+ // flushed and the outer loop terminates.
281+ if is_empty {
282+ writer.write_all(&buf[..total_written]).await?;
283+ writer.flush().await?;
284+ break;
285+ }
286+ }
287+ Ok(())
288+}
289+
119/// Like std::mem::replace() except it allows the replacement value to be mapped from the
290/// Like std::mem::replace() except it allows the replacement value to be mapped from the
120/// original value.
291/// original value.
121fn take_with<T, F>(mut_ref: &mut T, closure: F)
292fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -216,13 +387,15 @@ use helix_lsp::lsp;
216use url::Url;
387use url::Url;
217
388
218impl Document {
389impl Document {
219− pub fn new(text: Rope) -> Self {
390+ pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
391+ let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
220 let changes = ChangeSet::new(&text);
392 let changes = ChangeSet::new(&text);
221 let old_state = None;
393 let old_state = None;
222
394
223 Self {
395 Self {
224 id: DocumentId::default(),
396 id: DocumentId::default(),
225 path: None,
397 path: None,
398+ encoding,
226 text,
399 text,
227 selections: HashMap::default(),
400 selections: HashMap::default(),
228 indent_style: IndentStyle::Spaces(4),
401 indent_style: IndentStyle::Spaces(4),
@@ -242,29 +415,31 @@ impl Document {
242 }
415 }
243
416
244 // TODO: async fn?
417 // TODO: async fn?
418+ /// Create a new document from `path`. Encoding is auto-detected, but it can be manually
419+ /// overwritten with the `encoding` parameter.
245− pub fn load(
420+ pub fn open(
246 path: PathBuf,
421 path: PathBuf,
422+ encoding: Option<&'static encoding_rs::Encoding>,
247 theme: Option<&Theme>,
423 theme: Option<&Theme>,
248 config_loader: Option<&syntax::Loader>,
424 config_loader: Option<&syntax::Loader>,
249 ) -> Result<Self, Error> {
425 ) -> Result<Self, Error> {
250− use std::{fs::File, io::BufReader};
426+ if !path.exists() {
427+ return Ok(Self::default());
428+ }
251
429
252− let mut doc = if !path.exists() {
253− Rope::from(DEFAULT_LINE_ENDING.as_str())
254− } else {
255− let file = File::open(&path).context(format!("unable to open {:?}", path))?;
430+ let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
256− Rope::from_reader(BufReader::new(file))?
431+ let (mut rope, encoding) = from_reader(&mut file, encoding)?;
257− };
258
432
259 // search for line endings
433 // search for line endings
260− let line_ending = auto_detect_line_ending(&doc).unwrap_or(DEFAULT_LINE_ENDING);
434+ let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
261
435
262 // add missing newline at the end of file
436 // add missing newline at the end of file
263− if doc.len_bytes() == 0 || !char_is_line_ending(doc.char(doc.len_chars() - 1)) {
437+ if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
264− doc.insert(doc.len_chars(), line_ending.as_str());
438+ rope.insert(rope.len_chars(), line_ending.as_str());
265 }
439 }
266
440
267− let mut doc = Self::new(doc);
441+ let mut doc = Self::from(rope, Some(encoding));
442+
268 // set the path and try detecting the language
443 // set the path and try detecting the language
269 doc.set_path(&path)?;
444 doc.set_path(&path)?;
270 doc.detect_indent_style();
445 doc.detect_indent_style();
@@ -303,6 +478,8 @@ impl Document {
303
478
304 // TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
479 // TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
305 // or is that handled by the OS/async layer
480 // or is that handled by the OS/async layer
481+ /// The `Document`'s text is encoded according to its encoding and written to the file located
482+ /// at its `path()`.
306 pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
483 pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
307 // we clone and move text + path into the future so that we asynchronously save the current
484 // we clone and move text + path into the future so that we asynchronously save the current
308 // state without blocking any further edits.
485 // state without blocking any further edits.
@@ -320,8 +497,11 @@ impl Document {
320 self.last_saved_revision = history.current_revision();
497 self.last_saved_revision = history.current_revision();
321 self.history.set(history);
498 self.history.set(history);
322
499
500+ let encoding = self.encoding;
501+
502+ // We encode the file according to the `Document`'s encoding.
323 async move {
503 async move {
324− use tokio::{fs::File, io::AsyncWriteExt};
504+ use tokio::fs::File;
325 if let Some(parent) = path.parent() {
505 if let Some(parent) = path.parent() {
326 // TODO: display a prompt asking the user if the directories should be created
506 // TODO: display a prompt asking the user if the directories should be created
327 if !parent.exists() {
507 if !parent.exists() {
@@ -330,13 +510,9 @@ impl Document {
330 ));
510 ));
331 }
511 }
332 }
512 }
333− let mut file = File::create(path).await?;
334
513
335− // write all the rope chunks to file
514+ let mut file = File::create(path).await?;
336− for chunk in text.chunks() {
515+ to_writer(&mut file, encoding, &text).await?;
337− file.write_all(chunk.as_bytes()).await?;
338− }
339− // TODO: flush?
340
516
341 if let Some(language_server) = language_server {
517 if let Some(language_server) = language_server {
342 language_server
518 language_server
@@ -531,7 +707,7 @@ impl Document {
531 self.selections.insert(view_id, selection);
707 self.selections.insert(view_id, selection);
532 }
708 }
533
709
534− fn _apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
710+ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
535 let old_doc = self.text().clone();
711 let old_doc = self.text().clone();
536
712
537 let success = transaction.changes().apply(&mut self.text);
713 let success = transaction.changes().apply(&mut self.text);
@@ -594,7 +770,7 @@ impl Document {
594 });
770 });
595 }
771 }
596
772
597− let success = self._apply(transaction, view_id);
773+ let success = self.apply_impl(transaction, view_id);
598
774
599 if !transaction.changes().is_empty() {
775 if !transaction.changes().is_empty() {
600 // Compose this transaction with the previous one
776 // Compose this transaction with the previous one
@@ -608,7 +784,7 @@ impl Document {
608 pub fn undo(&mut self, view_id: ViewId) {
784 pub fn undo(&mut self, view_id: ViewId) {
609 let mut history = self.history.take();
785 let mut history = self.history.take();
610 let success = if let Some(transaction) = history.undo() {
786 let success = if let Some(transaction) = history.undo() {
611− self._apply(&transaction, view_id)
787+ self.apply_impl(transaction, view_id)
612 } else {
788 } else {
613 false
789 false
614 };
790 };
@@ -623,7 +799,7 @@ impl Document {
623 pub fn redo(&mut self, view_id: ViewId) {
799 pub fn redo(&mut self, view_id: ViewId) {
624 let mut history = self.history.take();
800 let mut history = self.history.take();
625 let success = if let Some(transaction) = history.redo() {
801 let success = if let Some(transaction) = history.redo() {
626− self._apply(&transaction, view_id)
802+ self.apply_impl(transaction, view_id)
627 } else {
803 } else {
628 false
804 false
629 };
805 };
@@ -638,14 +814,14 @@ impl Document {
638 pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
814 pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
639 let txns = self.history.get_mut().earlier(uk);
815 let txns = self.history.get_mut().earlier(uk);
640 for txn in txns {
816 for txn in txns {
641− self._apply(&txn, view_id);
817+ self.apply_impl(&txn, view_id);
642 }
818 }
643 }
819 }
644
820
645 pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
821 pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
646 let txns = self.history.get_mut().later(uk);
822 let txns = self.history.get_mut().later(uk);
647 for txn in txns {
823 for txn in txns {
648− self._apply(&txn, view_id);
824+ self.apply_impl(&txn, view_id);
649 }
825 }
650 }
826 }
651
827
@@ -670,12 +846,10 @@ impl Document {
670 self.history.set(history);
846 self.history.set(history);
671 }
847 }
672
848
673− #[inline]
674 pub fn id(&self) -> DocumentId {
849 pub fn id(&self) -> DocumentId {
675 self.id
850 self.id
676 }
851 }
677
852
678− #[inline]
679 pub fn is_modified(&self) -> bool {
853 pub fn is_modified(&self) -> bool {
680 let history = self.history.take();
854 let history = self.history.take();
681 let current_revision = history.current_revision();
855 let current_revision = history.current_revision();
@@ -683,12 +857,10 @@ impl Document {
683 current_revision != self.last_saved_revision || !self.changes.is_empty()
857 current_revision != self.last_saved_revision || !self.changes.is_empty()
684 }
858 }
685
859
686− #[inline]
687 pub fn mode(&self) -> Mode {
860 pub fn mode(&self) -> Mode {
688 self.mode
861 self.mode
689 }
862 }
690
863
691− #[inline]
692 /// Corresponding language scope name. Usually `source.<lang>`.
864 /// Corresponding language scope name. Usually `source.<lang>`.
693 pub fn language(&self) -> Option<&str> {
865 pub fn language(&self) -> Option<&str> {
694 self.language
866 self.language
@@ -696,21 +868,21 @@ impl Document {
696 .map(|language| language.scope.as_str())
868 .map(|language| language.scope.as_str())
697 }
869 }
698
870
699− #[inline]
700 pub fn language_config(&self) -> Option<&LanguageConfiguration> {
871 pub fn language_config(&self) -> Option<&LanguageConfiguration> {
701 self.language.as_deref()
872 self.language.as_deref()
702 }
873 }
703
874
704− #[inline]
705 /// Current document version, incremented at each change.
875 /// Current document version, incremented at each change.
706 pub fn version(&self) -> i32 {
876 pub fn version(&self) -> i32 {
707 self.version
877 self.version
708 }
878 }
709
879
880+ #[inline]
710 pub fn language_server(&self) -> Option<&helix_lsp::Client> {
881 pub fn language_server(&self) -> Option<&helix_lsp::Client> {
711 self.language_server.as_deref()
882 self.language_server.as_deref()
712 }
883 }
713
884
885+ #[inline]
714 /// Tree-sitter AST tree
886 /// Tree-sitter AST tree
715 pub fn syntax(&self) -> Option<&Syntax> {
887 pub fn syntax(&self) -> Option<&Syntax> {
716 self.syntax.as_ref()
888 self.syntax.as_ref()
@@ -756,10 +928,12 @@ impl Document {
756 self.path().map(|path| Url::from_file_path(path).unwrap())
928 self.path().map(|path| Url::from_file_path(path).unwrap())
757 }
929 }
758
930
931+ #[inline]
759 pub fn text(&self) -> &Rope {
932 pub fn text(&self) -> &Rope {
760 &self.text
933 &self.text
761 }
934 }
762
935
936+ #[inline]
763 pub fn selection(&self, view_id: ViewId) -> &Selection {
937 pub fn selection(&self, view_id: ViewId) -> &Selection {
764 &self.selections[&view_id]
938 &self.selections[&view_id]
765 }
939 }
@@ -787,6 +961,7 @@ impl Document {
787
961
788 // -- LSP methods
962 // -- LSP methods
789
963
964+ #[inline]
790 pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
965 pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
791 lsp::TextDocumentIdentifier::new(self.url().unwrap())
966 lsp::TextDocumentIdentifier::new(self.url().unwrap())
792 }
967 }
@@ -795,6 +970,7 @@ impl Document {
795 lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
970 lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
796 }
971 }
797
972
973+ #[inline]
798 pub fn diagnostics(&self) -> &[Diagnostic] {
974 pub fn diagnostics(&self) -> &[Diagnostic] {
799 &self.diagnostics
975 &self.diagnostics
800 }
976 }
@@ -804,6 +980,13 @@ impl Document {
804 }
980 }
805}
981}
806
982
983+impl Default for Document {
984+ fn default() -> Self {
985+ let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
986+ Self::from(text, None)
987+ }
988+}
989+
807#[cfg(test)]
990#[cfg(test)]
808mod test {
991mod test {
809 use super::*;
992 use super::*;
@@ -812,7 +995,7 @@ mod test {
812 fn changeset_to_changes() {
995 fn changeset_to_changes() {
813 use helix_lsp::{lsp, Client, OffsetEncoding};
996 use helix_lsp::{lsp, Client, OffsetEncoding};
814 let text = Rope::from("hello");
997 let text = Rope::from("hello");
815− let mut doc = Document::new(text);
998+ let mut doc = Document::from(text, None);
816 let view = ViewId::default();
999 let view = ViewId::default();
817 doc.set_selection(view, Selection::single(5, 5));
1000 doc.set_selection(view, Selection::single(5, 5));
818
1001
@@ -921,4 +1104,94 @@ mod test {
921 ]
1104 ]
922 );
1105 );
923 }
1106 }
1107+
1108+ #[test]
1109+ fn test_line_ending() {
1110+ if cfg!(windows) {
1111+ assert_eq!(Document::default().text().to_string(), "\r\n");
1112+ } else {
1113+ assert_eq!(Document::default().text().to_string(), "\n");
1114+ }
1115+ }
1116+
1117+ macro_rules! test_decode {
1118+ ($label:expr, $label_override:expr) => {
1119+ let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
1120+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
1121+ let path = base_path.join(format!("{}_in.txt", $label));
1122+ let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
1123+ assert!(path.exists());
1124+ assert!(ref_path.exists());
1125+
1126+ let mut file = std::fs::File::open(path).unwrap();
1127+ let text = from_reader(&mut file, Some(encoding))
1128+ .unwrap()
1129+ .0
1130+ .to_string();
1131+ let expectation = std::fs::read_to_string(ref_path).unwrap();
1132+ assert_eq!(text[..], expectation[..]);
1133+ };
1134+ }
1135+
1136+ macro_rules! test_encode {
1137+ ($label:expr, $label_override:expr) => {
1138+ let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
1139+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
1140+ let path = base_path.join(format!("{}_out.txt", $label));
1141+ let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
1142+ assert!(path.exists());
1143+ assert!(ref_path.exists());
1144+
1145+ let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
1146+ let mut buf: Vec<u8> = Vec::new();
1147+ helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
1148+
1149+ let expectation = std::fs::read(ref_path).unwrap();
1150+ assert_eq!(buf, expectation);
1151+ };
1152+ }
1153+
1154+ macro_rules! test_decode_fn {
1155+ ($name:ident, $label:expr, $label_override:expr) => {
1156+ #[test]
1157+ fn $name() {
1158+ test_decode!($label, $label_override);
1159+ }
1160+ };
1161+ ($name:ident, $label:expr) => {
1162+ #[test]
1163+ fn $name() {
1164+ test_decode!($label, $label);
1165+ }
1166+ };
1167+ }
1168+
1169+ macro_rules! test_encode_fn {
1170+ ($name:ident, $label:expr, $label_override:expr) => {
1171+ #[test]
1172+ fn $name() {
1173+ test_encode!($label, $label_override);
1174+ }
1175+ };
1176+ ($name:ident, $label:expr) => {
1177+ #[test]
1178+ fn $name() {
1179+ test_encode!($label, $label);
1180+ }
1181+ };
1182+ }
1183+
1184+ test_decode_fn!(test_big5_decode, "big5");
1185+ test_encode_fn!(test_big5_encode, "big5");
1186+ test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR");
1187+ test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR");
1188+ test_decode_fn!(test_gb18030_decode, "gb18030");
1189+ test_encode_fn!(test_gb18030_encode, "gb18030");
1190+ test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
1191+ test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
1192+ test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP");
1193+ test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP");
1194+ test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP");
1195+ test_decode_fn!(test_shift_jis_decode, "shift_jis");
1196+ test_encode_fn!(test_shift_jis_encode, "shift_jis");
924}
1197}
helix-view/src/editor.rs+3−4
@@ -17,7 +17,7 @@ use anyhow::Error;
17
17
18pub use helix_core::diagnostic::Severity;
18pub use helix_core::diagnostic::Severity;
19pub use helix_core::register::Registers;
19pub use helix_core::register::Registers;
20−use helix_core::{Position, DEFAULT_LINE_ENDING};
20+use helix_core::Position;
21
21
22#[derive(Debug)]
22#[derive(Debug)]
23pub struct Editor {
23pub struct Editor {
@@ -171,8 +171,7 @@ impl Editor {
171 }
171 }
172
172
173 pub fn new_file(&mut self, action: Action) -> DocumentId {
173 pub fn new_file(&mut self, action: Action) -> DocumentId {
174− use helix_core::Rope;
174+ let doc = Document::default();
175− let doc = Document::new(Rope::from(DEFAULT_LINE_ENDING.as_str()));
176 let id = self.documents.insert(doc);
175 let id = self.documents.insert(doc);
177 self.documents[id].id = id;
176 self.documents[id].id = id;
178 self.switch(id, action);
177 self.switch(id, action);
@@ -190,7 +189,7 @@ impl Editor {
190 let id = if let Some(id) = id {
189 let id = if let Some(id) = id {
191 id
190 id
192 } else {
191 } else {
193− let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?;
192+ let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
194
193
195 // try to find a language server based on the language name
194 // try to find a language server based on the language name
196 let language_server = doc
195 let language_server = doc