After getting the user's input, we need to figure out a filename to write the file out as. We can either use the title the user passed in, or we can try to use heuristics to find a title from the markdown content.
We can combine the user's title from the cli
arguments (an Option
) with the logic we
use as a heuristic for the markdown (also
an Option
).
// use `title` if the user passed it in,
// otherwise try to find a heading in the markdown
let document_title = title.or_else(|| {
contents
.lines()
.find(|v| v.starts_with("# "))
// markdown headings are required to have `# ` with
// at least one space
.map(|maybe_line| maybe_line.trim_start_matches("# ").to_string())
});
option.or_else()
returns the original
option if it is a Some
value, or executes
the function to return a different Option
if not. So document_title
is an
Option<String>
either way and we've
covered all of the possible scenarios:
Which leaves us to our markdown heuristic.
Markdown headings are required to have #
with at least one space, so we can turn
contents
into an iterator and find
the
first line that starts with #
.
If we find one, we want to trim #
off of
the heading to get just the heading content,
so we can use map
to operate on the value
inside of the Option
returned by find
if
it exists.
contents
.lines()
.find(|v| v.starts_with("# "))
.map(|maybe_line| maybe_line.trim_start_matches("# ").to_string())
Now this code works fine, but it exposes a bug in our file handling.
If we write_all
to the file
, that moves the
internal cursor to the end of that content. So
when we run our code now, contents
is
skipping the first #
bytes, which means our
heuristic will only find the second heading in
the file.
To fix this, we can bring in the std::io::Seek
trait, and seek to the beginning of the file.
let mut contents = String::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_string(&mut contents)?;
I have problems getting this to work :( If I run the following code:
use color_eyre::{eyre::WrapErr, Result};
use edit::{edit_file, Builder};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
const TEMPLATE: &[u8; 2] = b"# ";
pub fn write(garden_path: PathBuf, title: Option<String>) -> Result<()> {
let (mut file, filepath) = Builder::new()
.suffix(".md")
.rand_bytes(5)
.tempfile_in(&garden_path)
.wrap_err("Failed to create wip file")?
.keep()
.wrap_err("Failed to keep tempfile")?;
file.write_all(TEMPLATE)?;
// let the user write whatever they want in their favorite editor
// before returning to the cli and finishing up
edit_file(filepath)?;
// Read the user's changes back from the file into a string
let mut contents = String::new();
file.seek(SeekFrom::Start(0))?;
file.read_to_string(&mut contents)?;
dbg!(contents, title);
todo!()
}
The contents is always "# "
. I already added the seek call to the code above. Can you tell me what the mistake is?
yeah! it's most likely the fact that the editor you're using is actually moving the file and creating a totally new file when you save. vim is known to do this in some cases, for example, with it's filename~
style files.
You can check this by using dbg! on the file
before and after the user edits it. If that's the case for you, you can use fs::read instead of using the file returned from the builder.
Thanks, Chris!