Skip to content

Commit 781c38e

Browse files
committed
fix(write): do not set new path on document until write succeeds
If a document is written with a new path, currently, in the event that the write fails, the document still gets its path changed. This fixes it so that the path is not updated unless the write succeeds.
1 parent 318c5a7 commit 781c38e

File tree

4 files changed

+108
-29
lines changed

4 files changed

+108
-29
lines changed

helix-term/src/application.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use arc_swap::{access::Map, ArcSwap};
22
use futures_util::Stream;
33
use helix_core::{
44
config::{default_syntax_loader, user_syntax_loader},
5+
path::get_relative_path,
56
pos_at_coords, syntax, Selection,
67
};
78
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
@@ -452,17 +453,26 @@ impl Application {
452453
);
453454

454455
doc.set_last_saved_revision(doc_save_event.revision);
456+
455457
let lines = doc.text().len_lines();
456458
let bytes = doc.text().len_bytes();
457459

458-
let path_str = doc
459-
.path()
460-
.expect("document written without path")
461-
.to_string_lossy()
462-
.into_owned();
463-
464-
self.editor
465-
.set_status(format!("'{}' written, {}L {}B", path_str, lines, bytes));
460+
if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
461+
log::error!(
462+
"error setting path for doc '{:?}': {}",
463+
doc.path(),
464+
err.to_string(),
465+
);
466+
self.editor.set_error(err.to_string());
467+
} else {
468+
// TODO: fix being overwritten by lsp
469+
self.editor.set_status(format!(
470+
"'{}' written, {}L {}B",
471+
get_relative_path(&doc_save_event.path).to_string_lossy(),
472+
lines,
473+
bytes
474+
));
475+
}
466476
}
467477

468478
pub fn handle_terminal_events(&mut self, event: Result<Event, crossterm::ErrorKind>) {

helix-term/src/commands/typed.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,6 @@ fn write_impl(
199199
let jobs = &mut cx.jobs;
200200
let doc = doc_mut!(cx.editor);
201201

202-
if let Some(ref path) = path {
203-
doc.set_path(Some(path.as_ref().as_ref()))
204-
.context("invalid filepath")?;
205-
}
206-
207202
if doc.path().is_none() {
208203
bail!("cannot write a buffer without a filename");
209204
}
@@ -220,7 +215,7 @@ fn write_impl(
220215
shared
221216
});
222217

223-
doc.format_and_save(fmt, force)?;
218+
doc.format_and_save(fmt, path.map(AsRef::as_ref), force)?;
224219

225220
if path.is_some() {
226221
let id = doc.id();
@@ -483,7 +478,7 @@ fn write_all_impl(
483478
shared
484479
});
485480

486-
doc.format_and_save(fmt, force)?;
481+
doc.format_and_save::<_, PathBuf>(fmt, None, force)?;
487482
}
488483

489484
if quit {

helix-term/tests/test/write.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,57 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
117117
}
118118

