diff --git a/src/engine.rs b/src/engine.rs index 7e61745..3e1ad01 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -88,7 +88,7 @@ impl Engine { return Err(EditError::new("0|0000| must have hash 0000")); } match cmd { - Subcommand::Append(_) | Subcommand::Insert(_) => Ok(()), + Subcommand::Append(_) | Subcommand::Insert(_) | Subcommand::Read { .. } => Ok(()), _ => Err(EditError::new("0|0000| is only valid with i or a")), } } else { @@ -149,6 +149,7 @@ impl Engine { Subcommand::Dedent { levels } => self.dedent_range(start, end, *levels), Subcommand::Sort => self.sort_range(start, end), Subcommand::Print => self.print_range(start, end), + Subcommand::Read { path } => self.read_file(start, end, path), } } @@ -467,6 +468,13 @@ impl Engine { Ok(()) } + fn read_file(&mut self, start: usize, end: usize, path: &str) -> Result<(), EditError> { + let content = std::fs::read_to_string(path) + .map_err(|e| EditError::new(format!("failed to read file {path:?}: {e}")))?; + let lines: Vec = content.lines().map(|l| l.to_string()).collect(); + self.append_after(start, end, &lines) + } + fn global( &mut self, start: usize, diff --git a/src/parse.rs b/src/parse.rs index 6df91fa..2c5d5bb 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -34,6 +34,7 @@ pub enum Subcommand { Dedent { levels: usize }, Sort, Print, + Read { path: String }, } #[derive(Debug, Clone)] @@ -160,10 +161,10 @@ where return Err(EditError::new("0|0000| is not allowed in ranges")); } match cmd { - Subcommand::Append(_) | Subcommand::Insert(_) => {} + Subcommand::Append(_) | Subcommand::Insert(_) | Subcommand::Read { .. } => {} _ => { return Err(EditError::new( - "0|0000| is only allowed with i or a", + "0|0000| is only allowed with i, a, or r", )) } } @@ -253,6 +254,13 @@ where } Ok((Subcommand::Copy { dest }, "")) } + 'r' => { + let path = rest.trim().to_string(); + if path.is_empty() { + return Err(EditError::new("r requires a file path")); + } + Ok((Subcommand::Read { path }, "")) + } 'g' => parse_global(rest, false, read_text), 'v' => parse_global(rest, true, read_text), '>' => { diff --git a/tests/test_exhash.py b/tests/test_exhash.py index 35310ac..dc8efb1 100644 --- a/tests/test_exhash.py +++ b/tests/test_exhash.py @@ -159,3 +159,32 @@ def test_exhash_accepts_tuple_cmds(): a1, a2 = lnhash(1, "a"), lnhash(2, "b") res = exhash(text, (f"{a1}s/a/A/", f"{a2}s/b/B/")) assert res["lines"] == ["A", "B"] + +def test_exhash_read_file(tmp_path): + src = tmp_path / "src.txt" + src.write_text("x\ny") + text = "a\nb\nc\n" + addr = lnhash(2, "b") + res = exhash(text, [f"{addr}r {src}"]) + assert res["lines"] == ["a", "b", "x", "y", "c"] + assert res["modified"] == [3, 4] + +def test_exhash_read_file_at_zero(tmp_path): + src = tmp_path / "src.txt" + src.write_text("x\ny") + text = "a\nb\n" + res = exhash(text, ["0|0000|r " + str(src)]) + assert res["lines"] == ["x", "y", "a", "b"] + +def test_exhash_read_file_not_found(): + text = "a\nb\n" + addr = lnhash(1, "a") + with pytest.raises(ValueError, match="failed to read"): exhash(text, [f"{addr}r /tmp/nonexistent_exhash_test.txt"]) + +def test_exhash_read_file_with_other_cmds(tmp_path): + src = tmp_path / "src.txt" + src.write_text("new line") + text = "foo\nbar\nbaz\n" + a1, a2 = lnhash(1, "foo"), lnhash(3, "baz") + res = exhash(text, [f"{a2}r {src}", f"{a1}s/foo/FOO/"]) + assert res["lines"] == ["FOO", "bar", "baz", "new line"] \ No newline at end of file