exhash 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {exhash-0.2.2 → exhash-0.2.3}/Cargo.lock +1 -1
- {exhash-0.2.2 → exhash-0.2.3}/Cargo.toml +1 -1
- {exhash-0.2.2 → exhash-0.2.3}/DEV.md +1 -0
- {exhash-0.2.2 → exhash-0.2.3}/PKG-INFO +8 -2
- {exhash-0.2.2 → exhash-0.2.3}/README.md +7 -1
- {exhash-0.2.2 → exhash-0.2.3}/pyproject.toml +1 -1
- {exhash-0.2.2 → exhash-0.2.3}/python/exhash/__init__.py +6 -4
- {exhash-0.2.2 → exhash-0.2.3}/src/bin/exhash.rs +22 -6
- {exhash-0.2.2 → exhash-0.2.3}/src/engine.rs +52 -15
- {exhash-0.2.2 → exhash-0.2.3}/src/lib.rs +1 -1
- {exhash-0.2.2 → exhash-0.2.3}/src/python.rs +3 -3
- {exhash-0.2.2 → exhash-0.2.3}/tests/cli.rs +25 -0
- {exhash-0.2.2 → exhash-0.2.3}/tests/test_exhash.py +7 -0
- {exhash-0.2.2 → exhash-0.2.3}/.github/workflows/ci.yml +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/.gitignore +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/_config.yml +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/_layouts/default.html +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/python/exhash.data/scripts/.gitkeep +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/src/bin/lnhashview.rs +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/src/lnhash.rs +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/src/parse.rs +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/tools/build.sh +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/tools/bump.sh +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/tools/bump2.sh +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/tools/release.sh +0 -0
- {exhash-0.2.2 → exhash-0.2.3}/tools/test.sh +0 -0
|
@@ -49,6 +49,7 @@ cargo test && pytest -q
|
|
|
49
49
|
|
|
50
50
|
`edit_text` verifies lnhashes command-by-command against the current in-memory buffer, immediately before each command executes (not all upfront). If an earlier command shifts or rewrites a later target line, that later command will fail with a stale-hash error unless you recompute addresses.
|
|
51
51
|
The `$` (last line) and `%` (whole file) address forms are resolved against the current buffer and do not require hashes.
|
|
52
|
+
`edit_text_with_sw` exposes configurable shift width for `<` and `>`; `edit_text` defaults to `sw=4`.
|
|
52
53
|
|
|
53
54
|
## Release
|
|
54
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exhash
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Summary: Verified line-addressed file editor using lnhash addresses
|
|
@@ -71,6 +71,9 @@ EOF
|
|
|
71
71
|
# Dry-run
|
|
72
72
|
exhash --dry-run file.txt '12|abcd|d'
|
|
73
73
|
|
|
74
|
+
# Set shift width for < and >
|
|
75
|
+
exhash --sw 2 file.txt '12|abcd|>1'
|
|
76
|
+
|
|
74
77
|
# Last line and whole file shorthands (no hash)
|
|
75
78
|
exhash file.txt '$d'
|
|
76
79
|
exhash file.txt '%j'
|
|
@@ -117,7 +120,7 @@ view = lnhashview(text) # ["1|a1b2| foo", "2|c3d4| bar"]
|
|
|
117
120
|
|
|
118
121
|
### Editing
|
|
119
122
|
|
|
120
|
-
`exhash(text, cmds)` takes the text and a required iterable of command strings (use `[]` for no-op). For `a`/`i`/`c` commands, lines after the command are the text block (no `.` terminator needed):
|
|
123
|
+
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For `a`/`i`/`c` commands, lines after the command are the text block (no `.` terminator needed):
|
|
121
124
|
|
|
122
125
|
```py
|
|
123
126
|
addr = lnhash(1, "foo") # "1|a1b2|"
|
|
@@ -134,6 +137,9 @@ res = exhash(text, [f"{a1}s/foo/FOO/", f"{a2}s/bar/BAR/"])
|
|
|
134
137
|
|
|
135
138
|
# Append multiline text (no dot terminator)
|
|
136
139
|
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])
|
|
140
|
+
|
|
141
|
+
# Change shift width for < and >
|
|
142
|
+
res = exhash(text, [f"{addr}>1"], sw=2)
|
|
137
143
|
```
|
|
138
144
|
|
|
139
145
|
### Result dict
|
|
@@ -56,6 +56,9 @@ EOF
|
|
|
56
56
|
# Dry-run
|
|
57
57
|
exhash --dry-run file.txt '12|abcd|d'
|
|
58
58
|
|
|
59
|
+
# Set shift width for < and >
|
|
60
|
+
exhash --sw 2 file.txt '12|abcd|>1'
|
|
61
|
+
|
|
59
62
|
# Last line and whole file shorthands (no hash)
|
|
60
63
|
exhash file.txt '$d'
|
|
61
64
|
exhash file.txt '%j'
|
|
@@ -102,7 +105,7 @@ view = lnhashview(text) # ["1|a1b2| foo", "2|c3d4| bar"]
|
|
|
102
105
|
|
|
103
106
|
### Editing
|
|
104
107
|
|
|
105
|
-
`exhash(text, cmds)` takes the text and a required iterable of command strings (use `[]` for no-op). For `a`/`i`/`c` commands, lines after the command are the text block (no `.` terminator needed):
|
|
108
|
+
`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `<` and `>` shift. For `a`/`i`/`c` commands, lines after the command are the text block (no `.` terminator needed):
|
|
106
109
|
|
|
107
110
|
```py
|
|
108
111
|
addr = lnhash(1, "foo") # "1|a1b2|"
|
|
@@ -119,6 +122,9 @@ res = exhash(text, [f"{a1}s/foo/FOO/", f"{a2}s/bar/BAR/"])
|
|
|
119
122
|
|
|
120
123
|
# Append multiline text (no dot terminator)
|
|
121
124
|
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])
|
|
125
|
+
|
|
126
|
+
# Change shift width for < and >
|
|
127
|
+
res = exhash(text, [f"{addr}>1"], sw=2)
|
|
122
128
|
```
|
|
123
129
|
|
|
124
130
|
### Result dict
|
|
@@ -28,7 +28,7 @@ def exhash_result(results:list[dict]) -> str:
|
|
|
28
28
|
return '\n'.join(out)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def exhash(text:str, cmds:list[str]) -> dict:
|
|
31
|
+
def exhash(text:str, cmds:list[str], sw:int=4) -> dict:
|
|
32
32
|
"""Verified line-addressed editor. Apply commands to `text`, return a result dict.
|
|
33
33
|
|
|
34
34
|
Commands primarily use lnhash addresses: ``lineno|hash|cmd`` where hash is
|
|
@@ -55,13 +55,15 @@ def exhash(text:str, cmds:list[str]) -> dict:
|
|
|
55
55
|
j Join with next line; with range, joins all
|
|
56
56
|
m dest Move line(s) after dest address
|
|
57
57
|
t dest Copy line(s) after dest address
|
|
58
|
-
>[n] Indent n levels (default 1,
|
|
59
|
-
<[n] Dedent n levels (default 1)
|
|
58
|
+
>[n] Indent n levels (default 1, `sw` spaces each)
|
|
59
|
+
<[n] Dedent n levels (default 1, `sw` spaces each)
|
|
60
60
|
sort Sort lines alphabetically
|
|
61
61
|
p Print (include in output without changing)
|
|
62
62
|
g/pat/cmd Global: run cmd on matching lines
|
|
63
63
|
g!/pat/cmd Inverted global (also v/pat/cmd)
|
|
64
64
|
|
|
65
|
+
`sw` controls shift width for `<` and `>` and defaults to 4.
|
|
66
|
+
|
|
65
67
|
For a/i/c, remaining lines in the command string are the text block
|
|
66
68
|
(no '.' terminator needed, unlike the CLI).
|
|
67
69
|
|
|
@@ -84,5 +86,5 @@ def exhash(text:str, cmds:list[str]) -> dict:
|
|
|
84
86
|
"\\n".join(res["lines"]) # "baz\\nbar"
|
|
85
87
|
res = exhash(text, [f"{addr}a\\nnew line 1\\nnew line 2"])
|
|
86
88
|
"""
|
|
87
|
-
r = _exhash(text, *cmds)
|
|
89
|
+
r = _exhash(text, *cmds, sw=sw)
|
|
88
90
|
return dict(lines=r.lines, hashes=r.hashes, modified=r.modified, deleted=r.deleted)
|
|
@@ -4,11 +4,11 @@ use std::io::{self, Read};
|
|
|
4
4
|
use std::path::{Path, PathBuf};
|
|
5
5
|
use std::process;
|
|
6
6
|
|
|
7
|
-
use exhash::{
|
|
7
|
+
use exhash::{edit_text_with_sw, parse_commands_from_args};
|
|
8
8
|
|
|
9
9
|
fn usage() {
|
|
10
10
|
eprintln!("\
|
|
11
|
-
Usage: exhash [-h] [--dry-run] [--stdin] <file|-> [commands...]
|
|
11
|
+
Usage: exhash [-h] [--dry-run] [--stdin] [--sw N] <file|-> [commands...]
|
|
12
12
|
|
|
13
13
|
Verified line-addressed file editor using lnhash addresses.
|
|
14
14
|
|
|
@@ -36,8 +36,8 @@ COMMANDS
|
|
|
36
36
|
j Join with next line; with range, joins all lines in range
|
|
37
37
|
m dest Move line(s) after dest address
|
|
38
38
|
t dest Copy line(s) after dest address
|
|
39
|
-
>[n] Indent n levels (default 1,
|
|
40
|
-
<[n] Dedent n levels (default 1)
|
|
39
|
+
>[n] Indent n levels (default 1, --sw spaces each)
|
|
40
|
+
<[n] Dedent n levels (default 1, --sw spaces each)
|
|
41
41
|
sort Sort lines alphabetically
|
|
42
42
|
p Print (include lines in output without changing them)
|
|
43
43
|
g/pat/cmd Global: run cmd on matching lines
|
|
@@ -50,6 +50,7 @@ TEXT BLOCKS (a/i/c)
|
|
|
50
50
|
|
|
51
51
|
OPTIONS
|
|
52
52
|
--dry-run Don't write; show what would change on stdout
|
|
53
|
+
--sw N Shift width for < and > (default 4)
|
|
53
54
|
--stdin Read input from stdin (file arg must be '-');
|
|
54
55
|
outputs full file in lnhash format.
|
|
55
56
|
Text blocks (a/i/c) not supported in this mode.
|
|
@@ -125,6 +126,7 @@ fn main() {
|
|
|
125
126
|
|
|
126
127
|
let mut dry_run = false;
|
|
127
128
|
let mut stdin_mode = false;
|
|
129
|
+
let mut sw = 4usize;
|
|
128
130
|
|
|
129
131
|
let mut idx = 1;
|
|
130
132
|
while idx < args.len() {
|
|
@@ -137,6 +139,20 @@ fn main() {
|
|
|
137
139
|
stdin_mode = true;
|
|
138
140
|
idx += 1;
|
|
139
141
|
}
|
|
142
|
+
"--sw" => {
|
|
143
|
+
let Some(val) = args.get(idx + 1) else {
|
|
144
|
+
eprintln!("error: --sw requires an integer argument");
|
|
145
|
+
process::exit(2);
|
|
146
|
+
};
|
|
147
|
+
sw = match val.parse::<usize>() {
|
|
148
|
+
Ok(n) => n,
|
|
149
|
+
Err(_) => {
|
|
150
|
+
eprintln!("error: invalid --sw value {val:?}");
|
|
151
|
+
process::exit(2);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
idx += 2;
|
|
155
|
+
}
|
|
140
156
|
"--help" | "-h" => {
|
|
141
157
|
usage();
|
|
142
158
|
return;
|
|
@@ -184,7 +200,7 @@ fn main() {
|
|
|
184
200
|
}
|
|
185
201
|
};
|
|
186
202
|
|
|
187
|
-
let result = match
|
|
203
|
+
let result = match edit_text_with_sw(&input, &commands, sw) {
|
|
188
204
|
Ok(r) => r,
|
|
189
205
|
Err(e) => {
|
|
190
206
|
eprintln!("error: {e}");
|
|
@@ -230,7 +246,7 @@ fn main() {
|
|
|
230
246
|
}
|
|
231
247
|
};
|
|
232
248
|
|
|
233
|
-
let result = match
|
|
249
|
+
let result = match edit_text_with_sw(&text, &commands, sw) {
|
|
234
250
|
Ok(r) => r,
|
|
235
251
|
Err(e) => {
|
|
236
252
|
eprintln!("error: {e}");
|
|
@@ -30,10 +30,11 @@ struct Line {
|
|
|
30
30
|
struct Engine {
|
|
31
31
|
lines: Vec<Line>,
|
|
32
32
|
deleted: BTreeSet<usize>,
|
|
33
|
+
sw: usize,
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
impl Engine {
|
|
36
|
-
fn new(input_lines: Vec<String
|
|
37
|
+
fn new(input_lines: Vec<String>, sw: usize) -> Self {
|
|
37
38
|
let lines = input_lines
|
|
38
39
|
.into_iter()
|
|
39
40
|
.enumerate()
|
|
@@ -47,6 +48,7 @@ impl Engine {
|
|
|
47
48
|
Self {
|
|
48
49
|
lines,
|
|
49
50
|
deleted: BTreeSet::new(),
|
|
51
|
+
sw,
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -490,10 +492,10 @@ impl Engine {
|
|
|
490
492
|
|
|
491
493
|
fn indent_range(&mut self, start: usize, end: usize, levels: usize) -> Result<(), EditError> {
|
|
492
494
|
let (s, e) = self.resolve_range(start, end)?;
|
|
493
|
-
if levels == 0 {
|
|
495
|
+
if levels == 0 || self.sw == 0 {
|
|
494
496
|
return Ok(());
|
|
495
497
|
}
|
|
496
|
-
let prefix = "
|
|
498
|
+
let prefix = " ".repeat(self.sw * levels);
|
|
497
499
|
for idx in s..=e {
|
|
498
500
|
let new = format!("{}{}", prefix, self.lines[idx].text);
|
|
499
501
|
self.lines[idx].text = new;
|
|
@@ -504,12 +506,12 @@ impl Engine {
|
|
|
504
506
|
|
|
505
507
|
fn dedent_range(&mut self, start: usize, end: usize, levels: usize) -> Result<(), EditError> {
|
|
506
508
|
let (s, e) = self.resolve_range(start, end)?;
|
|
507
|
-
if levels == 0 {
|
|
509
|
+
if levels == 0 || self.sw == 0 {
|
|
508
510
|
return Ok(());
|
|
509
511
|
}
|
|
510
512
|
for idx in s..=e {
|
|
511
513
|
let old = self.lines[idx].text.clone();
|
|
512
|
-
let new = dedent(&old, levels);
|
|
514
|
+
let new = dedent(&old, levels, self.sw);
|
|
513
515
|
if new != old {
|
|
514
516
|
self.lines[idx].text = new;
|
|
515
517
|
self.lines[idx].modified = true;
|
|
@@ -590,9 +592,13 @@ impl Engine {
|
|
|
590
592
|
/// Each command's lnhashes are verified against the current text immediately before that
|
|
591
593
|
/// command is applied.
|
|
592
594
|
pub fn edit_text(input: &str, commands: &[Command]) -> Result<EditResult, EditError> {
|
|
595
|
+
edit_text_with_sw(input, commands, 4)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
pub fn edit_text_with_sw(input: &str, commands: &[Command], sw: usize) -> Result<EditResult, EditError> {
|
|
593
599
|
let input_lines: Vec<String> = input.lines().map(|l| l.to_string()).collect();
|
|
594
600
|
|
|
595
|
-
let mut eng = Engine::new(input_lines);
|
|
601
|
+
let mut eng = Engine::new(input_lines, sw);
|
|
596
602
|
for c in commands {
|
|
597
603
|
eng.verify_command(c)?;
|
|
598
604
|
eng.apply_command(c)?;
|
|
@@ -634,6 +640,7 @@ fn build_regex(pattern: &str, case_insensitive: bool) -> Result<Regex, EditError
|
|
|
634
640
|
}
|
|
635
641
|
|
|
636
642
|
fn join_strings(a: &str, b: &str) -> String {
|
|
643
|
+
let b = b.trim_start_matches(char::is_whitespace);
|
|
637
644
|
if a.is_empty() {
|
|
638
645
|
return b.to_string();
|
|
639
646
|
}
|
|
@@ -641,29 +648,27 @@ fn join_strings(a: &str, b: &str) -> String {
|
|
|
641
648
|
return a.to_string();
|
|
642
649
|
}
|
|
643
650
|
let a_end_ws = a.chars().last().map(|c| c.is_whitespace()).unwrap_or(false);
|
|
644
|
-
|
|
645
|
-
if a_end_ws || b_start_ws {
|
|
651
|
+
if a_end_ws {
|
|
646
652
|
format!("{a}{b}")
|
|
647
653
|
} else {
|
|
648
654
|
format!("{a} {b}")
|
|
649
655
|
}
|
|
650
656
|
}
|
|
651
657
|
|
|
652
|
-
fn dedent(line: &str, levels: usize) -> String {
|
|
658
|
+
fn dedent(line: &str, levels: usize, sw: usize) -> String {
|
|
653
659
|
let mut s = line.to_string();
|
|
660
|
+
if sw == 0 {
|
|
661
|
+
return s;
|
|
662
|
+
}
|
|
654
663
|
for _ in 0..levels {
|
|
655
|
-
if s.starts_with(" ") {
|
|
656
|
-
s = s[4..].to_string();
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
664
|
if s.starts_with('\t') {
|
|
660
665
|
s = s[1..].to_string();
|
|
661
666
|
continue;
|
|
662
667
|
}
|
|
663
|
-
// Remove up to
|
|
668
|
+
// Remove up to `sw` leading spaces as one level.
|
|
664
669
|
let mut removed = 0usize;
|
|
665
670
|
let bytes = s.as_bytes();
|
|
666
|
-
while removed <
|
|
671
|
+
while removed < sw && removed < bytes.len() && bytes[removed] == b' ' {
|
|
667
672
|
removed += 1;
|
|
668
673
|
}
|
|
669
674
|
if removed > 0 {
|
|
@@ -755,6 +760,15 @@ mod tests {
|
|
|
755
760
|
assert_eq!(res.deleted, vec![2]);
|
|
756
761
|
}
|
|
757
762
|
|
|
763
|
+
#[test]
|
|
764
|
+
fn join_single_address_collapses_indented_next_line_to_single_space() {
|
|
765
|
+
let input = "hello\n world\n";
|
|
766
|
+
let cmd = format!("{}j", addr(1, "hello"));
|
|
767
|
+
let cmds = parse_commands_from_script(&cmd).unwrap();
|
|
768
|
+
let res = edit_text(input, &cmds).unwrap();
|
|
769
|
+
assert_eq!(res.lines, vec!["hello world".to_string()]);
|
|
770
|
+
}
|
|
771
|
+
|
|
758
772
|
#[test]
|
|
759
773
|
fn percent_address_joins_whole_file() {
|
|
760
774
|
let input = "a\nb\nc\n";
|
|
@@ -854,6 +868,18 @@ mod tests {
|
|
|
854
868
|
assert_eq!(res.modified, vec![1, 2]);
|
|
855
869
|
}
|
|
856
870
|
|
|
871
|
+
#[test]
|
|
872
|
+
fn indent_and_dedent_with_custom_sw() {
|
|
873
|
+
let input = "a\n b\n";
|
|
874
|
+
let cmd1 = format!("{}>2", addr(1, "a"));
|
|
875
|
+
let cmd2 = format!("{}<1", addr(2, " b"));
|
|
876
|
+
let script = format!("{}\n{}\n", cmd1, cmd2);
|
|
877
|
+
let cmds = parse_commands_from_script(&script).unwrap();
|
|
878
|
+
let res = edit_text_with_sw(input, &cmds, 2).unwrap();
|
|
879
|
+
assert_eq!(res.lines, vec![" a".to_string(), "b".to_string()]);
|
|
880
|
+
assert_eq!(res.modified, vec![1, 2]);
|
|
881
|
+
}
|
|
882
|
+
|
|
857
883
|
#[test]
|
|
858
884
|
fn sort_range() {
|
|
859
885
|
let input = "c\na\nb\n";
|
|
@@ -935,6 +961,17 @@ mod tests {
|
|
|
935
961
|
assert_eq!(res.modified, vec![1]);
|
|
936
962
|
}
|
|
937
963
|
|
|
964
|
+
#[test]
|
|
965
|
+
fn join_range_preserves_outer_whitespace_and_trims_middle_line_prefixes() {
|
|
966
|
+
let input = " alpha\n beta \n gamma \n";
|
|
967
|
+
let cmd = format!("{},{}j", addr(1, " alpha"), addr(3, " gamma "));
|
|
968
|
+
let cmds = parse_commands_from_script(&cmd).unwrap();
|
|
969
|
+
let res = edit_text(input, &cmds).unwrap();
|
|
970
|
+
assert_eq!(res.lines, vec![" alpha beta gamma ".to_string()]);
|
|
971
|
+
assert_eq!(res.deleted, vec![2, 3]);
|
|
972
|
+
assert_eq!(res.modified, vec![1]);
|
|
973
|
+
}
|
|
974
|
+
|
|
938
975
|
#[test]
|
|
939
976
|
fn multi_command_rechecks_hashes_after_each_command() {
|
|
940
977
|
let input = "a\nb\nc\n";
|
|
@@ -10,7 +10,7 @@ mod parse;
|
|
|
10
10
|
#[cfg(feature = "pyo3")]
|
|
11
11
|
mod python;
|
|
12
12
|
|
|
13
|
-
pub use engine::{edit_text, EditResult};
|
|
13
|
+
pub use engine::{edit_text, edit_text_with_sw, EditResult};
|
|
14
14
|
pub use lnhash::{format_lnhash, line_hash_u16, parse_lnhash, LnHash};
|
|
15
15
|
pub use parse::{parse_commands_from_args, parse_commands_from_script, parse_commands_from_strs, Address, Command, Subcommand};
|
|
16
16
|
|
|
@@ -36,12 +36,12 @@ fn lnhashview(text: &str) -> Vec<String> {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
#[pyfunction]
|
|
39
|
-
#[pyo3(name = "exhash", signature = (text, *cmds))]
|
|
40
|
-
fn py_exhash(text: &str, cmds: Vec<String
|
|
39
|
+
#[pyo3(name = "exhash", signature = (text, *cmds, sw=4))]
|
|
40
|
+
fn py_exhash(text: &str, cmds: Vec<String>, sw: usize) -> PyResult<EditResultPy> {
|
|
41
41
|
let cmd_refs: Vec<&str> = cmds.iter().map(|s| s.as_str()).collect();
|
|
42
42
|
let parsed = crate::parse_commands_from_strs(&cmd_refs)
|
|
43
43
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
44
|
-
let res = crate::
|
|
44
|
+
let res = crate::edit_text_with_sw(text, &parsed, sw)
|
|
45
45
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
46
46
|
Ok(res.into())
|
|
47
47
|
}
|
|
@@ -129,6 +129,31 @@ fn exhash_dry_run_does_not_write() {
|
|
|
129
129
|
assert_eq!(read_file(&file), "foo\nbar\n");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
#[test]
|
|
133
|
+
fn exhash_custom_sw_option_changes_shift_width() {
|
|
134
|
+
let dir = mk_temp_dir("exhash_custom_sw");
|
|
135
|
+
let file = dir.join("f.txt");
|
|
136
|
+
write_file(&file, "a\n");
|
|
137
|
+
|
|
138
|
+
let a1 = format_lnhash(1, "a");
|
|
139
|
+
let cmd = format!("{a1}>1");
|
|
140
|
+
|
|
141
|
+
let bin = env!("CARGO_BIN_EXE_exhash");
|
|
142
|
+
let out = Command::new(bin)
|
|
143
|
+
.arg("--sw")
|
|
144
|
+
.arg("2")
|
|
145
|
+
.arg(&file)
|
|
146
|
+
.arg(cmd)
|
|
147
|
+
.output()
|
|
148
|
+
.unwrap();
|
|
149
|
+
assert!(out.status.success());
|
|
150
|
+
|
|
151
|
+
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
152
|
+
let expected = format!("{} a\n", format_lnhash(1, " a"));
|
|
153
|
+
assert_eq!(stdout, expected);
|
|
154
|
+
assert_eq!(read_file(&file), " a\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
#[test]
|
|
133
158
|
fn exhash_rejects_stale_lnhash_and_leaves_file_unchanged() {
|
|
134
159
|
let dir = mk_temp_dir("exhash_stale");
|
|
@@ -105,6 +105,13 @@ def test_exhash_move_destination_can_use_last_line():
|
|
|
105
105
|
assert res["lines"] == ["b", "c", "a"]
|
|
106
106
|
assert res["modified"] == [3]
|
|
107
107
|
|
|
108
|
+
def test_exhash_custom_sw():
|
|
109
|
+
text = "a\n"
|
|
110
|
+
a1 = lnhash(1, "a")
|
|
111
|
+
res = exhash(text, [f"{a1}>1"], sw=2)
|
|
112
|
+
assert res["lines"] == [" a"]
|
|
113
|
+
assert res["modified"] == [1]
|
|
114
|
+
|
|
108
115
|
def test_exhash_append():
|
|
109
116
|
text = "a\nb\n"
|
|
110
117
|
addr = lnhash(1, "a")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|