119119
#[tokio::test]
120-
#[ignore]
120+
async fn test_write_new_path() -> anyhow::Result<()> {
121+
let mut file1 = tempfile::NamedTempFile::new().unwrap();
122+
let mut file2 = tempfile::NamedTempFile::new().unwrap();
123+
124+
test_key_sequences(
125+
&mut Application::new(
126+
Args {
127+
files: vec![(file1.path().to_path_buf(), Position::default())],
128+
..Default::default()
129+
},
130+
Config::default(),
131+
)?,
132+
vec![
133+
(
134+
Some("ii can eat glass, it will not hurt me<ret><esc>:w<ret>"),
135+
Some(&|app| {
136+
let doc = doc!(app.editor);
137+
assert!(!app.editor.is_err());
138+
assert_eq!(file1.path(), doc.path().unwrap());
139+
}),
140+
),
141+
(
142+
Some(&format!(":w {}<ret>", file2.path().to_string_lossy())),
143+
Some(&|app| {
144+
let doc = doc!(app.editor);
145+
assert!(!app.editor.is_err());
146+
assert_eq!(file2.path(), doc.path().unwrap());
147+
assert!(app.editor.document_by_path(file1.path()).is_none());
148+
}),
149+
),
150+
],
151+
)
152+
.await?;
153+
154+
file1.as_file_mut().flush()?;
155+
file1.as_file_mut().sync_all()?;
156+
file2.as_file_mut().flush()?;
157+
file2.as_file_mut().sync_all()?;
158+
159+
let mut file1_content = String::new();
160+
file1.as_file_mut().read_to_string(&mut file1_content)?;
161+
assert_eq!("i can eat glass, it will not hurt me\n", file1_content);
162+
163+
let mut file2_content = String::new();
164+
file2.as_file_mut().read_to_string(&mut file2_content)?;
165+
assert_eq!("i can eat glass, it will not hurt me\n", file2_content);
166+
167+
Ok(())
168+
}
169+
170+
#[tokio::test]
121171
async fn test_write_fail_new_path() -> anyhow::Result<()> {
122172
let file = helpers::new_readonly_tempfile()?;
123173

helix-view/src/document.rs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ impl Serialize for Mode {
9090
pub struct DocumentSaveEvent {
9191
pub revision: usize,
9292
pub doc_id: DocumentId,
93+
pub path: PathBuf,
9394
}
9495

9596
pub type DocumentSaveEventResult = Result<DocumentSaveEvent, anyhow::Error>;
@@ -456,39 +457,61 @@ impl Document {
456457
Some(fut)
457458
}
458459

459-
pub fn save(&mut self, force: bool) -> Result<(), anyhow::Error> {
460-
self.save_impl::<futures_util::future::Ready<_>>(None, force)
460+
pub fn save<P: Into<PathBuf>>(
461+
&mut self,
462+
path: Option<P>,
463+
force: bool,
464+
) -> Result<(), anyhow::Error> {
465+
self.save_impl::<futures_util::future::Ready<_>, _>(None, path, force)
461466
}
462467

463-
pub fn format_and_save(
468+
pub fn format_and_save<F, P>(
464469
&mut self,
465-
formatting: Option<impl Future<Output = LspFormatting> + 'static + Send>,
470+
formatting: Option<F>,
471+
path: Option<P>,
466472
force: bool,
467-
) -> anyhow::Result<()> {
468-
self.save_impl(formatting, force)
473+
) -> anyhow::Result<()>
474+
where
475+
F: Future<Output = LspFormatting> + 'static + Send,
476+
P: Into<PathBuf>,
477+
{
478+
self.save_impl(formatting, path, force)
469479
}
470480

471-
// TODO: impl Drop to handle ensuring writes when closed
472481
/// The `Document`'s text is encoded according to its encoding and written to the file located
473482
/// at its `path()`.
474483
///
475484
/// If `formatting` is present, it supplies some changes that we apply to the text before saving.
476-
fn save_impl<F: Future<Output = LspFormatting> + 'static + Send>(
485+
fn save_impl<F, P>(
477486
&mut self,
478487
formatting: Option<F>,
488+
path: Option<P>,
479489
force: bool,
480-
) -> Result<(), anyhow::Error> {
490+
) -> Result<(), anyhow::Error>
491+
where
492+
F: Future<Output = LspFormatting> + 'static + Send,
493+
P: Into<PathBuf>,
494+
{
481495
if self.save_sender.is_none() {
482496
bail!("saves are closed for this document!");
483497
}
484498

485499
// we clone and move text + path into the future so that we asynchronously save the current
486500
// state without blocking any further edits.
487-
488501
let mut text = self.text().clone();
489-
let path = self.path.clone().expect("Can't save with no path set!");
490-
let identifier = self.identifier();
491502

503+
let path = match path {
504+
Some(path) => helix_core::path::get_canonicalized_path(&path.into())?,
505+
None => {
506+
if self.path.is_none() {
507+
bail!("Can't save with no path set!");
508+
}
509+
510+
self.path.as_ref().unwrap().clone()
511+
}
512+
};
513+
514+
let identifier = self.identifier();
492515
let language_server = self.language_server.clone();
493516

494517
// mark changes up to now as saved
@@ -520,12 +543,13 @@ impl Document {
520543
}
521544
}
522545

523-
let mut file = File::create(path).await?;
546+
let mut file = File::create(&path).await?;
524547
to_writer(&mut file, encoding, &text).await?;
525548

526549
let event = DocumentSaveEvent {
527550
revision: current_rev,
528551
doc_id,
552+
path,
529553
};
530554

531555
if let Some(language_server) = language_server {

0 commit comments

Comments
 (0)