├── .gitignore ├── src ├── tests │ ├── modes │ │ ├── mod.rs │ │ ├── ex.rs │ │ ├── snapshots │ │ │ └── vicut__tests__modes__normal__vimode_normal_structures.snap │ │ ├── insert.rs │ │ ├── visual.rs │ │ └── normal.rs │ ├── files.rs │ ├── snapshots │ │ └── vicut__tests__modes__vimode_normal_cmds.snap │ ├── pattern_match.rs │ ├── wiki_examples.rs │ ├── linebuf.rs │ ├── golden_files │ │ ├── vicut_main_all_comments.rs │ │ └── vicut_main_extracted_impl.rs │ ├── editor.rs │ └── mod.rs ├── modes │ ├── search.rs │ ├── replace.rs │ ├── mod.rs │ ├── insert.rs │ ├── ex.rs │ └── visual.rs ├── keys.rs ├── register.rs ├── reader.rs ├── vic │ └── vic.pest └── vicmd.rs ├── Cargo.toml ├── LICENSE.md ├── .github └── workflows │ └── release.yaml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *perf* 3 | flamegraph.svg 4 | collapsed* 5 | *.vic 6 | IDEAS.md 7 | -------------------------------------------------------------------------------- /src/tests/modes/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::modes::{insert::ViInsert, normal::ViNormal, ViMode}; 2 | 3 | pub mod normal; 4 | pub mod insert; 5 | pub mod visual; 6 | pub mod ex; 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vicut" 3 | version = "0.4.2" 4 | edition = "2024" 5 | license = "MIT" 6 | repository = "https://github.com/km-clay/vicut" 7 | description = "A CLI text processor that uses Vim commands to transform text and extract fields" 8 | keywords = ["cli", "vim", "fields", "text", "slice"] 9 | categories = ["command-line-utilities", "text-editors", "text-processing", "value-formatting"] 10 | 11 | [dependencies] 12 | bitflags = "2.9.1" 13 | env_logger = "0.11.8" 14 | glob = "0.3.2" 15 | itertools = "0.14.0" 16 | log = "0.4.27" 17 | pest = "2.8.1" 18 | pest_derive = "2.8.1" 19 | rayon = "1.10.0" 20 | regex = "1.11.1" 21 | serde = { version = "1.0.219", features = ["derive"] } 22 | serde_json = "1.0.140" 23 | unicode-segmentation = "1.12.0" 24 | unicode-width = "0.2.0" 25 | 26 | [target.'cfg(target_os = "linux")'.dependencies] 27 | tikv-jemallocator = "0.5.4" 28 | 29 | [dev-dependencies] 30 | tempfile = "3.20.0" 31 | insta = "1.42.2" 32 | pretty_assertions = "1.4.1" 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Kyler Clay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/tests/files.rs: -------------------------------------------------------------------------------- 1 | use super::vicut_integration; 2 | 3 | const VICUT_MAIN: &str = include_str!("golden_files/vicut_main.rs"); 4 | /// `vicut -m ":%s/Opts/Arguments/g"` 5 | const MAIN_ARGV_REPLACED: &str = include_str!("golden_files/vicut_main_argv_replaced.rs"); 6 | /// `vicut -m "/impl Opts" -c "V$%"` 7 | const MAIN_EXTRACTED_IMPL: &str = include_str!("golden_files/vicut_main_extracted_impl.rs"); 8 | /// `vicut -g "//\s[^/].*" -m "f/" -c "$" -n vicut_main.rs` 9 | const MAIN_ALL_COMMENTS: &str = include_str!("golden_files/vicut_main_all_comments.rs"); 10 | 11 | #[test] 12 | #[ignore] 13 | fn file_replace_argv() { 14 | vicut_integration( 15 | VICUT_MAIN, 16 | &["-m", ":%s/Opts/Arguments/g"], 17 | MAIN_ARGV_REPLACED.trim_end() 18 | ); 19 | } 20 | 21 | #[test] 22 | #[ignore] 23 | fn file_extract_impl() { 24 | vicut_integration( 25 | VICUT_MAIN, 26 | &["-m", "/impl Opts", "-c", "V$%"], 27 | MAIN_EXTRACTED_IMPL.trim_end(), 28 | ); 29 | } 30 | 31 | #[test] 32 | #[ignore] 33 | fn file_all_comments() { 34 | vicut_integration( 35 | VICUT_MAIN, 36 | &["-g", "//\\s[^/].*", 37 | "-m", "f/", 38 | "-c", "$", 39 | "-n", 40 | "--end", 41 | ], 42 | MAIN_ALL_COMMENTS.trim_end(), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-and-release: 10 | name: Build and Release 11 | runs-on: ubuntu-latest # GitHub-hosted runner 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup toolchain 18 | run: | 19 | rustup override set default 20 | rustup target add x86_64-unknown-linux-musl 21 | 22 | - name: Run tests 23 | run: cargo test --release 24 | 25 | - name: Install musl tools 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install -y musl-tools 29 | 30 | - name: Build dynamic binary 31 | run: | 32 | cargo build --release 33 | cp target/release/vicut vicut-x86_64-linux-dynamic 34 | 35 | - name: Build static binary 36 | env: 37 | RUSTFLAGS: '-C relocation-model=static' 38 | CC: musl-gcc 39 | run: | 40 | cargo build --release --target x86_64-unknown-linux-musl 41 | cp target/x86_64-unknown-linux-musl/release/vicut vicut-x86_64-linux-static 42 | 43 | - name: Upload Release Artifacts 44 | uses: softprops/action-gh-release@v1 45 | with: 46 | files: | 47 | vicut-x86_64-linux-dynamic 48 | vicut-x86_64-linux-static 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /src/tests/modes/ex.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::vicut_integration; 2 | 3 | 4 | #[test] 5 | fn ex_delete() { 6 | vicut_integration( 7 | "Foo\nBar\nBiz", 8 | &[ 9 | "-m", ":d", 10 | ], 11 | "Bar\nBiz" 12 | ); 13 | } 14 | 15 | #[test] 16 | fn ex_yank() { 17 | vicut_integration( 18 | "\tFoo\nBar\nBiz", 19 | &[ 20 | "-m", ":yjp", 21 | ], 22 | "\tFoo\nBar\n\tFoo\nBiz" 23 | ); 24 | } 25 | 26 | #[test] 27 | fn ex_put() { 28 | vicut_integration( 29 | "Foo\nBar\nBiz", 30 | &[ 31 | "-m", ":1y:2p", 32 | ], 33 | "Foo\nBar\nFoo\nBiz" 34 | ); 35 | vicut_integration( 36 | "Foo\nBar\nBiz", 37 | &[ 38 | "-m", ":d:1,2p", 39 | ], 40 | "Bar\nFoo\nBiz\nFoo" 41 | ); 42 | } 43 | 44 | #[test] 45 | fn ex_substitution() { 46 | vicut_integration( 47 | "Foo\nBar\nBiz\nFoo\nBuzz\nFoo\nBaz", 48 | &[ 49 | "-m", ":%s/Foo/Replaced/g", 50 | ], 51 | "Replaced\nBar\nBiz\nReplaced\nBuzz\nReplaced\nBaz", 52 | ); 53 | } 54 | 55 | #[test] 56 | fn ex_normal() { 57 | vicut_integration( 58 | "Foo\nBar\nBiz\nFoo\nBuzz\nFoo\nBaz", 59 | &[ 60 | "-m", ":/Biz/normal! iNew Text", 61 | ], 62 | "Foo\nBar\nNew TextBiz\nFoo\nBuzz\nFoo\nBaz", 63 | ); 64 | } 65 | 66 | #[test] 67 | fn ex_global_delete() { 68 | vicut_integration( 69 | "Foo\nBar\nBiz\nFoo\nBuzz\nFoo\nBaz", 70 | &[ 71 | "-m", ":g/Foo/d", 72 | ], 73 | "Bar\nBiz\nBuzz\nBaz", 74 | ); 75 | } 76 | 77 | #[test] 78 | fn ex_global_normal() { 79 | vicut_integration( 80 | "Foo\nBar\nBiz\nFoo\nBuzz\nFoo\nBaz", 81 | &[ 82 | "-m", ":g/Foo/normal! iNew Text", 83 | ], 84 | "New TextFoo\nBar\nBiz\nNew TextFoo\nBuzz\nNew TextFoo\nBaz", 85 | ); 86 | } 87 | 88 | #[test] 89 | fn ex_global_normal_nested() { 90 | vicut_integration( 91 | "Foo\nBar\nBiz\nFoo\nBuzz\nFoo\nBaz", 92 | &[ 93 | "-m", ":g/Baz/normal! :g/Bar/normal! :g/Biz/normal! :g/Buzz/normal! :g/Foo/normal! cwWow!", 94 | ], 95 | "Wow!\nBar\nBiz\nWow!\nBuzz\nWow!\nBaz", 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/tests/snapshots/vicut__tests__modes__vimode_normal_cmds.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/modes.rs 3 | expression: cmds 4 | --- 5 | [ 6 | ViCmd { 7 | register: RegisterName { 8 | name: None, 9 | count: 1, 10 | append: false, 11 | }, 12 | verb: Some( 13 | VerbCmd( 14 | 1, 15 | Delete, 16 | ), 17 | ), 18 | motion: Some( 19 | MotionCmd( 20 | 2, 21 | WordMotion( 22 | Start, 23 | Normal, 24 | Forward, 25 | ), 26 | ), 27 | ), 28 | raw_seq: "d2w", 29 | flags: CmdFlags( 30 | 0x0, 31 | ), 32 | }, 33 | ViCmd { 34 | register: RegisterName { 35 | name: None, 36 | count: 1, 37 | append: false, 38 | }, 39 | verb: Some( 40 | VerbCmd( 41 | 1, 42 | Rot13, 43 | ), 44 | ), 45 | motion: Some( 46 | MotionCmd( 47 | 5, 48 | WordMotion( 49 | Start, 50 | Normal, 51 | Backward, 52 | ), 53 | ), 54 | ), 55 | raw_seq: "g?5b", 56 | flags: CmdFlags( 57 | 0x0, 58 | ), 59 | }, 60 | ViCmd { 61 | register: RegisterName { 62 | name: None, 63 | count: 1, 64 | append: false, 65 | }, 66 | verb: Some( 67 | VerbCmd( 68 | 2, 69 | Put( 70 | Before, 71 | ), 72 | ), 73 | ), 74 | motion: None, 75 | raw_seq: "2P", 76 | flags: CmdFlags( 77 | 0x0, 78 | ), 79 | }, 80 | ViCmd { 81 | register: RegisterName { 82 | name: None, 83 | count: 1, 84 | append: false, 85 | }, 86 | verb: Some( 87 | VerbCmd( 88 | 1, 89 | Delete, 90 | ), 91 | ), 92 | motion: Some( 93 | MotionCmd( 94 | 5, 95 | ForwardChar, 96 | ), 97 | ), 98 | raw_seq: "5x", 99 | flags: CmdFlags( 100 | 0x0, 101 | ), 102 | }, 103 | ] 104 | -------------------------------------------------------------------------------- /src/tests/modes/snapshots/vicut__tests__modes__normal__vimode_normal_structures.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/modes/normal.rs 3 | expression: cmds 4 | --- 5 | [ 6 | ViCmd { 7 | register: RegisterName { 8 | name: None, 9 | count: 1, 10 | append: false, 11 | }, 12 | verb: Some( 13 | VerbCmd( 14 | 1, 15 | Delete, 16 | ), 17 | ), 18 | motion: Some( 19 | MotionCmd( 20 | 2, 21 | WordMotion( 22 | Start, 23 | Normal, 24 | Forward, 25 | ), 26 | ), 27 | ), 28 | raw_seq: "d2w", 29 | flags: CmdFlags( 30 | 0x0, 31 | ), 32 | }, 33 | ViCmd { 34 | register: RegisterName { 35 | name: None, 36 | count: 1, 37 | append: false, 38 | }, 39 | verb: Some( 40 | VerbCmd( 41 | 1, 42 | Rot13, 43 | ), 44 | ), 45 | motion: Some( 46 | MotionCmd( 47 | 5, 48 | WordMotion( 49 | Start, 50 | Normal, 51 | Backward, 52 | ), 53 | ), 54 | ), 55 | raw_seq: "g?5b", 56 | flags: CmdFlags( 57 | 0x0, 58 | ), 59 | }, 60 | ViCmd { 61 | register: RegisterName { 62 | name: None, 63 | count: 1, 64 | append: false, 65 | }, 66 | verb: Some( 67 | VerbCmd( 68 | 2, 69 | Put( 70 | Before, 71 | ), 72 | ), 73 | ), 74 | motion: None, 75 | raw_seq: "2P", 76 | flags: CmdFlags( 77 | 0x0, 78 | ), 79 | }, 80 | ViCmd { 81 | register: RegisterName { 82 | name: None, 83 | count: 1, 84 | append: false, 85 | }, 86 | verb: Some( 87 | VerbCmd( 88 | 1, 89 | Delete, 90 | ), 91 | ), 92 | motion: Some( 93 | MotionCmd( 94 | 5, 95 | ForwardChar, 96 | ), 97 | ), 98 | raw_seq: "5x", 99 | flags: CmdFlags( 100 | 0x0, 101 | ), 102 | }, 103 | ] 104 | -------------------------------------------------------------------------------- /src/modes/search.rs: -------------------------------------------------------------------------------- 1 | 2 | use crate::{modes::{common_cmds, ModeReport, ViMode}, vicmd::{CmdFlags, Direction, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}}; 3 | 4 | 5 | pub struct ViSearch { 6 | pending_pattern: String, 7 | count: usize, 8 | next_is_escaped: bool, 9 | direction: Direction 10 | } 11 | 12 | impl ViSearch { 13 | pub fn new(count: usize, direction: Direction, ) -> Self { 14 | Self { 15 | pending_pattern: Default::default(), 16 | count, 17 | next_is_escaped: false, 18 | direction 19 | } 20 | } 21 | } 22 | 23 | impl ViMode for ViSearch { 24 | fn handle_key(&mut self, key: crate::keys::KeyEvent) -> Option { 25 | use crate::keys::{KeyEvent as E, KeyCode as C, ModKeys as M}; 26 | match key { 27 | E(C::Char('\r'), M::NONE) | 28 | E(C::Enter, M::NONE) => { 29 | let start_cmd = if self.direction == Direction::Forward { "/" } else { "?" }; 30 | let raw_seq = format!("{start_cmd}{}",self.pending_pattern.clone()); 31 | let motion = match self.direction { 32 | Direction::Forward => Motion::PatternSearch(std::mem::take(&mut self.pending_pattern)), 33 | Direction::Backward => Motion::PatternSearchRev(std::mem::take(&mut self.pending_pattern)), 34 | }; 35 | Some(ViCmd { 36 | register: RegisterName::default(), 37 | verb: Some(VerbCmd(1, Verb::NormalMode)), 38 | motion: Some(MotionCmd(self.count, motion)), 39 | flags: CmdFlags::empty(), 40 | raw_seq 41 | }) 42 | } 43 | E(C::Esc, M::NONE) => { 44 | Some(ViCmd { 45 | register: RegisterName::default(), 46 | verb: Some(VerbCmd(1, Verb::NormalMode)), 47 | motion: None, 48 | flags: CmdFlags::empty(), 49 | raw_seq: "".into(), 50 | }) 51 | } 52 | E(C::Char(ch), M::NONE) => { 53 | if self.next_is_escaped { 54 | self.pending_pattern.push(ch); 55 | self.next_is_escaped = false; 56 | } else { 57 | match ch { 58 | '\\' => { 59 | self.pending_pattern.push(ch); 60 | self.next_is_escaped = true; 61 | } 62 | _ => self.pending_pattern.push(ch), 63 | } 64 | } 65 | None 66 | } 67 | _ => common_cmds(key) 68 | } 69 | } 70 | 71 | fn is_repeatable(&self) -> bool { 72 | false 73 | } 74 | 75 | fn as_replay(&self) -> Option { 76 | None 77 | } 78 | 79 | fn cursor_style(&self) -> String { 80 | "\x1b[2 q".to_string() 81 | } 82 | 83 | fn pending_seq(&self) -> Option { 84 | Some(self.pending_pattern.clone()) 85 | } 86 | 87 | fn move_cursor_on_undo(&self) -> bool { 88 | false 89 | } 90 | 91 | fn clamp_cursor(&self) -> bool { 92 | true 93 | } 94 | 95 | fn hist_scroll_start_pos(&self) -> Option { 96 | None 97 | } 98 | 99 | fn report_mode(&self) -> super::ModeReport { 100 | ModeReport::Search 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modes/replace.rs: -------------------------------------------------------------------------------- 1 | use crate::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word}; 2 | use crate::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; 3 | 4 | use super::{common_cmds, CmdReplay, ModeReport, ViMode}; 5 | 6 | #[derive(Default,Debug)] 7 | pub struct ViReplace { 8 | cmds: Vec, 9 | pending_cmd: ViCmd, 10 | repeat_count: u16 11 | } 12 | 13 | impl ViReplace { 14 | pub fn new() -> Self { 15 | Self::default() 16 | } 17 | pub fn with_count(mut self, repeat_count: u16) -> Self { 18 | self.repeat_count = repeat_count; 19 | self 20 | } 21 | pub fn register_and_return(&mut self) -> Option { 22 | let mut cmd = self.take_cmd(); 23 | cmd.normalize_counts(); 24 | self.register_cmd(&cmd); 25 | Some(cmd) 26 | } 27 | pub fn register_cmd(&mut self, cmd: &ViCmd) { 28 | self.cmds.push(cmd.clone()) 29 | } 30 | pub fn take_cmd(&mut self) -> ViCmd { 31 | std::mem::take(&mut self.pending_cmd) 32 | } 33 | } 34 | 35 | impl ViMode for ViReplace { 36 | fn handle_key(&mut self, key: E) -> Option { 37 | match key { 38 | E(K::Char(ch), M::NONE) => { 39 | self.pending_cmd.set_verb(VerbCmd(1,Verb::ReplaceChar(ch))); 40 | self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); 41 | self.register_and_return() 42 | } 43 | E(K::Char('W'), M::CTRL) => { 44 | self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); 45 | self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); 46 | self.register_and_return() 47 | } 48 | E(K::Char('H'), M::CTRL) | 49 | E(K::Backspace, M::NONE) => { 50 | self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); 51 | self.register_and_return() 52 | } 53 | 54 | E(K::BackTab, M::NONE) => { 55 | self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward)); 56 | self.register_and_return() 57 | } 58 | 59 | E(K::Char('I'), M::CTRL) | 60 | E(K::Tab, M::NONE) => { 61 | self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); 62 | self.register_and_return() 63 | } 64 | 65 | E(K::Esc, M::NONE) => { 66 | self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode)); 67 | self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); 68 | self.register_and_return() 69 | } 70 | _ => common_cmds(key) 71 | } 72 | } 73 | fn is_repeatable(&self) -> bool { 74 | true 75 | } 76 | fn cursor_style(&self) -> String { 77 | "\x1b[4 q".to_string() 78 | } 79 | fn pending_seq(&self) -> Option { 80 | None 81 | } 82 | fn as_replay(&self) -> Option { 83 | Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) 84 | } 85 | fn move_cursor_on_undo(&self) -> bool { 86 | true 87 | } 88 | fn clamp_cursor(&self) -> bool { 89 | true 90 | } 91 | fn hist_scroll_start_pos(&self) -> Option { 92 | Some(To::End) 93 | } 94 | fn report_mode(&self) -> ModeReport { 95 | ModeReport::Replace 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/tests/modes/insert.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::vicut_integration; 2 | 3 | use super::*; 4 | 5 | #[test] 6 | fn vimode_insert_structures() { 7 | let raw = "abcdefghijklmnopqrstuvwxyz1234567890-=[];'<>/\\x1b"; 8 | let mut mode = ViInsert::new(); 9 | let cmds = mode.cmds_from_raw(raw); 10 | insta::assert_debug_snapshot!(cmds) 11 | } 12 | 13 | #[test] 14 | fn two_inserts() { 15 | vicut_integration( 16 | "foo bar biz", 17 | &[ "-m", "iInserting some text2wiAnd some more here too" ], 18 | 19 | "Inserting some textfoo bar And some more here toobiz", 20 | ); 21 | } 22 | 23 | #[test] 24 | fn ctrl_w() { 25 | vicut_integration( 26 | "foo bar biz", 27 | &[ "-m", "eiInserting_some_text" ], 28 | 29 | "foo bar biz", 30 | ); 31 | } 32 | 33 | #[test] 34 | fn linebreaks() { 35 | // Also tests 'a' at the end of the buffer 36 | vicut_integration( 37 | "foo bar biz", 38 | &[ "-m", "$abar foo biz" ], 39 | "foo bar biz\nbar foo biz", 40 | ); 41 | vicut_integration( 42 | "foo bar biz", 43 | &[ "-m", "$abar foo biz" ], 44 | "foo bar biz\nbar foo biz", 45 | ) 46 | } 47 | 48 | #[test] 49 | fn navigation() { 50 | vicut_integration( 51 | "foo bar biz\nbar foo biz", 52 | &[ 53 | "-m", "j", 54 | "-m", "", 55 | "-c", "e", 56 | ], 57 | "bar", 58 | ) 59 | } 60 | 61 | #[test] 62 | fn backspace_and_delete() { 63 | vicut_integration( 64 | "foo bar biz\nbar foo biz", 65 | &[ 66 | "-m", "jw", 67 | "-m", "i", 68 | "-c", "e" 69 | ], 70 | "biz", 71 | ) 72 | } 73 | 74 | #[test] 75 | fn end_of_line_motion_boundary() { 76 | vicut_integration( 77 | "foo bar", 78 | &[ 79 | "-m", "$", 80 | "-m", "i", 81 | "-c", "b" 82 | ], 83 | "bar" 84 | ); 85 | } 86 | 87 | #[test] 88 | fn prefix_insert() { 89 | vicut_integration( 90 | " foo bar", 91 | &[ 92 | "-m", "$", 93 | "-m", "Iinserting some text at the start" 94 | ], 95 | " inserting some text at the startfoo bar" 96 | ); 97 | } 98 | 99 | #[test] 100 | fn insert_unicode() { 101 | vicut_integration( 102 | "foo", 103 | &[ 104 | "-m", "ea→bar", 105 | ], 106 | "foo→bar" 107 | ); 108 | } 109 | 110 | #[test] 111 | fn insert_in_empty_line() { 112 | vicut_integration( 113 | "foo\n\nbiz", 114 | &[ 115 | "-m", "jibar", 116 | ], 117 | "foo\nbar\nbiz" 118 | ); 119 | } 120 | 121 | #[test] 122 | fn insert_from_visual_mode() { 123 | vicut_integration( 124 | "foo biz bar", 125 | &[ 126 | "-m", "wveIinserting some text", 127 | ], 128 | "inserting some textfoo biz bar" 129 | ); 130 | vicut_integration( 131 | "foo biz bar", 132 | &[ 133 | "-m", "wveAinserting some text", 134 | ], 135 | "foo bizinserting some text bar" 136 | ); 137 | } 138 | 139 | #[test] 140 | fn insert_empty_buffer() { 141 | vicut_integration( 142 | "", 143 | &[ 144 | "-m", "ihello world" 145 | ], 146 | "hello world" 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/tests/pattern_match.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::vicut_integration; 2 | 3 | 4 | #[test] 5 | fn pattern_matching_api_path_regex() { 6 | vicut_integration( 7 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 8 | &[ "-c", r"/\/api\/v1\/\w+" ], 9 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /" 10 | ); 11 | } 12 | 13 | #[test] 14 | fn pattern_matching_status_code() { 15 | vicut_integration( 16 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 17 | &[ "-c", r"/\b\d{3}\b4n", ], 18 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 2" 19 | ); 20 | } 21 | 22 | #[test] 23 | fn pattern_matching_session_id() { 24 | vicut_integration( 25 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 26 | &[ "-c", r"/\(\w+-\w+\)" ], 27 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (" 28 | ); 29 | } 30 | 31 | #[test] 32 | fn pattern_matching_literal() { 33 | vicut_integration( 34 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 35 | &[ "-c", r"/logged" ], 36 | "User_453 l" 37 | ); 38 | } 39 | 40 | #[test] 41 | fn pattern_matching_forward_and_backward() { 42 | vicut_integration( 43 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 44 | &[ "-c", r"/logged?User" ], 45 | "U" 46 | ); 47 | } 48 | 49 | #[test] 50 | fn pattern_matching_flag_list() { 51 | vicut_integration( 52 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [authenticated,admin,cachehit]", 53 | &[ "-c", r"/\[\w+(,\w+)*\]" ], 54 | "User_453 logged in from IP 192.168.0.42 at [2025-06-14 04:15:32] with session ID (abc123-XYZ), request path: /api/v1/users?limit=100&offset=200. Status: 200 OK. Response time: 123.45ms. Flags: [" 55 | ); 56 | } 57 | 58 | #[test] 59 | fn pattern_matching_literal2() { 60 | vicut_integration( 61 | "The quick brown fox jumps over the lazy dog", 62 | &[ "-c", r"/fox", ], 63 | "The quick brown f" 64 | ); 65 | } 66 | 67 | #[test] 68 | fn pattern_matching_regex_test() { 69 | vicut_integration( 70 | "The quick brown fox jumps over the lazy dog", 71 | &[ "-c", r"/\b.o.", ], 72 | "The quick brown f" 73 | ); 74 | } 75 | 76 | #[test] 77 | fn pattern_matching_mix_search_and_command() { 78 | vicut_integration( 79 | "The quick brown fox jumps over the lazy dog", 80 | &[ "-c", r"/\b.o.\bn", ], 81 | "The quick brown fox jumps over the lazy d" 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/modes/mod.rs: -------------------------------------------------------------------------------- 1 | //! The logic for the various emulations of Vim modes is held in this module. 2 | //! 3 | //! All parsing of KeyEvents into ViCmds takes place in this module. 4 | 5 | use unicode_segmentation::UnicodeSegmentation; 6 | 7 | use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; 8 | use super::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd}; 9 | 10 | pub mod normal; 11 | pub mod insert; 12 | pub mod replace; 13 | pub mod visual; 14 | pub mod search; 15 | pub mod ex; 16 | 17 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 18 | pub enum ModeReport { 19 | Insert, 20 | Normal, 21 | Visual, 22 | Replace, 23 | Search, 24 | Ex, 25 | Unknown 26 | } 27 | 28 | #[derive(Debug,Clone)] 29 | pub enum CmdReplay { 30 | ModeReplay { cmds: Vec, repeat: u16 }, 31 | Single(ViCmd), 32 | Motion(Motion) 33 | } 34 | 35 | impl CmdReplay { 36 | pub fn mode(cmds: Vec, repeat: u16) -> Self { 37 | Self::ModeReplay { cmds, repeat } 38 | } 39 | pub fn single(cmd: ViCmd) -> Self { 40 | Self::Single(cmd) 41 | } 42 | pub fn motion(motion: Motion) -> Self { 43 | Self::Motion(motion) 44 | } 45 | } 46 | 47 | pub enum CmdState { 48 | Pending, 49 | Complete, 50 | Invalid 51 | } 52 | 53 | pub trait ViMode { 54 | fn handle_key(&mut self, key: E) -> Option; 55 | fn handle_key_fallible(&mut self, key: E) -> Result,String> { 56 | // Default behavior 57 | Ok(self.handle_key(key)) 58 | } 59 | fn is_repeatable(&self) -> bool; 60 | fn as_replay(&self) -> Option; 61 | fn cursor_style(&self) -> String; 62 | fn pending_seq(&self) -> Option; 63 | fn move_cursor_on_undo(&self) -> bool; 64 | fn clamp_cursor(&self) -> bool; 65 | fn hist_scroll_start_pos(&self) -> Option; 66 | fn report_mode(&self) -> ModeReport; 67 | fn cmds_from_raw(&mut self, raw: &str) -> Vec { 68 | let mut cmds = vec![]; 69 | for ch in raw.graphemes(true) { 70 | let key = E::new(ch, M::NONE); 71 | match self.report_mode() { 72 | ModeReport::Ex => { 73 | let Ok(option) = self.handle_key_fallible(key) else { 74 | return vec![] 75 | }; 76 | let Some(cmd) = option else { 77 | continue 78 | }; 79 | cmds.push(cmd) 80 | } 81 | _ => { 82 | let Some(cmd) = self.handle_key(key) else { 83 | continue 84 | }; 85 | cmds.push(cmd) 86 | } 87 | } 88 | } 89 | cmds 90 | } 91 | } 92 | 93 | pub fn common_cmds(key: E) -> Option { 94 | let mut pending_cmd = ViCmd::new(); 95 | match key { 96 | E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BeginningOfLine)), 97 | E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::EndOfLine)), 98 | E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)), 99 | E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)), 100 | E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)), 101 | E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)), 102 | E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLineOrNewline)), 103 | E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)), 104 | E(K::Delete, M::NONE) => { 105 | pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); 106 | pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)); 107 | } 108 | E(K::Backspace, M::NONE) | 109 | E(K::Char('H'), M::CTRL) => { 110 | pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); 111 | pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)); 112 | } 113 | _ => return None 114 | } 115 | Some(pending_cmd) 116 | } 117 | -------------------------------------------------------------------------------- /src/modes/insert.rs: -------------------------------------------------------------------------------- 1 | use crate::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word}; 2 | use crate::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; 3 | 4 | use super::{common_cmds, CmdReplay, ModeReport, ViMode}; 5 | 6 | #[derive(Default,Debug)] 7 | pub struct ViInsert { 8 | cmds: Vec, 9 | pending_cmd: ViCmd, 10 | repeat_count: u16 11 | } 12 | 13 | impl ViInsert { 14 | pub fn new() -> Self { 15 | Self::default() 16 | } 17 | pub fn with_count(mut self, repeat_count: u16) -> Self { 18 | self.repeat_count = repeat_count; 19 | self 20 | } 21 | pub fn register_and_return(&mut self) -> Option { 22 | let mut cmd = self.take_cmd(); 23 | cmd.normalize_counts(); 24 | self.register_cmd(&cmd); 25 | Some(cmd) 26 | } 27 | pub fn ctrl_w_is_undo(&self) -> bool { 28 | let insert_count = self.cmds.iter().filter(|cmd| { 29 | matches!(cmd.verb(),Some(VerbCmd(1, Verb::InsertChar(_)))) 30 | }).count(); 31 | let backspace_count = self.cmds.iter().filter(|cmd| { 32 | matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) 33 | }).count(); 34 | insert_count > backspace_count 35 | } 36 | pub fn register_cmd(&mut self, cmd: &ViCmd) { 37 | self.cmds.push(cmd.clone()) 38 | } 39 | pub fn take_cmd(&mut self) -> ViCmd { 40 | std::mem::take(&mut self.pending_cmd) 41 | } 42 | } 43 | 44 | impl ViMode for ViInsert { 45 | fn handle_key(&mut self, key: E) -> Option { 46 | match key { 47 | // Carriage return -> newline 48 | E(K::Enter, M::NONE) | 49 | E(K::Char('\r'), M::NONE) => { 50 | self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar('\n'))); 51 | self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); 52 | self.register_and_return() 53 | } 54 | E(K::Char(ch), M::NONE) => { 55 | self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch))); 56 | self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); 57 | self.register_and_return() 58 | } 59 | E(K::Char('W'), M::CTRL) => { 60 | self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); 61 | self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); 62 | self.register_and_return() 63 | } 64 | E(K::Char('H'), M::CTRL) | 65 | E(K::Backspace, M::NONE) => { 66 | self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); 67 | self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardCharForced)); 68 | self.register_and_return() 69 | } 70 | 71 | E(K::BackTab, M::NONE) => { 72 | self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward)); 73 | self.register_and_return() 74 | } 75 | 76 | E(K::Char('I'), M::CTRL) | 77 | E(K::Tab, M::NONE) => { 78 | self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); 79 | self.register_and_return() 80 | } 81 | 82 | E(K::Esc, M::NONE) => { 83 | self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode)); 84 | self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); 85 | self.register_and_return() 86 | } 87 | _ => common_cmds(key) 88 | } 89 | } 90 | 91 | 92 | fn is_repeatable(&self) -> bool { 93 | true 94 | } 95 | 96 | fn as_replay(&self) -> Option { 97 | Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) 98 | } 99 | 100 | fn cursor_style(&self) -> String { 101 | "\x1b[6 q".to_string() 102 | } 103 | fn pending_seq(&self) -> Option { 104 | None 105 | } 106 | fn move_cursor_on_undo(&self) -> bool { 107 | true 108 | } 109 | fn clamp_cursor(&self) -> bool { 110 | false 111 | } 112 | fn hist_scroll_start_pos(&self) -> Option { 113 | Some(To::End) 114 | } 115 | fn report_mode(&self) -> ModeReport { 116 | ModeReport::Insert 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/tests/modes/visual.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::vicut_integration; 2 | 3 | #[test] 4 | fn select_in_parens() { 5 | vicut_integration( 6 | "This text is (selected) and this is not.", 7 | &[ 8 | "-c", "vi)", 9 | ], 10 | "selected", 11 | ); 12 | } 13 | 14 | #[test] 15 | fn select_in_brackets() { 16 | vicut_integration( 17 | "This text is [selected] and this is not.", 18 | &[ 19 | "-c", "vi]", 20 | ], 21 | "selected", 22 | ); 23 | } 24 | 25 | #[test] 26 | fn select_in_braces() { 27 | vicut_integration( 28 | "This text is {selected} and this is not.", 29 | &[ 30 | "-c", "vi{", 31 | ], 32 | "selected", 33 | ); 34 | } 35 | 36 | #[test] 37 | fn select_in_quotes() { 38 | vicut_integration( 39 | "This text is \"selected\" and this is not.", 40 | &[ 41 | "-c", "vi\"", 42 | ], 43 | "selected", 44 | ); 45 | } 46 | 47 | #[test] 48 | fn select_in_single_quotes() { 49 | vicut_integration( 50 | "This text is 'selected' and this is not.", 51 | &[ 52 | "-c", "vi'", 53 | ], 54 | "selected", 55 | ); 56 | } 57 | 58 | #[test] 59 | fn select_around_parens() { 60 | vicut_integration( 61 | "This text is (selected) and this is not.", 62 | &[ 63 | "-c", "va)", 64 | ], 65 | "(selected) ", 66 | ); 67 | } 68 | 69 | #[test] 70 | fn select_around_brackets() { 71 | vicut_integration( 72 | "This text is [selected] and this is not.", 73 | &[ 74 | "-c", "va]", 75 | ], 76 | "[selected] ", 77 | ); 78 | } 79 | 80 | #[test] 81 | fn select_around_braces() { 82 | vicut_integration( 83 | "This text is {selected} and this is not.", 84 | &[ 85 | "-c", "va{", 86 | ], 87 | "{selected} ", 88 | ); 89 | } 90 | 91 | #[test] 92 | fn select_around_quotes() { 93 | vicut_integration( 94 | "This text is \"selected\" and this is not.", 95 | &[ 96 | "-c", "va\"", 97 | ], 98 | "\"selected\" ", 99 | ); 100 | } 101 | 102 | #[test] 103 | fn select_around_single_quotes() { 104 | vicut_integration( 105 | "This text is 'selected' and this is not.", 106 | &[ 107 | "-c", "va'", 108 | ], 109 | "'selected' ", 110 | ); 111 | } 112 | 113 | #[test] 114 | fn select_lines() { 115 | vicut_integration( 116 | "Line 1\nLine 2\nLine 3", 117 | &[ 118 | "-m", "Vjd", 119 | ], 120 | "Line 3", 121 | ); 122 | } 123 | 124 | #[test] 125 | fn select_lines_with_count() { 126 | vicut_integration( 127 | "Line 1\nLine 2\nLine 3", 128 | &[ 129 | "-c", "2Vj", 130 | ], 131 | "Line 1\nLine 2", 132 | ); 133 | } 134 | 135 | #[test] 136 | fn del_inner_line() { 137 | vicut_integration( 138 | "Line 1\nLine 2\nLine 3", 139 | &[ 140 | "-m", "$v0d", 141 | ], 142 | "\nLine 2\nLine 3", 143 | ); 144 | } 145 | 146 | #[test] 147 | fn select_block() { 148 | vicut_integration( 149 | "Line 1\nLine 2\nLine 3", 150 | &[ 151 | "-c", "jl", 152 | ], 153 | "Li\nLi", 154 | ); 155 | } 156 | 157 | #[test] 158 | fn delete_block() { 159 | vicut_integration( 160 | "Line 1\nLine 2\nLine 3", 161 | &[ 162 | "-m", "jld", 163 | ], 164 | "ne 1\nne 2\nLine 3", 165 | ); 166 | } 167 | 168 | #[test] 169 | fn change_block() { 170 | vicut_integration( 171 | "Line 1\nLine 2\nLine 3", 172 | &[ 173 | "-m", "jlocNew Text", 174 | ], 175 | "New Textne 1\nNew Textne 2\nLine 3", 176 | ); 177 | } 178 | 179 | #[test] 180 | fn change_block_weird() { 181 | vicut_integration( 182 | "abcdefg\nabcd\nabcdegh\nabcde\nabcdefg", 183 | &[ 184 | "-m", "G0$hhhhhkkkkocfoo", 185 | ], 186 | "afoog\nafoo\nafooh\nafoo\nafoog" 187 | ); 188 | } 189 | 190 | #[test] 191 | fn delete_put_block() { 192 | vicut_integration( 193 | "abcdefgh\nabcd\nabcdefghi\nabcde\nabcdefg", 194 | &[ 195 | "-m", "l4jldp", 196 | ], 197 | "adbcefgh\nadbc\nadbcefghi\nadbce\nadbcefg", 198 | ); 199 | } 200 | 201 | #[test] 202 | fn put_block_pad_short_lines() { 203 | vicut_integration( 204 | "abcdefgh\nabcd\nabcdefghi\nabcde\nabcdefg", 205 | &[ 206 | "-m", "G0ll4kd4lp", 207 | ], 208 | "adefghbc\nad bc\nadefghbci\nade bc\nadefg bc", 209 | ); 210 | } 211 | 212 | #[test] 213 | fn put_block_preserve_formatting() { 214 | vicut_integration( 215 | "abcdefgh\nabcd\nabcdefghi\nabcde\nabcdefg", 216 | &[ 217 | "-m", "l$Gdp", 218 | ], 219 | "abcdefgh\nabcd\nabcdefghi\nabcde\nabcdefg", 220 | ); 221 | } 222 | -------------------------------------------------------------------------------- /src/keys.rs: -------------------------------------------------------------------------------- 1 | //! Logic describing key presses is held here. 2 | //! 3 | //! Credit to Rustyline for the design ideas in this module 4 | //! https://github.com/kkawakam/rustyline 5 | 6 | use std::sync::Arc; 7 | use unicode_segmentation::UnicodeSegmentation; 8 | 9 | /// A single key event. 10 | /// 11 | /// Contains the key itself, along with mod keys like Shift and Ctrl 12 | #[derive(Clone,PartialEq,Eq,Debug)] 13 | pub struct KeyEvent(pub KeyCode, pub ModKeys); 14 | 15 | 16 | impl KeyEvent { 17 | /// Parse a new KeyEvent 18 | pub fn new(ch: &str, mut mods: ModKeys) -> Self { 19 | use {KeyCode as K, KeyEvent as E, ModKeys as M}; 20 | 21 | let mut graphemes = ch.graphemes(true); 22 | 23 | let first = match graphemes.next() { 24 | Some(g) => g, 25 | None => return E(K::Null, mods), 26 | }; 27 | 28 | // If more than one grapheme, it's not a single key event 29 | if graphemes.next().is_some() { 30 | return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired 31 | } 32 | 33 | let mut chars = first.chars(); 34 | 35 | let single_char = chars.next(); 36 | let is_single_char = chars.next().is_none(); 37 | 38 | match single_char { 39 | Some(c) if is_single_char && c.is_control() => { 40 | match c { 41 | '\x00' => E(K::Char('@'), mods | M::CTRL), 42 | '\x01' => E(K::Char('A'), mods | M::CTRL), 43 | '\x02' => E(K::Char('B'), mods | M::CTRL), 44 | '\x03' => E(K::Char('C'), mods | M::CTRL), 45 | '\x04' => E(K::Char('D'), mods | M::CTRL), 46 | '\x05' => E(K::Char('E'), mods | M::CTRL), 47 | '\x06' => E(K::Char('F'), mods | M::CTRL), 48 | '\x07' => E(K::Char('G'), mods | M::CTRL), 49 | '\x08' => E(K::Backspace, mods), 50 | '\x09' => { 51 | if mods.contains(M::SHIFT) { 52 | mods.remove(M::SHIFT); 53 | E(K::BackTab, mods) 54 | } else { 55 | E(K::Tab, mods) 56 | } 57 | } 58 | '\x0a' => E(K::Char('J'), mods | M::CTRL), 59 | '\x0b' => E(K::Char('K'), mods | M::CTRL), 60 | '\x0c' => E(K::Char('L'), mods | M::CTRL), 61 | '\x0d' => E(K::Enter, mods), 62 | '\x0e' => E(K::Char('N'), mods | M::CTRL), 63 | '\x0f' => E(K::Char('O'), mods | M::CTRL), 64 | '\x10' => E(K::Char('P'), mods | M::CTRL), 65 | '\x11' => E(K::Char('Q'), mods | M::CTRL), 66 | '\x12' => E(K::Char('R'), mods | M::CTRL), 67 | '\x13' => E(K::Char('S'), mods | M::CTRL), 68 | '\x14' => E(K::Char('T'), mods | M::CTRL), 69 | '\x15' => E(K::Char('U'), mods | M::CTRL), 70 | '\x16' => E(K::Char('V'), mods | M::CTRL), 71 | '\x17' => E(K::Char('W'), mods | M::CTRL), 72 | '\x18' => E(K::Char('X'), mods | M::CTRL), 73 | '\x19' => E(K::Char('Y'), mods | M::CTRL), 74 | '\x1a' => E(K::Char('Z'), mods | M::CTRL), 75 | '\x1b' => E(K::Esc, mods), 76 | '\x1c' => E(K::Char('\\'), mods | M::CTRL), 77 | '\x1d' => E(K::Char(']'), mods | M::CTRL), 78 | '\x1e' => E(K::Char('^'), mods | M::CTRL), 79 | '\x1f' => E(K::Char('_'), mods | M::CTRL), 80 | '\x7f' => E(K::Backspace, mods), 81 | '\u{9b}' => E(K::Esc, mods | M::SHIFT), 82 | _ => E(K::Null, mods), 83 | } 84 | } 85 | Some(c) if is_single_char => { 86 | if !mods.is_empty() { 87 | mods.remove(M::SHIFT); 88 | } 89 | E(K::Char(c), mods) 90 | } 91 | _ => { 92 | // multi-char grapheme (emoji, accented, etc) 93 | if !mods.is_empty() { 94 | mods.remove(M::SHIFT); 95 | } 96 | E(K::Grapheme(Arc::from(first)), mods) 97 | } 98 | } 99 | } 100 | } 101 | 102 | /// Key codes describing different keyboard inputs. 103 | #[derive(Clone,PartialEq,Eq,Debug)] 104 | pub enum KeyCode { 105 | UnknownEscSeq, 106 | Backspace, 107 | BackTab, 108 | BracketedPasteStart, 109 | BracketedPasteEnd, 110 | Char(char), 111 | Grapheme(Arc), 112 | Delete, 113 | Down, 114 | End, 115 | Enter, 116 | Esc, 117 | F(u8), 118 | Home, 119 | Insert, 120 | Left, 121 | Null, 122 | PageDown, 123 | PageUp, 124 | Right, 125 | Tab, 126 | Up, 127 | } 128 | 129 | bitflags::bitflags! { 130 | /// Mod keys like Ctrl,Alt, and Shift. 131 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 132 | pub struct ModKeys: u8 { 133 | /// Control modifier 134 | const CTRL = 1<<3; 135 | /// Escape or Alt modifier 136 | const ALT = 1<<2; 137 | /// Shift modifier 138 | const SHIFT = 1<<1; 139 | 140 | /// No modifier 141 | const NONE = 0; 142 | /// Ctrl + Shift 143 | const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); 144 | /// Alt + Shift 145 | const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); 146 | /// Ctrl + Alt 147 | const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); 148 | /// Ctrl + Alt + Shift 149 | const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/tests/wiki_examples.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::vicut_integration; 2 | 3 | #[test] 4 | fn wiki_example_simple() { 5 | vicut_integration( 6 | "foo bar biz", 7 | &[ "-c", "e" ], 8 | "foo" 9 | ); 10 | } 11 | 12 | #[test] 13 | fn wiki_example_simple2() { 14 | vicut_integration( 15 | "foo bar", 16 | &[ "-m", "w", "-c", "e" ], 17 | "bar" 18 | ); 19 | } 20 | 21 | #[test] 22 | fn wiki_example_template_string() { 23 | vicut_integration( 24 | "foo bar biz\nbiz foo bar\nbar biz foo", 25 | &[ 26 | "--linewise", "--template", "< {{1}} > ( {{2}} ) { {{3}} }", 27 | "-c", "e", 28 | "-m", "w", 29 | "-r", "2", "2", 30 | "-m", "0j", 31 | "-r", "4", "2", 32 | ], 33 | "< foo > ( bar ) { biz }\n< biz > ( foo ) { bar }\n< bar > ( biz ) { foo }" 34 | ); 35 | } 36 | 37 | #[test] 38 | fn wiki_example_json_output() { 39 | vicut_integration( 40 | "foo bar", 41 | &[ 42 | "--json", 43 | "-c", "e", 44 | "-m", "w", 45 | "-c", "e" 46 | ], 47 | "[\n {\n \"1\": \"foo\",\n \"2\": \"bar\"\n }\n]" 48 | ); 49 | } 50 | 51 | #[test] 52 | fn wiki_example_text_objects() { 53 | vicut_integration( 54 | "enp8s0 ethernet connected Wired connection 1\nlo loopback connected (externally) lo \nwlp15s0 wifi disconnected -- \np2p-dev-wlp15s0 wifi-p2p disconnected -- ", 55 | &[ 56 | "--linewise", "--delimiter", " --- ", 57 | "-c", "E", "-m", "w", 58 | "-r", "2", "1", 59 | "-c", "ef)", "-m", "w", 60 | "-c", "$" 61 | ], 62 | "enp8s0 --- ethernet --- connected --- Wired connection 1\nlo --- loopback --- connected (externally) --- lo \nwlp15s0 --- wifi --- disconnected --- -- \np2p-dev-wlp15s0 --- wifi-p2p --- disconnected --- -- " 63 | ); 64 | } 65 | 66 | #[test] 67 | fn wiki_example_repeat_command() { 68 | vicut_integration( 69 | "a b c d e f", 70 | &[ 71 | "--delimiter", " | ", 72 | "-c", "wge", 73 | "-m", "w", 74 | "-r", "2", "2", 75 | ], 76 | "a | b | c" 77 | ); 78 | } 79 | 80 | #[test] 81 | fn wiki_example_nested_repeat() { 82 | vicut_integration( 83 | "foo bar biz\nbiz foo bar\nbar biz foo", 84 | &[ 85 | "--json", 86 | "-c", "e", 87 | "-m", "jb", 88 | "-r", "2", "2", 89 | "-m", "w2k", 90 | "-n", 91 | "-r", "5", "2", 92 | ], 93 | "[ 94 | { 95 | \"1\": \"foo\", 96 | \"2\": \"biz\", 97 | \"3\": \"bar\" 98 | }, 99 | { 100 | \"1\": \"bar\", 101 | \"2\": \"foo\", 102 | \"3\": \"biz\" 103 | }, 104 | { 105 | \"1\": \"biz\", 106 | \"2\": \"bar\", 107 | \"3\": \"foo\" 108 | } 109 | ]" 110 | ); 111 | } 112 | 113 | #[test] 114 | fn wiki_example_name_fields_json() { 115 | vicut_integration( 116 | "31200) FiberFast Networks (Portland, OR, United States) [321.23 km]", 117 | &[ 118 | "--json", 119 | "-c", "name=id", "e", 120 | "-m", "W", 121 | "-c", "name=provider", "t(h", 122 | "-c", "name=location", "vi)", 123 | "-c", "name=distance", "vi]", 124 | ], 125 | "[ 126 | { 127 | \"distance\": \"321.23 km\", 128 | \"id\": \"31200\", 129 | \"location\": \"Portland, OR, United States\", 130 | \"provider\": \"FiberFast Networks\" 131 | } 132 | ]" 133 | ); 134 | } 135 | 136 | #[test] 137 | fn wiki_example_name_fields_template() { 138 | vicut_integration( 139 | "31200) FiberFast Networks (Portland, OR, United States) [321.23 km]", 140 | &[ 141 | "--template", "{{id}} - {{provider}} @ {{location}} ({{distance}})", 142 | "-c", "name=id", "e", 143 | "-m", "W", 144 | "-c", "name=provider", "t(h", 145 | "-c", "name=location", "vi)", 146 | "-c", "name=distance", "vi]", 147 | ], 148 | "31200 - FiberFast Networks @ Portland, OR, United States (321.23 km)" 149 | ); 150 | } 151 | 152 | #[test] 153 | fn wiki_example_edit_buffer() { 154 | vicut_integration( 155 | "useful_data1 some_garbage useful_data2", 156 | &[ 157 | "--json", 158 | "-c", "wdwe" 159 | ], 160 | "[ 161 | { 162 | \"1\": \"useful_data1 useful_data2\" 163 | } 164 | ]" 165 | ); 166 | } 167 | 168 | #[test] 169 | fn wiki_example_insert_mode() { 170 | vicut_integration( 171 | "some_stuff some_stuff some_stuff", 172 | &[ 173 | "--delimiter", " --- ", 174 | "-c", "iField 1: we", 175 | "-m", "w", 176 | "-c", "iField 2: we", 177 | "-m", "w", 178 | "-c", "iField 3: we", 179 | ], 180 | "Field 1: some_stuff --- Field 2: some_stuff --- Field 3: some_stuff" 181 | ); 182 | } 183 | 184 | #[test] 185 | fn wiki_example_visual_mode() { 186 | vicut_integration( 187 | "This text has (some stuff) inside of parenthesis, and [some other stuff] inside of brackets", 188 | &[ 189 | "--delimiter", " -- ", 190 | "-c", "vi)", 191 | "-c", "vi]" 192 | ], 193 | "some stuff -- some other stuff" 194 | ); 195 | } 196 | 197 | #[test] 198 | fn wiki_example_visual_mode2() { 199 | vicut_integration( 200 | "31200) FiberFast Networks (Portland, OR, United States) [321.23 km]\n18220) MetroLink Broadband (Austin, TX, United States) [121.47 km]\n29834) Skyline Internet (Denver, CO, United States) [295.88 km]", 201 | &[ 202 | "--linewise", "--delimiter", " --- ", 203 | "-c", "e", 204 | "-m", "2w", 205 | "-c", "t(h", 206 | "-c", "vi)", 207 | "-c", "vi]", 208 | ], 209 | "31200 --- FiberFast Networks --- Portland, OR, United States --- 321.23 km\n18220 --- MetroLink Broadband --- Austin, TX, United States --- 121.47 km\n29834 --- Skyline Internet --- Denver, CO, United States --- 295.88 km" 210 | ); 211 | } 212 | 213 | #[test] 214 | fn wiki_example_substitution() { 215 | vicut_integration( 216 | "foo bar foo\nbar foo bar\nfoo bar foo", 217 | &[ 218 | "-m", ":%s/foo/###/g", 219 | "-m", ":%s/bar/%%%/g", 220 | "-m", ":%s/%%%/foo/g", 221 | "-m", ":%s/###/bar/g" 222 | ], 223 | "bar foo bar\nfoo bar foo\nbar foo bar" 224 | ); 225 | } 226 | 227 | #[test] 228 | fn wiki_example_append_date() { 229 | vicut_integration( 230 | "foo bar foo\nbar foo bar\nfoo bar foo", 231 | &[ 232 | // Can't use the actual date command because then the test is non-deterministic 233 | "-m", "Go:r !echo 2025-06-18 10:31:56" 234 | ], 235 | "foo bar foo\nbar foo bar\nfoo bar foo\n\n\n2025-06-18 10:31:56" 236 | ); 237 | } 238 | -------------------------------------------------------------------------------- /src/tests/linebuf.rs: -------------------------------------------------------------------------------- 1 | use crate::linebuf::LineBuf; 2 | 3 | #[test] 4 | fn linebuf_empty_linebuf() { 5 | let mut buf = LineBuf::new(); 6 | assert_eq!(buf.as_str(), ""); 7 | buf.update_graphemes_lazy(); 8 | assert!(buf.grapheme_indices().is_empty()); 9 | assert!(buf.slice(0..0).is_none()); 10 | } 11 | 12 | #[test] 13 | fn linebuf_ascii_content() { 14 | let mut buf = LineBuf::new().with_initial("hello".into(), 0); 15 | 16 | buf.update_graphemes_lazy(); 17 | assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]); 18 | 19 | assert_eq!(buf.grapheme_at(0), Some("h")); 20 | assert_eq!(buf.grapheme_at(4), Some("o")); 21 | assert_eq!(buf.slice(1..4), Some("ell")); 22 | assert_eq!(buf.slice_to(2), Some("he")); 23 | assert_eq!(buf.slice_from(2), Some("llo")); 24 | } 25 | 26 | #[test] 27 | fn linebuf_unicode_graphemes() { 28 | let mut buf = LineBuf::new().with_initial("a🇺🇸b́c".into(), 0); 29 | 30 | buf.update_graphemes_lazy(); 31 | let indices = buf.grapheme_indices(); 32 | assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker 33 | 34 | assert_eq!(buf.grapheme_at(0), Some("a")); 35 | assert_eq!(buf.grapheme_at(1), Some("🇺🇸")); 36 | assert_eq!(buf.grapheme_at(2), Some("b́")); // b + combining accent 37 | assert_eq!(buf.grapheme_at(3), Some("c")); 38 | assert_eq!(buf.grapheme_at(4), None); // out of bounds 39 | 40 | assert_eq!(buf.slice(0..2), Some("a🇺🇸")); 41 | assert_eq!(buf.slice(1..3), Some("🇺🇸b́")); 42 | assert_eq!(buf.slice(2..4), Some("b́c")); 43 | } 44 | 45 | #[test] 46 | fn linebuf_slice_to_from_cursor() { 47 | let mut buf = LineBuf::new().with_initial("abçd".into(), 2); 48 | 49 | buf.update_graphemes_lazy(); 50 | assert_eq!(buf.slice_to_cursor(), Some("ab")); 51 | assert_eq!(buf.slice_from_cursor(), Some("çd")); 52 | } 53 | 54 | #[test] 55 | fn linebuf_out_of_bounds_slices() { 56 | let mut buf = LineBuf::new().with_initial("test".into(), 0); 57 | 58 | buf.update_graphemes_lazy(); 59 | 60 | assert_eq!(buf.grapheme_at(5), None); // out of bounds 61 | assert_eq!(buf.slice(2..5), None); // end out of bounds 62 | assert_eq!(buf.slice(4..4), None); // valid but empty 63 | } 64 | 65 | #[test] 66 | fn linebuf_this_line() { 67 | let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; 68 | let mut buf = LineBuf::new().with_initial(initial.into(), 57); 69 | let (start,end) = buf.this_line(); 70 | assert_eq!(buf.slice(start..end), Some("This is the third line\n")) 71 | } 72 | 73 | #[test] 74 | fn linebuf_prev_line() { 75 | let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; 76 | let mut buf = LineBuf::new().with_initial(initial.into(), 57); 77 | let (start,end) = buf.nth_prev_line(1).unwrap(); 78 | assert_eq!(buf.slice(start..end), Some("This is the second line\n")) 79 | } 80 | 81 | #[test] 82 | fn linebuf_prev_line_first_line_is_empty() { 83 | let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; 84 | let mut buf = LineBuf::new().with_initial(initial.into(), 36); 85 | let (start,end) = buf.nth_prev_line(1).unwrap(); 86 | assert_eq!(buf.slice(start..end), Some("This is the first line\n")) 87 | } 88 | 89 | #[test] 90 | fn linebuf_next_line() { 91 | let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; 92 | let mut buf = LineBuf::new().with_initial(initial.into(), 57); 93 | let (start,end) = buf.nth_next_line(1).unwrap(); 94 | assert_eq!(buf.slice(start..end), Some("This is the fourth line")) 95 | } 96 | 97 | #[test] 98 | fn linebuf_next_line_last_line_is_empty() { 99 | let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n"; 100 | let mut buf = LineBuf::new().with_initial(initial.into(), 57); 101 | let (start,end) = buf.nth_next_line(1).unwrap(); 102 | assert_eq!(buf.slice(start..end), Some("This is the fourth line\n")) 103 | } 104 | 105 | #[test] 106 | fn linebuf_next_line_several_trailing_newlines() { 107 | let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n"; 108 | let mut buf = LineBuf::new().with_initial(initial.into(), 81); 109 | let (start,end) = buf.nth_next_line(1).unwrap(); 110 | assert_eq!(buf.slice(start..end), Some("\n")) 111 | } 112 | 113 | #[test] 114 | fn linebuf_next_line_only_newlines() { 115 | let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; 116 | let mut buf = LineBuf::new().with_initial(initial.into(), 7); 117 | let (start,end) = buf.nth_next_line(1).unwrap(); 118 | assert_eq!(start, 8); 119 | assert_eq!(buf.slice(start..end), Some("\n")) 120 | } 121 | 122 | #[test] 123 | fn linebuf_prev_line_only_newlines() { 124 | let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; 125 | let mut buf = LineBuf::new().with_initial(initial.into(), 7); 126 | let (start,end) = buf.nth_prev_line(1).unwrap(); 127 | assert_eq!(buf.slice(start..end), Some("\n")); 128 | assert_eq!(start, 6); 129 | } 130 | 131 | #[test] 132 | fn linebuf_cursor_motion() { 133 | let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶".into(), 0); 134 | 135 | buf.update_graphemes_lazy(); 136 | let total = buf.grapheme_indices.as_ref().unwrap().len(); 137 | 138 | for i in 0..total { 139 | buf.cursor.set(i); 140 | 141 | let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string(); 142 | let expected_from = if i + 1 < total { 143 | buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() 144 | } else { 145 | // last grapheme, ends at buffer end 146 | buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() 147 | }; 148 | 149 | let expected_at = { 150 | let start = buf.grapheme_indices_owned()[i]; 151 | let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len()); 152 | buf.buffer.get(start..end).map(|slice| slice.to_string()) 153 | }; 154 | 155 | assert_eq!( 156 | buf.slice_to_cursor(), 157 | Some(expected_to.as_str()), 158 | "Failed at cursor position {i}: slice_to_cursor" 159 | ); 160 | assert_eq!( 161 | buf.slice_from_cursor(), 162 | Some(expected_from.as_str()), 163 | "Failed at cursor position {i}: slice_from_cursor" 164 | ); 165 | assert_eq!( 166 | buf.grapheme_at(i).map(|slice| slice.to_string()), 167 | expected_at, 168 | "Failed at cursor position {i}: grapheme_at" 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/tests/golden_files/vicut_main_all_comments.rs: -------------------------------------------------------------------------------- 1 | // So using it in pool.install() doesn't work. We have to initialize it in the closure there. 2 | // We need to initialize stream in each branch, since Box does not implement send/sync 3 | // Used to call the main logic internally 4 | // Testing fixture 5 | // Print help or version info and exit early if `--help` or `--version` are found 6 | // Testing 7 | // Testing fixture for the debug profile 8 | // Simplest of the three routes. 9 | // Default execution pathway. Operates on `stdin`. 10 | // Operates on the content of the files, and either prints to stdout, or edits the files in-place 11 | // Execution pathway for handling filenames given as arguments 12 | // Output has already been handled 13 | // Output has already been handled 14 | // So using it in pool.install() doesn't work. We have to initialize it in the closure there. 15 | // We need to initialize stream in each branch, since Box does not implement send/sync 16 | // Each route in this function operates on individual lines from the input 17 | // The pathway for when the `--linewise` flag is set 18 | // Pair each line with its original index 19 | // Reads the complete input from stdin and then splits it into its lines for execution. 20 | // This function is used for `--linewise` execution on stdin. 21 | // Executes commands on lines from stdin, using multi-threaded processing 22 | // Sort lines 23 | // Write back to file 24 | // Separate content by file 25 | // Process each line's content 26 | // Backup files are created if `--backup-files` is enabled. 27 | // Errors during reading, transformation, or writing will abort the program with a diagnostic. 28 | // - Print to `stdout`, optionally prefixed by filename (`if multiple input files`) 29 | // - Write the result back to the original file (`-i is set`) 30 | // 7. Reconstruct each file's contents and either: 31 | // 6. Sort each file’s lines by line number to restore the original order. 32 | // 5. Group the transformed lines by filename in a `BTreeMap`. 33 | // 4. Use a parallel iterator to transform each line using `execute()`. 34 | // 3. Tag each line with its originating filename and line number. 35 | // 2. Combine all lines from all files into a single work pool. 36 | // 1. Split each file into its lines. 37 | // Steps: 38 | // the full outputs in order. 39 | // transforming each line independently using the `execute()` function and then reconstructing 40 | // This function is used for `--linewise` execution. It processes all lines in parallel, 41 | // Executes all input files line-by-line using multi-threaded processing. 42 | // Write back to file 43 | // Process each file's content 44 | // 3. Decide how to handle output depending on whether args.edit_inplace is set. 45 | // 2. Call `execute()` on each file's contents 46 | // 1. Create a `work` vector containing a tuple of the file's path, and it's contents. 47 | // The steps this function walks through are as follows: 48 | // Multi-thread the execution of file input. 49 | // -n 50 | // -c name= 51 | // -c 52 | // -m 53 | // Negative branch 54 | // Execute our commands 55 | // Set the cursor on the start of the line 56 | // Positive branch 57 | // LineBuf::eval_motion() *always* returns MotionKind::Lines() for Motion::Global/NotGlobal. 58 | // Here we ask ViCut's editor directly to evaluate the Global motion for us. 59 | // -g/-v [--else ] 60 | // We use recursion so that we can nest repeats easily 61 | // -r 62 | // Execute a single `Cmd` 63 | // in each line. The newline characters are vital to `LineBuf`'s navigation logic. 64 | // We use this instead of `String::lines()` because that method does not include the newline itself 65 | // Split a string slice into it's lines. 66 | // Trim the fields 🧑‍🌾 67 | // want to see that output, with or without globals. 68 | // But if the files vector is empty, the user is working on stdin, so they will probably 69 | // We don't want to spam the output with entire files with no matches in that case, 70 | // that the user is probably searching for something, potentially in a group of files. 71 | // if args has files it is working on, and the command list has a global, that means 72 | // fmt_lines is empty, so the user didn't write any -c commands 73 | // Let's figure out if we want to print the whole buffer 74 | // Next we loop over `args.cmds` and execute each one in sequence. 75 | // Here we are going to initialize a new instance of `ViCut` to manage state for editing this input 76 | // Execute the user's commands. 77 | // The loop looks for patterns like {{1}} or {{foo}} to interpolate on 78 | // We use a state machine here to interpolate the fields 79 | // Format the output according to the given format string 80 | // Push the new string 81 | // Also clear fields for the next line 82 | // Join the fields by the delimiter 83 | // So let's double pop the 2d vector and grab the value of our only field 84 | // We performed len checks in no_fields_extracted(), so unwrap is safe 85 | // Let's check to see if we are outputting the whole buffer 86 | // If we did extract some fields, we print each record one at a time, and each field will be separated by `delimiter` 87 | // If we didn't extract any fields, we do our best to preserve the formatting of the original input 88 | // Perform standard output formatting. 89 | // This can be depended on, since `"0"` is a reserved field name that cannot be set by user input. 90 | // Checks for the `"0"` field name, which is a sentinel value that says "We didn't get any `-c` commands" 91 | // Check to see if we didn't explicitly extract any fields 92 | // Format the output as JSON 93 | // `lines` is a two-dimensional vector of tuples, each representing a key/value pair for extract fields. 94 | // Format the stuff we extracted according to user specification 95 | // If `trace` is true, then trace!() calls always activate, with our custom formatting. 96 | // This interacts with the `--trace` flag that can be passed in the arguments. 97 | // Initialize the logger 98 | // Prints out the help info for `vicut` 99 | // "Get some help" - Michael Jordan 100 | /// We check all three separately instead of just the last one, so that we can give better error messages 101 | /// 3. The path given refers to a file that we are allowed to read. 102 | /// 2. The path given refers to a file. 103 | /// 1. The path given exists. 104 | /// Checks to make sure the following invariants are met: 105 | /// Handle a filename passed as an argument. 106 | // no need to be pressed about a missing '--end' when nothing would come after it 107 | // Let's just submit the current -g commands. 108 | // If we got here, we have run out of arguments 109 | // We're done here 110 | // Now we start working on this 111 | /// ``` 112 | /// vicut -g 'foo' -g 'bar' -c 'd' --else -v 'baz' -c 'y' --end --end 113 | /// ```bash 114 | /// deep combinations of conditionals and scopes, like: 115 | /// build a nested command execution tree from the input. This allows arbitrarily 116 | /// Because of this recursive structure, we use a recursive descent parser to 117 | /// and `-r` repeats. 118 | /// can contain other nested `-g` or `-v` invocations, as well as `--else` branches 119 | /// that will only execute if a pattern match (or non-match) succeeds. These blocks 120 | /// `-g` and `-v` are special cases: each introduces a scoped block of commands 121 | /// Handles `-g` and `-v` global conditionals. 122 | // So we can't let people use it arbitrarily, or weird shit starts happening 123 | // We use '0' as a sentinel value to say "We didn't slice any fields, so this field is the entire buffer" 124 | /// Parse the user's arguments 125 | // The arguments passed to the program by the user 126 | // Whether to execute on a match, or on no match 127 | // The field name used in `Cmd::NamedField` 128 | // For linux we use Jemalloc. It is ***significantly*** faster than the default allocator in this case, for some reason. 129 | -------------------------------------------------------------------------------- /src/register.rs: -------------------------------------------------------------------------------- 1 | //! This module contains logic for emulation of Vim's registers feature. 2 | //! 3 | //! It contains the `Registers` struct, which is held in a thread local, global variable. 4 | use std::{cell::RefCell, fmt::Display}; 5 | 6 | thread_local! { 7 | /// The global state for all registers. 8 | /// 9 | /// This variable is thread local, so it can be freely mutated. 10 | pub static REGISTERS: RefCell = const { RefCell::new(Registers::new()) }; 11 | } 12 | 13 | /// Attempt to read from the register corresponding to the given character 14 | pub fn read_register(ch: Option) -> Option { 15 | REGISTERS.with_borrow(|regs| regs.get_reg(ch).map(|r| r.content().clone())) 16 | } 17 | 18 | /// Attempt to write to the register corresponding to the given character 19 | pub fn write_register(ch: Option, buf: RegisterContent) { 20 | REGISTERS.with_borrow_mut(|regs| if let Some(r) = regs.get_reg_mut(ch) { r.write(buf); }) 21 | } 22 | 23 | /// Attempt to append text to the register corresponding to the given character 24 | pub fn append_register(ch: Option, buf: RegisterContent) { 25 | REGISTERS.with_borrow_mut(|regs| if let Some(r) = regs.get_reg_mut(ch) { r.append(buf) }) 26 | } 27 | 28 | #[derive(Default,Debug)] 29 | pub struct Registers { 30 | default: Register, 31 | a: Register, 32 | b: Register, 33 | c: Register, 34 | d: Register, 35 | e: Register, 36 | f: Register, 37 | g: Register, 38 | h: Register, 39 | i: Register, 40 | j: Register, 41 | k: Register, 42 | l: Register, 43 | m: Register, 44 | n: Register, 45 | o: Register, 46 | p: Register, 47 | q: Register, 48 | r: Register, 49 | s: Register, 50 | t: Register, 51 | u: Register, 52 | v: Register, 53 | w: Register, 54 | x: Register, 55 | y: Register, 56 | z: Register, 57 | } 58 | 59 | impl Registers { 60 | pub const fn new() -> Self { 61 | // Wish I could use Self::default() here 62 | // but Default::default() is not a const fn 63 | // So here we go 64 | Self { 65 | default: Register::new(), 66 | a: Register::new(), 67 | b: Register::new(), 68 | c: Register::new(), 69 | d: Register::new(), 70 | e: Register::new(), 71 | f: Register::new(), 72 | g: Register::new(), 73 | h: Register::new(), 74 | i: Register::new(), 75 | j: Register::new(), 76 | k: Register::new(), 77 | l: Register::new(), 78 | m: Register::new(), 79 | n: Register::new(), 80 | o: Register::new(), 81 | p: Register::new(), 82 | q: Register::new(), 83 | r: Register::new(), 84 | s: Register::new(), 85 | t: Register::new(), 86 | u: Register::new(), 87 | v: Register::new(), 88 | w: Register::new(), 89 | x: Register::new(), 90 | y: Register::new(), 91 | z: Register::new(), 92 | } 93 | } 94 | /// Get a register by name. Read only. 95 | pub fn get_reg(&self, ch: Option) -> Option<&Register> { 96 | let Some(ch) = ch else { 97 | return Some(&self.default) 98 | }; 99 | match ch { 100 | 'a' => Some(&self.a), 101 | 'b' => Some(&self.b), 102 | 'c' => Some(&self.c), 103 | 'd' => Some(&self.d), 104 | 'e' => Some(&self.e), 105 | 'f' => Some(&self.f), 106 | 'g' => Some(&self.g), 107 | 'h' => Some(&self.h), 108 | 'i' => Some(&self.i), 109 | 'j' => Some(&self.j), 110 | 'k' => Some(&self.k), 111 | 'l' => Some(&self.l), 112 | 'm' => Some(&self.m), 113 | 'n' => Some(&self.n), 114 | 'o' => Some(&self.o), 115 | 'p' => Some(&self.p), 116 | 'q' => Some(&self.q), 117 | 'r' => Some(&self.r), 118 | 's' => Some(&self.s), 119 | 't' => Some(&self.t), 120 | 'u' => Some(&self.u), 121 | 'v' => Some(&self.v), 122 | 'w' => Some(&self.w), 123 | 'x' => Some(&self.x), 124 | 'y' => Some(&self.y), 125 | 'z' => Some(&self.z), 126 | _ => None 127 | } 128 | } 129 | /// Get a mutable reference to a register by name. 130 | pub fn get_reg_mut(&mut self, ch: Option) -> Option<&mut Register> { 131 | let Some(ch) = ch else { 132 | return Some(&mut self.default) 133 | }; 134 | match ch { 135 | 'a' => Some(&mut self.a), 136 | 'b' => Some(&mut self.b), 137 | 'c' => Some(&mut self.c), 138 | 'd' => Some(&mut self.d), 139 | 'e' => Some(&mut self.e), 140 | 'f' => Some(&mut self.f), 141 | 'g' => Some(&mut self.g), 142 | 'h' => Some(&mut self.h), 143 | 'i' => Some(&mut self.i), 144 | 'j' => Some(&mut self.j), 145 | 'k' => Some(&mut self.k), 146 | 'l' => Some(&mut self.l), 147 | 'm' => Some(&mut self.m), 148 | 'n' => Some(&mut self.n), 149 | 'o' => Some(&mut self.o), 150 | 'p' => Some(&mut self.p), 151 | 'q' => Some(&mut self.q), 152 | 'r' => Some(&mut self.r), 153 | 's' => Some(&mut self.s), 154 | 't' => Some(&mut self.t), 155 | 'u' => Some(&mut self.u), 156 | 'v' => Some(&mut self.v), 157 | 'w' => Some(&mut self.w), 158 | 'x' => Some(&mut self.x), 159 | 'y' => Some(&mut self.y), 160 | 'z' => Some(&mut self.z), 161 | _ => None 162 | } 163 | } 164 | } 165 | 166 | #[derive(Default,Clone,Debug)] 167 | pub enum RegisterContent { 168 | Span(String), 169 | Line(String), 170 | Block(Vec), 171 | #[default] 172 | Empty 173 | } 174 | 175 | impl RegisterContent { 176 | pub fn clear(&mut self) { 177 | match self { 178 | Self::Span(s) => s.clear(), 179 | Self::Line(s) => s.clear(), 180 | Self::Block(v) => v.clear(), 181 | Self::Empty => {} 182 | } 183 | } 184 | pub fn len(&self) -> usize { 185 | match self { 186 | Self::Span(s) => s.len(), 187 | Self::Line(s) => s.len(), 188 | Self::Block(v) => v.len(), 189 | Self::Empty => 0 190 | } 191 | } 192 | pub fn is_empty(&self) -> bool { 193 | match self { 194 | Self::Span(s) => s.is_empty(), 195 | Self::Line(s) => s.is_empty(), 196 | Self::Block(v) => v.is_empty(), 197 | Self::Empty => true 198 | } 199 | } 200 | } 201 | 202 | impl Display for RegisterContent { 203 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 204 | match self { 205 | Self::Span(s) => write!(f, "{}", s), 206 | Self::Line(s) => write!(f, "{}", s), 207 | Self::Block(v) => write!(f, "{}", v.join("\n")), 208 | Self::Empty => write!(f, "") 209 | } 210 | } 211 | } 212 | 213 | /// A single register. 214 | /// 215 | /// The `is_whole_line` field is flipped to `true` when you do something like `dd` to delete an entire line 216 | /// If content is `put` from the register, behavior changes depending on this field. 217 | #[derive(Clone,Default,Debug)] 218 | pub struct Register { 219 | content: RegisterContent, 220 | } 221 | impl Register { 222 | pub const fn new() -> Self { 223 | Self { 224 | content: RegisterContent::Span(String::new()), 225 | } 226 | } 227 | pub fn content(&self) -> &RegisterContent { 228 | &self.content 229 | } 230 | pub fn write(&mut self, buf: RegisterContent) { 231 | self.content = buf 232 | } 233 | pub fn append(&mut self, buf: RegisterContent) { 234 | match buf { 235 | RegisterContent::Empty => {}, 236 | RegisterContent::Span(ref s) | 237 | RegisterContent::Line(ref s) => { 238 | match &mut self.content { 239 | RegisterContent::Empty => self.content = buf, 240 | RegisterContent::Span(existing) => existing.push_str(s), 241 | RegisterContent::Line(existing) => existing.push_str(s), 242 | RegisterContent::Block(_) => { 243 | self.content = buf 244 | } 245 | } 246 | } 247 | RegisterContent::Block(v) => { 248 | match &mut self.content { 249 | RegisterContent::Block(existing) => existing.extend(v), 250 | _ => { 251 | self.content = RegisterContent::Block(v); 252 | } 253 | } 254 | } 255 | } 256 | } 257 | pub fn clear(&mut self) { 258 | self.content.clear() 259 | } 260 | pub fn is_line(&self) -> bool { 261 | matches!(self.content, RegisterContent::Line(_)) 262 | } 263 | pub fn is_block(&self) -> bool { 264 | matches!(self.content, RegisterContent::Block(_)) 265 | } 266 | pub fn is_span(&self) -> bool { 267 | matches!(self.content, RegisterContent::Span(_)) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the logic for converting command strings like 'di)' into key events. 2 | //! 3 | //! The main driver of this logic is the `RawReader` struct, which implements the `KeyReader` trait. 4 | //! 5 | use std::collections::VecDeque; 6 | 7 | use crate::keys::{KeyCode, KeyEvent, ModKeys}; 8 | 9 | /// Structs implementing this trait can be used to produce KeyEvents. 10 | /// Having this as a trait leaves us open to implementing different input methods in the future. 11 | /// An interactive mode perhaps? 12 | pub trait KeyReader { 13 | fn read_key(&mut self) -> Option; 14 | } 15 | 16 | /// Struct for reading raw command input strings. 17 | /// 18 | /// `RawReader`'s main job is to read a command string like 'd2w' and convert it into KeyEvents 19 | /// KeyEvents are the tokens that our mode structs use to parse new Vim commands. 20 | #[derive(Default,Debug)] 21 | pub struct RawReader { 22 | pub bytes: VecDeque, 23 | pub is_escaped: bool // The last byte was a backslash or not 24 | } 25 | 26 | impl RawReader { 27 | pub fn new() -> Self { 28 | Self::default() 29 | } 30 | pub fn with_initial(mut self, bytes: &[u8]) -> Self { 31 | let bytes = bytes.iter(); 32 | self.bytes.extend(bytes); 33 | self 34 | } 35 | 36 | pub fn load_bytes(&mut self, bytes: &[u8]) { 37 | self.bytes.clear(); 38 | let bytes = bytes.iter(); 39 | self.bytes.extend(bytes); 40 | } 41 | pub fn get_bytes(&self) -> VecDeque { 42 | self.bytes.clone() 43 | } 44 | pub fn get_chars(&self) -> Vec { 45 | self.bytes.iter().map(|b| *b as char).collect() 46 | } 47 | pub fn set_bytes(&mut self, bytes: VecDeque) { 48 | self.bytes = bytes 49 | } 50 | pub fn push_bytes_front(&mut self, bytes: &[u8]) { 51 | for byte in bytes.iter().rev() { 52 | self.bytes.push_front(*byte); 53 | } 54 | } 55 | 56 | /// Parse an escape sequence 57 | pub fn parse_esc_seq(&mut self) -> Option { 58 | let mut seq = vec![0x1b]; 59 | let b1 = self.bytes.pop_front()?; 60 | seq.push(b1); 61 | 62 | match b1 { 63 | b'[' => { 64 | let b2 = self.bytes.pop_front()?; 65 | seq.push(b2); 66 | 67 | match b2 { 68 | b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())), 69 | b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())), 70 | b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())), 71 | b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())), 72 | b'1'..=b'9' => { 73 | let mut digits = vec![b2]; 74 | 75 | while let Some(&b) = self.bytes.front() { 76 | seq.push(b); 77 | self.bytes.pop_front(); 78 | 79 | if b == b'~' || b == b';' { 80 | break; 81 | } else if b.is_ascii_digit() { 82 | digits.push(b); 83 | } else { 84 | break; 85 | } 86 | } 87 | 88 | let key = match digits.as_slice() { 89 | [b'1'] => KeyCode::Home, 90 | [b'3'] => KeyCode::Delete, 91 | [b'4'] => KeyCode::End, 92 | [b'5'] => KeyCode::PageUp, 93 | [b'6'] => KeyCode::PageDown, 94 | [b'7'] => KeyCode::Home, // xterm alternate 95 | [b'8'] => KeyCode::End, // xterm alternate 96 | 97 | [b'1', b'5'] => KeyCode::F(5), 98 | [b'1', b'7'] => KeyCode::F(6), 99 | [b'1', b'8'] => KeyCode::F(7), 100 | [b'1', b'9'] => KeyCode::F(8), 101 | [b'2', b'0'] => KeyCode::F(9), 102 | [b'2', b'1'] => KeyCode::F(10), 103 | [b'2', b'3'] => KeyCode::F(11), 104 | [b'2', b'4'] => KeyCode::F(12), 105 | _ => KeyCode::Esc, 106 | }; 107 | 108 | Some(KeyEvent(key, ModKeys::empty())) 109 | } 110 | _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), 111 | } 112 | } 113 | 114 | b'O' => { 115 | let b2 = self.bytes.pop_front()?; 116 | seq.push(b2); 117 | 118 | let key = match b2 { 119 | b'P' => KeyCode::F(1), 120 | b'Q' => KeyCode::F(2), 121 | b'R' => KeyCode::F(3), 122 | b'S' => KeyCode::F(4), 123 | _ => KeyCode::Esc, 124 | }; 125 | 126 | Some(KeyEvent(key, ModKeys::empty())) 127 | } 128 | 129 | _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), 130 | } 131 | } 132 | /// Parse a byte alias 133 | /// 134 | /// This is where aliases like `` and `` are parsed into KeyEvents 135 | pub fn parse_byte_alias(&mut self) -> Option { 136 | let mut buf = vec![]; 137 | let mut byte_iter = self.bytes.iter().copied(); 138 | for b in byte_iter.by_ref() { 139 | match b { 140 | b'>' => break, 141 | _ => buf.push(b) 142 | } 143 | } 144 | 145 | if buf.is_empty() { 146 | return None 147 | } 148 | 149 | let mut mods = ModKeys::NONE; 150 | 151 | // Collect mod keys 152 | // Order does not matter here 153 | loop { 154 | if buf.as_slice().starts_with(b"c-") { 155 | mods |= ModKeys::CTRL; 156 | buf = buf[2..].to_vec(); 157 | } else if buf.as_slice().starts_with(b"s-") { 158 | mods |= ModKeys::SHIFT; 159 | buf = buf[2..].to_vec(); 160 | } else if buf.as_slice().starts_with(b"a-") { 161 | mods |= ModKeys::ALT; 162 | buf = buf[2..].to_vec(); 163 | } else { 164 | break; 165 | } 166 | } 167 | 168 | let is_fn_key = buf.len() > 1 && buf[0] == b'f' && buf[1..].iter().all(|c| (*c as char).is_ascii_digit()); 169 | let is_alphanum_key = buf.len() == 1 && (buf[0] as char).is_alphanumeric(); 170 | 171 | // Match aliases 172 | let result = match buf.as_slice() { 173 | // Common weird keys 174 | b"esc" => Some(KeyEvent(KeyCode::Esc, mods)), 175 | b"CR" => Some(KeyEvent(KeyCode::Char('\r'), mods)), 176 | b"return" | 177 | b"enter" => Some(KeyEvent(KeyCode::Enter, mods)), 178 | b"tab" => Some(KeyEvent(KeyCode::Char('\t'), mods)), 179 | b"BS" => Some(KeyEvent(KeyCode::Backspace, mods)), 180 | b"del" => Some(KeyEvent(KeyCode::Delete, mods)), 181 | b"ins" => Some(KeyEvent(KeyCode::Insert, mods)), 182 | b"home" => Some(KeyEvent(KeyCode::Home, mods)), 183 | b"end" => Some(KeyEvent(KeyCode::End, mods)), 184 | b"left" => Some(KeyEvent(KeyCode::Left, mods)), 185 | b"right" => Some(KeyEvent(KeyCode::Right, mods)), 186 | b"up" => Some(KeyEvent(KeyCode::Up, mods)), 187 | b"down" => Some(KeyEvent(KeyCode::Down, mods)), 188 | b"pgup" => Some(KeyEvent(KeyCode::PageUp, mods)), 189 | b"pgdown" => Some(KeyEvent(KeyCode::PageDown, mods)), 190 | 191 | // Check for alphanumeric keys 192 | b_ch if is_alphanum_key => Some(KeyEvent(KeyCode::Char((b_ch[0] as char).to_ascii_uppercase()), mods)), 193 | 194 | // Check for function keys 195 | _ if is_fn_key => { 196 | let stripped = buf.strip_prefix(b"f").unwrap(); 197 | std::str::from_utf8(stripped) 198 | .ok() 199 | .and_then(|s| { 200 | s.parse::().ok() 201 | }) 202 | .map(|n| KeyEvent(KeyCode::F(n), mods)) 203 | } 204 | _ => None 205 | }; 206 | if result.is_some() { 207 | self.bytes = byte_iter.collect(); 208 | } 209 | result 210 | } 211 | } 212 | 213 | impl KeyReader for RawReader { 214 | /// Read a single KeyEvent from the internal byte deque. 215 | fn read_key(&mut self) -> Option { 216 | use core::str; 217 | 218 | let mut collected = Vec::with_capacity(4); 219 | 220 | loop { 221 | let byte = self.bytes.pop_front()?; 222 | 223 | // Check for byte aliases like '' and '' 224 | if byte == b'<' && !self.is_escaped { 225 | if let Some(key) = self.parse_byte_alias() { 226 | return Some(key) 227 | } 228 | } 229 | if byte == b'\\' { 230 | self.is_escaped = !self.is_escaped; 231 | } else { 232 | self.is_escaped = false; 233 | } 234 | 235 | collected.push(byte); 236 | 237 | // If it's an escape sequence, delegate 238 | if collected[0] == 0x1b && collected.len() == 1 { 239 | if let Some(&_next @ (b'[' | b'O')) = self.bytes.front() { 240 | let seq = self.parse_esc_seq(); 241 | return seq 242 | } 243 | } 244 | 245 | // Try parse as valid UTF-8 246 | if let Ok(s) = str::from_utf8(&collected) { 247 | return Some(KeyEvent::new(s, ModKeys::empty())); 248 | } 249 | 250 | if collected.len() >= 4 { 251 | break; 252 | } 253 | } 254 | 255 | None 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/vic/vic.pest: -------------------------------------------------------------------------------- 1 | WHITESPACE = _{ " " | "\t" | NEWLINE } 2 | COMMENT = _{ !"\\" ~ "#" ~ (!NEWLINE ~ ANY)+ ~ NEWLINE? } 3 | 4 | // Prelude options 5 | 6 | backup_ext = { "backup_ext" ~ "=" ~ atomic_string } 7 | template = { "template" ~ "=" ~ atomic_string } 8 | delimiter = { "delimiter" ~ "=" ~ atomic_string } 9 | file = { "file" ~ "=" ~ "\"" ~ inner ~ "\"" } 10 | files = { "files" ~ "=" ~ "[" ~ (("\"" ~ inner ~ "\"" ~ ",")* ~ ("\"" ~ inner ~ "\"")?)? ~ "]" } 11 | pipe_in = { "pipe_in" ~ "=" ~ atomic_string } 12 | pipe_out = { "pipe_out" ~ "=" ~ atomic_string } 13 | write = { "write" ~ "=" ~ "\"" ~ inner ~ "\"" } 14 | max_jobs = { "max_jobs" ~ "=" ~ "\"" ~ int ~ "\"" } 15 | trace = { "trace" } 16 | json = { "json" } 17 | linewise = { "linewise" } 18 | serial = { "serial" } 19 | trim_fields = { "trim_fields" } 20 | keep_mode = { "keep_mode" } 21 | backup = { "backup" } 22 | edit_inplace = { "edit_inplace" } 23 | silent = { "silent" } 24 | no_input = { "no_input" } 25 | global_uses_line_numbers = { "global_uses_line_numbers" } 26 | 27 | opt = { 28 | json 29 | | pipe_in 30 | | pipe_out 31 | | linewise 32 | | serial 33 | | trim_fields 34 | | keep_mode 35 | | backup_ext 36 | | backup 37 | | template 38 | | delimiter 39 | | max_jobs 40 | | trace 41 | | file 42 | | no_input 43 | | silent 44 | | global_uses_line_numbers 45 | | edit_inplace 46 | | write 47 | | files 48 | } 49 | opts_block = { "{" ~ ((opt ~ ",")* ~ opt?)? ~ "}" } 50 | prelude = { "opts" ~ opts_block } 51 | 52 | // Commands 53 | 54 | global = _{ "global" | "g" } 55 | not_global = _{ "v" | "not_global" | "!global" } 56 | move = _{ "move" | "m" } 57 | cut = _{ "cut" | "c" } 58 | next = { "next" | "n" } 59 | echo = _{ "echo" } 60 | repeat = _{ "repeat" | "r" } 61 | yank = _{ "yank" | "y" } 62 | break_loop = _{ "break" } 63 | push = _{ "push" } 64 | pop = _{ "pop" } 65 | continue_loop = _{ "continue" } 66 | return = _{ "return" | "ret" } 67 | 68 | include = { "include" ~ atomic_string } 69 | alias = { "alias" ~ var_name ~ block } 70 | repeat_cmd = ${ repeat ~ WHITESPACE+ ~ (int | var) ~ WHITESPACE+ ~ block } 71 | global_cmd = ${ global ~ WHITESPACE+ ~ pattern ~ WHITESPACE+ ~ block ~ WHITESPACE+ ~ ("else" ~ WHITESPACE+ ~ block)? } 72 | move_cmd = ${ move ~ WHITESPACE+ ~ vim_cmd } 73 | buf_cmd = ${ buf_switch | buf_id } 74 | buf_switch = ${ "buf" ~ WHITESPACE+ ~ "switch" ~ WHITESPACE+ ~ expr } 75 | buf_id = ${ "buf" ~ WHITESPACE+ ~ "id" } 76 | echo_cmd = ${ echo ~ (" "+ ~ expr)* } 77 | cut_cmd = ${ cut ~ (WHITESPACE+ ~ name_def)? ~ WHITESPACE+ ~ vim_cmd } 78 | return_cmd = ${ return ~ (WHITESPACE+ ~ vim_cmd)? } 79 | push_cmd = ${ push ~ WHITESPACE+ ~ var ~ WHITESPACE+ ~ expr } 80 | pop_cmd = ${ pop ~ WHITESPACE+ ~ var } 81 | yank_cmd = ${ yank ~ (WHITESPACE+ ~ register)? ~ WHITESPACE+ ~ expr } 82 | not_global_cmd = ${ not_global ~ WHITESPACE+ ~ pattern ~ block ~ ("else" ~ block)? } 83 | 84 | // Control flow 85 | 86 | for_block = { "for" ~ var_name ~ "in" ~ (range_inclusive | range | array | var) ~ block } 87 | while_block = { "while" ~ (bool_expr | bool_expr_single | var | bool) ~ block } 88 | until_block = { "until" ~ (bool_expr | bool_expr_single | var | bool) ~ block } 89 | if_block = { "if" ~ (bool_expr | bool_expr_single | var | bool) ~ block ~ elif_block* ~ else_block? } 90 | elif_block = { "elif" ~ (bool_expr | bool_expr_single | var | bool) ~ block } 91 | else_block = { "else" ~ block } 92 | 93 | // Register interaction 94 | 95 | register = { "@" ~ reg_name } 96 | reg_name = { ASCII_ALPHA } 97 | 98 | // Variable operations 99 | 100 | var_ident = @{ (ASCII_ALPHANUMERIC | "_")+ } 101 | index = { expr } 102 | var_index = { var_ident ~ "[" ~ index ~ "]" } 103 | var_name = { var_index | var_ident } 104 | var = { "$" ~ var_name } 105 | var_declare = { "let" ~ var_name ~ "=" ~ expr } 106 | var_mut = { var_name ~ "=" ~ expr } 107 | var_add = { var_name ~ "+=" ~ (bin_expr | int | var) } 108 | var_sub = { var_name ~ "-=" ~ (bin_expr | int | var) } 109 | var_mult = { var_name ~ "*=" ~ (bin_expr | int | var) } 110 | var_div = { var_name ~ "/=" ~ (bin_expr | int | var) } 111 | var_pow = { var_name ~ "^=" ~ (bin_expr | int | var) } 112 | var_mod = { var_name ~ "%=" ~ (bin_expr | int | var) } 113 | 114 | // Function stuff 115 | 116 | func_def_args = { "(" ~ (var_name ~ ",")* ~ var_name? ~ ")" } 117 | func_name = { var_ident ~ func_def_args } 118 | func_def = { "def" ~ func_name ~ block } 119 | 120 | func_call_args = { "(" ~ ((expr) ~ ",")* ~ (expr)? ~ ")" } 121 | func_call = { var_ident ~ func_call_args } 122 | 123 | // Binary and Boolean expressions 124 | 125 | add = { "+" } 126 | sub = { "-" } 127 | mult = { "*" } 128 | div = { "/" } 129 | modulo = { "%" } 130 | pow = { "**" } 131 | range = { expr ~ ".." ~ expr } 132 | range_inclusive = { expr ~ "..=" ~ expr } 133 | unary_minus = { "-" } 134 | bin_op = _{ add | sub | mult | div | modulo | pow } 135 | bin_lit = { unary_minus? ~ (var | int) } 136 | bin_atom = { bin_lit | ("(" ~ bin_expr ~ ")") } 137 | bin_expr = { bin_atom ~ (bin_op ~ bin_atom)* } 138 | 139 | eq = { "==" } 140 | ne = { "!=" } 141 | lt = { "<" } 142 | le = { "<=" } 143 | gt = { ">" } 144 | ge = { ">=" } 145 | and = { "&&" } 146 | or = { "||" } 147 | not = { "!" } 148 | true_lit = { "true" } 149 | false_lit = { "false" } 150 | bool_conjunction = { and | or } 151 | bool_op = _{ eq | ne | lt | le | ge | gt } 152 | bool_lit = { not? ~ ("(" ~ bool_expr_single ~ ")" | value | pop_cmd | return_cmd | buf_cmd | func_call) } 153 | bool_expr_single = { (bool_lit ~ bool_op ~ bool_lit) | (not ~ bool_lit) | bool | "(" ~ bool_expr ~ ")" } 154 | bool_expr = { bool_expr_single ~ (bool_conjunction ~ (bool_expr_single | bool_lit))* } 155 | 156 | ternary = { expr_not_ternary ~ "?" ~ expr ~ ":" ~ expr } 157 | num_expr = { bin_expr | ternary | var | int } 158 | expr_not_ternary = { return_cmd | bin_expr | bool_expr | value } 159 | expr = { pop_cmd | return_cmd | buf_cmd | ternary | bool_expr | bin_expr | value | func_call | null } 160 | 161 | // Primitives 162 | 163 | inner = ${ (("\\\\") | ("\\" ~ "\"") | (!"\"" ~ ANY))* } 164 | atomic_string = @{ "\"" ~ inner ~ "\"" } 165 | literal = { ("\"" ~ inner ~ "\"") } 166 | bool = { true_lit | false_lit } 167 | int = @{ ASCII_DIGIT+ } 168 | array = { "[" ~ ((expr ~ ",")* ~ expr?)? ~ "]" } 169 | null = { "null" } 170 | regex_lit = { PUSH(regex_delim) ~ (&PEEK ~ PUSH(regex_delim))* ~ regex ~ POP_ALL } 171 | regex_delim = _{ !(ASCII_ALPHANUMERIC | "'" | "}" | "{" | "(" | ")" | "[" | "]") ~ ANY } 172 | regex = { (!PEEK_ALL ~ ANY)* } 173 | value = { array | null | var | int | literal | bool | register | regex_lit } 174 | 175 | vim_cmd = { expr } 176 | pattern = { literal | regex_lit | var } 177 | name_def = { "name" ~ "=" ~ "\"" ~ inner ~ "\"" } 178 | 179 | 180 | 181 | cmd = { 182 | var_declare 183 | | var_add 184 | | for_block 185 | | if_block 186 | | while_block 187 | | until_block 188 | | var_sub 189 | | var_mut 190 | | var_mult 191 | | var_div 192 | | var_pow 193 | | var_mod 194 | | include 195 | | alias 196 | | not_global_cmd 197 | | global_cmd 198 | | repeat_cmd 199 | | move_cmd 200 | | cut_cmd 201 | | yank_cmd 202 | | echo_cmd 203 | | push_cmd 204 | | pop_cmd 205 | | buf_cmd 206 | | return_cmd 207 | | func_def 208 | | func_call 209 | | next 210 | | break_loop 211 | | continue_loop 212 | } 213 | 214 | block = !{ "{" ~ cmd* ~ "}" } 215 | 216 | vic = { SOI ~ WHITESPACE* ~ prelude? ~ cmd* ~ WHITESPACE* ~ EOI } 217 | -------------------------------------------------------------------------------- /src/tests/golden_files/vicut_main_extracted_impl.rs: -------------------------------------------------------------------------------- 1 | impl Opts { 2 | /// Parse the user's arguments 3 | pub fn parse() -> Result { 4 | let mut new = Self::default(); 5 | let mut args = std::env::args().skip(1).peekable(); 6 | while let Some(arg) = args.next() { 7 | match arg.as_str() { 8 | "--json" | "-j" => { 9 | new.json = true; 10 | } 11 | "--trace" => { 12 | new.trace = true; 13 | } 14 | "--linewise" => { 15 | new.linewise = true; 16 | } 17 | "--serial" => { 18 | new.single_thread = true; 19 | } 20 | "--trim-fields" => { 21 | new.trim_fields = true; 22 | } 23 | "--keep-mode" => { 24 | new.keep_mode = true; 25 | } 26 | "--backup" => { 27 | new.backup_files = true; 28 | } 29 | "-i" => { 30 | new.edit_inplace = true; 31 | } 32 | "--template" | "-t" => { 33 | let Some(next_arg) = args.next() else { 34 | return Err(format!("Expected a format string after '{arg}'")) 35 | }; 36 | if next_arg.starts_with('-') { 37 | return Err(format!("Expected a format string after '{arg}', found {next_arg}")) 38 | } 39 | new.template = Some(next_arg) 40 | } 41 | "--delimiter" | "-d" => { 42 | let Some(next_arg) = args.next() else { continue }; 43 | if next_arg.starts_with('-') { 44 | return Err(format!("Expected a delimiter after '{arg}', found {next_arg}")) 45 | } 46 | new.delimiter = Some(next_arg) 47 | } 48 | "-n" | "--next" => new.cmds.push(Cmd::BreakGroup), 49 | "-r" | "--repeat" => { 50 | let cmd_count = args 51 | .next() 52 | .unwrap_or("1".into()) 53 | .parse::() 54 | .map_err(|_| format!("Expected a number after '{arg}'"))?; 55 | let repeat_count = args 56 | .next() 57 | .unwrap_or("1".into()) 58 | .parse::() 59 | .map_err(|_| format!("Expected a number after '{arg}'"))?; 60 | 61 | new.cmds.push(Cmd::Repeat(cmd_count, repeat_count)); 62 | } 63 | "-m" | "--move" => { 64 | let Some(arg) = args.next() else { continue }; 65 | if arg.starts_with('-') { 66 | return Err(format!("Expected a motion command after '-m', found {arg}")) 67 | } 68 | new.cmds.push(Cmd::Motion(arg)) 69 | } 70 | "-c" | "--cut" => { 71 | let Some(arg) = args.next() else { continue }; 72 | if arg.starts_with("name=") { 73 | let name = arg.strip_prefix("name=").unwrap().to_string(); 74 | if name == "0" { 75 | // We use '0' as a sentinel value to say "We didn't slice any fields, so this field is the entire buffer" 76 | // So we can't let people use it arbitrarily, or weird shit starts happening 77 | return Err("Field name '0' is a reserved field name.".into()) 78 | } 79 | let Some(arg) = args.next() else { continue }; 80 | if arg.starts_with('-') { 81 | return Err(format!("Expected a selection command after '-c', found {arg}")) 82 | } 83 | new.cmds.push(Cmd::NamedField(name,arg)); 84 | } else { 85 | if arg.starts_with('-') { 86 | return Err(format!("Expected a selection command after '-c', found {arg}")) 87 | } 88 | new.cmds.push(Cmd::Field(arg)); 89 | } 90 | } 91 | "-v" | "--not-global" | 92 | "-g" | "--global" => { 93 | let global = new.handle_global_arg(arg.as_str(), &mut args); 94 | new.cmds.push(global); 95 | } 96 | _ => new.handle_filename(arg) 97 | } 98 | } 99 | Ok(new) 100 | } 101 | /// Handles `-g` and `-v` global conditionals. 102 | /// 103 | /// `-g` and `-v` are special cases: each introduces a scoped block of commands 104 | /// that will only execute if a pattern match (or non-match) succeeds. These blocks 105 | /// can contain other nested `-g` or `-v` invocations, as well as `--else` branches 106 | /// and `-r` repeats. 107 | /// 108 | /// Because of this recursive structure, we use a recursive descent parser to 109 | /// build a nested command execution tree from the input. This allows arbitrarily 110 | /// deep combinations of conditionals and scopes, like: 111 | /// 112 | /// ```bash 113 | /// vicut -g 'foo' -g 'bar' -c 'd' --else -v 'baz' -c 'y' --end --end 114 | /// ``` 115 | fn handle_global_arg(&mut self,arg: &str, args: &mut Peekable>) -> Cmd { 116 | let polarity = match arg { 117 | "-v" | "--not-global" => false, 118 | "-g" | "--global" => true, 119 | _ => unreachable!("found arg: {arg}") 120 | }; 121 | let mut then_cmds = vec![]; 122 | let mut else_cmds = None; 123 | let Some(arg) = args.next() else { 124 | return Cmd::Global { 125 | pattern: arg.into(), 126 | then_cmds, 127 | else_cmds, 128 | polarity 129 | }; 130 | }; 131 | if arg.starts_with('-') { 132 | eprintln!("Expected a selection command after '-c', found {arg}"); 133 | std::process::exit(1) 134 | } 135 | while let Some(global_arg) = args.next() { 136 | match global_arg.as_str() { 137 | "-n" | "--next" => self.cmds.push(Cmd::BreakGroup), 138 | "-r" | "--repeat" => { 139 | let cmd_count = args 140 | .next() 141 | .unwrap_or("1".into()) 142 | .parse::() 143 | .unwrap_or_else(|_| { 144 | eprintln!("Expected a number after '{global_arg}'"); 145 | std::process::exit(1) 146 | }); 147 | let repeat_count = args 148 | .next() 149 | .unwrap_or("1".into()) 150 | .parse::() 151 | .unwrap_or_else(|_| { 152 | eprintln!("Expected a number after '{global_arg}'"); 153 | std::process::exit(1) 154 | }); 155 | 156 | if let Some(else_cmds) = else_cmds.as_mut() { 157 | else_cmds.push(Cmd::Repeat(cmd_count, repeat_count)); 158 | } else { 159 | then_cmds.push(Cmd::Repeat(cmd_count, repeat_count)); 160 | } 161 | } 162 | "-m" | "--move" => { 163 | let Some(arg) = args.next() else { continue }; 164 | if arg.starts_with('-') { 165 | eprintln!("Expected a motion command after '-m', found {arg}"); 166 | std::process::exit(1); 167 | } 168 | if let Some(else_cmds) = else_cmds.as_mut() { 169 | else_cmds.push(Cmd::Motion(arg)) 170 | } else { 171 | then_cmds.push(Cmd::Motion(arg)) 172 | } 173 | } 174 | "-c" | "--cut" => { 175 | let Some(arg) = args.next() else { continue }; 176 | if arg.starts_with("name=") { 177 | let name = arg.strip_prefix("name=").unwrap().to_string(); 178 | let Some(arg) = args.next() else { continue }; 179 | if arg.starts_with('-') { 180 | eprintln!("Expected a selection command after '-c', found {arg}"); 181 | std::process::exit(1); 182 | } 183 | if let Some(cmds) = else_cmds.as_mut() { 184 | cmds.push(Cmd::NamedField(name,arg)); 185 | } else { 186 | then_cmds.push(Cmd::NamedField(name,arg)); 187 | } 188 | } else { 189 | if arg.starts_with('-') { 190 | eprintln!("Expected a selection command after '-c', found {arg}"); 191 | std::process::exit(1); 192 | } 193 | if let Some(cmds) = else_cmds.as_mut() { 194 | cmds.push(Cmd::Field(arg)); 195 | } else { 196 | then_cmds.push(Cmd::Field(arg)); 197 | } 198 | } 199 | } 200 | "-g" | "--global" | 201 | "-v" | "--not-global" => { 202 | let nested = self.handle_global_arg(&global_arg, args); 203 | if let Some(cmds) = else_cmds.as_mut() { 204 | cmds.push(nested); 205 | } else { 206 | then_cmds.push(nested); 207 | } 208 | } 209 | "--else" => { 210 | // Now we start working on this 211 | else_cmds = Some(vec![]); 212 | } 213 | "--end" => { 214 | // We're done here 215 | return Cmd::Global { 216 | pattern: arg, 217 | then_cmds, 218 | else_cmds, 219 | polarity 220 | }; 221 | } 222 | _ => { 223 | eprintln!("Expected command flag in '-g' scope\nDid you forget to close '-g' with '--end'?"); 224 | std::process::exit(1); 225 | } 226 | } 227 | if args.peek().is_some_and(|arg| !arg.starts_with('-')) { break } 228 | } 229 | 230 | // If we got here, we have run out of arguments 231 | // Let's just submit the current -g commands. 232 | // no need to be pressed about a missing '--end' when nothing would come after it 233 | Cmd::Global { 234 | pattern: arg, 235 | then_cmds, 236 | else_cmds, 237 | polarity 238 | } 239 | } 240 | /// Handle a filename passed as an argument. 241 | /// 242 | /// Checks to make sure the following invariants are met: 243 | /// 1. The path given exists. 244 | /// 2. The path given refers to a file. 245 | /// 3. The path given refers to a file that we are allowed to read. 246 | /// 247 | /// We check all three separately instead of just the last one, so that we can give better error messages 248 | fn handle_filename(&mut self, filename: String) { 249 | let path = PathBuf::from(filename.trim().to_string()); 250 | if !path.exists() { 251 | eprintln!("vicut: file not found '{}'",path.display()); 252 | std::process::exit(1); 253 | } 254 | if !path.is_file() { 255 | eprintln!("vicut: '{}' is not a file",path.display()); 256 | std::process::exit(1); 257 | } 258 | if fs::File::open(&path).is_err() { 259 | eprintln!("vicut: failed to read file '{}'",path.display()); 260 | std::process::exit(1); 261 | } 262 | if !self.files.contains(&path) { 263 | self.files.push(path) 264 | } 265 | } 266 | } 267 | 268 | -------------------------------------------------------------------------------- /src/tests/editor.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::{normal_cmd, LOREM_IPSUM}; 2 | use pretty_assertions::assert_eq; 3 | 4 | 5 | #[test] 6 | fn editor_delete_word() { 7 | assert_eq!(normal_cmd( 8 | "dw", 9 | "The quick brown fox jumps over the lazy dog", 10 | 16), 11 | ("The quick brown jumps over the lazy dog".into(), 16) 12 | ); 13 | } 14 | 15 | #[test] 16 | fn editor_delete_backwards() { 17 | assert_eq!(normal_cmd( 18 | "2db", 19 | "The quick brown fox jumps over the lazy dog", 20 | 16), 21 | ("The fox jumps over the lazy dog".into(), 4) 22 | ); 23 | } 24 | 25 | #[test] 26 | fn editor_rot13_five_words_backwards() { 27 | assert_eq!(normal_cmd( 28 | "g?5b", 29 | "The quick brown fox jumps over the lazy dog", 30 | 31), 31 | ("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4) 32 | ); 33 | } 34 | 35 | #[test] 36 | fn editor_delete_word_on_whitespace() { 37 | assert_eq!(normal_cmd( 38 | "dw", 39 | "The quick brown fox", 40 | 10), //on the whitespace between "quick" and "brown" 41 | ("The quick brown fox".into(), 10) 42 | ); 43 | } 44 | 45 | #[test] 46 | fn editor_delete_5_words() { 47 | assert_eq!(normal_cmd( 48 | "5dw", 49 | "The quick brown fox jumps over the lazy dog", 50 | 16,), 51 | ("The quick brown dog".into(), 16) 52 | ); 53 | } 54 | 55 | #[test] 56 | fn editor_delete_end_includes_last() { 57 | assert_eq!(normal_cmd( 58 | "de", 59 | "The quick brown fox::::jumps over the lazy dog", 60 | 16), 61 | ("The quick brown ::::jumps over the lazy dog".into(), 16) 62 | ); 63 | } 64 | 65 | #[test] 66 | fn editor_delete_end_unicode_word() { 67 | assert_eq!(normal_cmd( 68 | "de", 69 | "naïve café world", 70 | 0), 71 | (" café world".into(), 0) 72 | ); 73 | } 74 | 75 | #[test] 76 | fn editor_inplace_edit_cursor_position() { 77 | assert_eq!(normal_cmd( 78 | "5~", 79 | "foobar", 80 | 0), 81 | ("FOOBAr".into(), 4) 82 | ); 83 | assert_eq!(normal_cmd( 84 | "5rg", 85 | "foobar", 86 | 0), 87 | ("gggggr".into(), 4) 88 | ); 89 | } 90 | 91 | #[test] 92 | fn editor_insert_mode_not_clamped() { 93 | assert_eq!(normal_cmd( 94 | "a", 95 | "foobar", 96 | 5), 97 | ("foobar".into(), 6) 98 | ) 99 | } 100 | 101 | #[test] 102 | fn editor_overshooting_motions() { 103 | assert_eq!(normal_cmd( 104 | "5dw", 105 | "foo bar", 106 | 0), 107 | ("".into(), 0) 108 | ); 109 | assert_eq!(normal_cmd( 110 | "3db", 111 | "foo bar", 112 | 0), 113 | ("foo bar".into(), 0) 114 | ); 115 | assert_eq!(normal_cmd( 116 | "3dj", 117 | "foo bar", 118 | 0), 119 | ("foo bar".into(), 0) 120 | ); 121 | assert_eq!(normal_cmd( 122 | "3dk", 123 | "foo bar", 124 | 0), 125 | ("foo bar".into(), 0) 126 | ); 127 | } 128 | 129 | #[test] 130 | fn editor_textobj_quoted() { 131 | assert_eq!(normal_cmd( 132 | "di\"", 133 | "this buffer has \"some \\\"quoted\" text", 134 | 0), 135 | ("this buffer has \"\" text".into(), 17) 136 | ); 137 | assert_eq!(normal_cmd( 138 | "da\"", 139 | "this buffer has \"some \\\"quoted\" text", 140 | 0), 141 | ("this buffer has text".into(), 16) 142 | ); 143 | assert_eq!(normal_cmd( 144 | "di'", 145 | "this buffer has 'some \\'quoted' text", 146 | 0), 147 | ("this buffer has '' text".into(), 17) 148 | ); 149 | assert_eq!(normal_cmd( 150 | "da'", 151 | "this buffer has 'some \\'quoted' text", 152 | 0), 153 | ("this buffer has text".into(), 16) 154 | ); 155 | assert_eq!(normal_cmd( 156 | "di`", 157 | "this buffer has `some \\`quoted` text", 158 | 0), 159 | ("this buffer has `` text".into(), 17) 160 | ); 161 | assert_eq!(normal_cmd( 162 | "da`", 163 | "this buffer has `some \\`quoted` text", 164 | 0), 165 | ("this buffer has text".into(), 16) 166 | ); 167 | } 168 | 169 | #[test] 170 | fn editor_textobj_delimited() { 171 | assert_eq!(normal_cmd( 172 | "di)", 173 | "this buffer has (some \\(\\)(inner) \\(\\)delimited) text", 174 | 0), 175 | ("this buffer has () text".into(), 17) 176 | ); 177 | assert_eq!(normal_cmd( 178 | "da)", 179 | "this buffer has (some \\(\\)(inner) \\(\\)delimited) text", 180 | 0), 181 | ("this buffer has text".into(), 16) 182 | ); 183 | assert_eq!(normal_cmd( 184 | "di]", 185 | "this buffer has [some \\[\\][inner] \\[\\]delimited] text", 186 | 0), 187 | ("this buffer has [] text".into(), 17) 188 | ); 189 | assert_eq!(normal_cmd( 190 | "da]", 191 | "this buffer has [some \\[\\][inner] \\[\\]delimited] text", 192 | 0), 193 | ("this buffer has text".into(), 16) 194 | ); 195 | assert_eq!(normal_cmd( 196 | "di}", 197 | "this buffer has {some \\{\\}{inner} \\{\\}delimited} text", 198 | 0), 199 | ("this buffer has {} text".into(), 17) 200 | ); 201 | assert_eq!(normal_cmd( 202 | "da}", 203 | "this buffer has {some \\{\\}{inner} \\{\\}delimited} text", 204 | 0), 205 | ("this buffer has text".into(), 16) 206 | ); 207 | assert_eq!(normal_cmd( 208 | "di>", 209 | "this buffer has \\<\\>delimited> text", 210 | 0), 211 | ("this buffer has <> text".into(), 17) 212 | ); 213 | assert_eq!(normal_cmd( 214 | "da>", 215 | "this buffer has \\<\\>delimited> text", 216 | 0), 217 | ("this buffer has text".into(), 16) 218 | ); 219 | } 220 | 221 | #[test] 222 | fn editor_delete_line_up() { 223 | assert_eq!(normal_cmd( 224 | "dk", 225 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 226 | 239), 227 | ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 242,) 228 | ) 229 | } 230 | 231 | #[test] 232 | fn editor_sentence_operations() { 233 | assert_eq!(normal_cmd( 234 | "d)", 235 | LOREM_IPSUM, 236 | 0), 237 | ("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 0) 238 | ); 239 | assert_eq!(normal_cmd( 240 | "5d)", 241 | LOREM_IPSUM, 242 | 0), 243 | ("Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 0) 244 | ); 245 | assert_eq!(normal_cmd( 246 | "d5)", 247 | LOREM_IPSUM, 248 | 0), 249 | ("Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 0) 250 | ); 251 | assert_eq!(normal_cmd( 252 | "d)", 253 | "This sentence has some closing delimiters after it.)]'\" And this is another sentence.", 254 | 0), 255 | ("And this is another sentence.".into(), 0) 256 | ); 257 | assert_eq!(normal_cmd( 258 | "d3)", 259 | LOREM_IPSUM, 260 | 232), 261 | ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 232) 262 | ); 263 | assert_eq!(normal_cmd( 264 | "d2(", 265 | LOREM_IPSUM, 266 | 335), 267 | ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 124) 268 | ); 269 | assert_eq!(normal_cmd( 270 | "dis", 271 | LOREM_IPSUM, 272 | 257), 273 | ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 232) 274 | ); 275 | assert_eq!(normal_cmd( 276 | "das", 277 | LOREM_IPSUM, 278 | 257), 279 | ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 232) 280 | ); 281 | } 282 | 283 | //"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." 284 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vicut 2 | 3 | `vicut` is a Vim-based, scriptable, headless text editor for the command line. 4 | 5 | ![vicut](https://github.com/user-attachments/assets/3e0e9e10-cc41-4203-9302-9965a0e42893) 6 | 7 | 8 | It combines the power of Vim motions/operators with the general-use applicability of command line tools like `sed`, `awk`, and `cut`. `vicut` can be used to extract fields, edit text files in-place, apply global substitutions, and more. 9 | 10 | ## Why vicut? 11 | I'm fluent with Vim and often find myself wishing I could use its expressive editing features **outside** the interactive editor — especially when writing shell scripts. Tools like `awk`, `sed`, and `cut` are powerful for formatting command output and extracting fields, but I’ve lost count of how many times I’ve thought: 12 | > *"This would be way easier if I could just ask Vim to do it."* 13 | 14 | So I decided to repurpose [my shell](https://github.com/km-clay/fern)'s line editor into a CLI tool that can process files and input streams using Vim commands. 15 | 16 | ## Features 17 | 18 | ### 🔍 Core Editing 19 | 20 | * Apply Vim-style editing commands (e.g. `:%s/foo/bar/g`, `d2W`, `ci}`) to one or more files, or stdin. 21 | * Use Vim motions to extract or modify structured data. 22 | * Perform in-place file edits with `-i`, or print to stdout by default. 23 | 24 | ### 📦 Flexible Output 25 | 26 | * Output in plain text, JSON (`--json`), or interpolate fields using a format string (`--template`). 27 | * Chain multiple editing/extraction commands. 28 | * Capture multiple fields using `-c`, and structure them using `-n` and `--delimiter`. 29 | 30 | ### ⚡ High Performance 31 | 32 | * Use `--linewise` for stream-style processing like `sed`, but multi-threaded. 33 | * Combine regex pattern matching with Vim-style editing in a single tool. 34 | 35 | ## ⚙️ Usage 36 | 37 | `vicut` uses an internal text editing engine based on Vim. File names can be given as arguments, or text can be given using stdin. There are six command flags you can use to issue commands to the internal editor. 38 | 39 | * `-c`/`--cut ` executes a Vim command (something like `5w`, `vi)`, `:%s/foo/bar/g`, etc) and returns the span of text covered by the cursor's motion as a field. Any arbitrary number of fields can be extracted using `-c`. If no `-c` commands are given, `vicut` will print the entire buffer as a single field. 40 | * `-m`/`--move ` silently executes a Vim command. `-m` does not extract a field from the buffer like `-c` does, making it ideal for positioning the cursor before `-c` calls, or making edits to the buffer. 41 | * `-r`/`--repeat ` repeats `N` previous commands `R` times. Repeats can be logically nested. 42 | * `-n`/`--next` concludes the current 'field group' and starts a new one. Each field group is printed as a separate record in the output, or as a separate JSON object if using `--json` 43 | * `-g`/`--global ` allows for conditional execution of command flags. Any command flags following `-g` will only execute on lines that match the pattern given after `-g`. Fallback commands can be given using the `--else` flag. You can return from the `-g` scope with the `--exit` flag, which will allow you to continue writing unconditional commands. For the purpose of repetition with `-r`, the entire `-g` block counts as a single command to be repeated. 44 | * `-v`/`--not-global ` same behavior as `-g`, except it executes the contained command flags on lines that *don't* match the given pattern. 45 | 46 | Command flags can be given any number of times, and the commands are executed in order of appearance. 47 | 48 | ### Output Format Options 49 | 50 | Output can be structured in three different ways using these options: 51 | * `-j`/`--json` emits the extracted field data as a json object, ready to be piped into other programs, such as `jq` 52 | * `-d`/`--delimiter ` lets you give a field separator as an argument to the flag. The separator is placed inbetween each field in each record. 53 | * `-t`/`--template ` lets you define a custom output format using a format string. Fields are interpolated on placeholders that look like `{{1}}` or `{{field_name}}`. 54 | 55 | ### Execution Behavior Options 56 | 57 | * `-i` If you have given files as arguments to read from, the `-i` flag will make `vicut` edit the contents of those files in-place. This is an atomic operation, meaning changes will only be written to the files if all operations succeed. 58 | * `--backup` If the `-i` option has been set, this will create a backup of the files to be edited. 59 | * `--backup-extension` Allows you to set an arbitrary file extension to use for the backups. Default is `.bak` 60 | * `--keep-mode` The internal editor always returns to Normal mode after each call to `-m` or `-c`. This flag prevents that behavior, and causes the internal editor's mode to persist between calls. 61 | * `--linewise` Makes `vicut` treat each line of text in the input as a separate buffer. The sequence of commands you give to `vicut` will be applied to every line. This operation utilizes multi-threading to operate on lines in parallel, making it far faster than full buffer editing. 62 | * `--serial` Makes `--linewise` mode operate on each line sequentially instead of using multi-threading. 63 | * `--jobs` Restricts the number of threads `--linewise` can create for operating on lines. 64 | * `--trim-fields` Trims leading and trailing whitespace from fields extracted by `-c`. 65 | 66 | #### ℹ️ Examples and in-depth usage ideas can be found on the [wiki](https://github.com/km-clay/vicut/wiki) 67 | 68 | ## 🚀 Performance 69 | While tools like `awk` and `sed` do beat `vicut` in speed for full-buffer processing, `vicut`'s `--linewise` mode emulates the stream processing behaviors of `awk` and `sed` by treating each line of input as an independent buffer, and processing each in parallel. This allows `vicut`'s performance to scale horizontally across CPU cores, giving it an edge over traditional Unix text processors for non-trivial inputs— even while executing more semantically rich operations like Vim motions. 70 | 71 | On structured input, execution speed of `vicut` in `--linewise` mode is comparable to or faster than the speeds of `sed` and `awk` on datasets up to 1 million lines. 72 | Here's a benchmark using a generated data set that looks like this: 73 | ``` 74 | 00001) Provider-1 (City-1, State-1) [924.05 km] 75 | 00002) Provider-2 (City-2, State-2) [593.91 km] 76 | 00003) Provider-3 (City-3, State-3) [306.39 km] 77 | 00004) Provider-4 (City-4, State-4) [578.94 km] 78 | 00005) Provider-5 (City-5, State-5) [740.13 km] 79 | ... 80 | ``` 81 | With the target output being: 82 | ``` 83 | 00001 ---- Provider-1 ---- City-1, State-1 ---- 924.05 km 84 | 00002 ---- Provider-2 ---- City-2, State-2 ---- 593.91 km 85 | 00003 ---- Provider-3 ---- City-3, State-3 ---- 306.39 km 86 | 00004 ---- Provider-4 ---- City-4, State-4 ---- 578.94 km 87 | 00005 ---- Provider-5 ---- City-5, State-5 ---- 740.13 km 88 | ... 89 | ``` 90 | ### 25,000 lines 91 | | Tool | Command | Wall-Clock Time | 92 | | ------- | -------------------------------------------------------------------------------------- | --------------- | 93 | | `sed` | `sed -E -e 's/[][]//g' -e 's/(\) \| \()/ ---- /g'` | 20.0ms | 94 | | `awk` | `awk -vOFS=" --- " -F'[][()]' '{ print $1, $2, $3, " " $5 }'` | 13.7ms | 95 | | `vicut` | `vicut --linewise --delimiter ' ---- ' -c 'e' -m '2w' -c 't(h' -c 'vi)' -c 'vi]'` | 11.9ms | 96 | 97 | 98 | ### 100,000 lines 99 | | Tool | Command | Wall-Clock Time | 100 | | ------- | -------------------------------------------------------------------------------------- | --------------- | 101 | | `sed` | `sed -E -e 's/[][]//g' -e 's/(\) \| \()/ ---- /g'` | 76.0ms | 102 | | `awk` | `awk -vOFS=" ---- " -F'[][()]' '{ print $1, $2, $3, " " $5 }'` | 51.6ms | 103 | | `vicut` | `vicut --linewise --delimiter ' ---- ' -c 'e' -m '2w' -c 't(h' -c 'vi)' -c 'vi]'` | 35.2ms | 104 | 105 | ### 1,000,000 lines 106 | | Tool | Command | Wall-Clock Time | 107 | | ------- | -------------------------------------------------------------------------------------- | --------------- | 108 | | `sed` | `sed -E -e 's/[][]//g' -e 's/(\) \| \()/ ---- /g'` | 756.4ms | 109 | | `awk` | `awk -vOFS=" ---- " -F'[][()]' '{ print $1, $2, $3, " " $5 }'` | 499.1ms | 110 | | `vicut` | `vicut --linewise --delimiter ' ---- ' -c 'e' -m '2w' -c 't(h' -c 'vi)' -c 'vi]'` | 296.0ms | 111 | 112 | *Benchmark recorded using an AMD Ryzen 9 9950X (16-Core) running Arch Linux* 113 | 114 | The command used to generate the datasets was this, if you want to reproduce these benchmarks at home: 115 | `seq -w 1 1000000 | awk 'BEGIN { OFMT="%.2f" } { printf "%05d) Provider-%d (City-%d, State-%d) [%.2f km]\n", $1, $1, $1, $1, rand()*1000 }' > providers.txt` 116 | 117 | --- 118 | 119 | ## 📦 Installation 120 | 121 | > **Note:** Building requires the [Rust toolchain](https://rustup.rs), which includes the `rustc` compiler and the `cargo` package manager. 122 | 123 | --- 124 | 125 | ### Install via Cargo (Recommended) 126 | 127 | If you have Rust installed, you can install `vicut` directly from [crates.io](https://crates.io/crates/vicut) using: 128 | 129 | ```bash 130 | cargo install vicut 131 | ``` 132 | 133 | This will install the `vicut` binary to `~/.cargo/bin` — make sure that directory is in your `$PATH`. 134 | 135 | --- 136 | 137 | ### Build from Source 138 | 139 | Alternatively, you can build `vicut` manually: 140 | 141 | 1. Clone the repository and navigate into it: 142 | 143 | ```bash 144 | git clone https://github.com/km-clay/vicut 145 | cd vicut 146 | ``` 147 | 148 | 2. Build the binary: 149 | 150 | ```bash 151 | cargo build --release 152 | ``` 153 | 154 | 3. Install it to a directory in your `$PATH`: 155 | 156 | ```bash 157 | install -Dm755 target/release/vicut ~/.local/bin 158 | ``` 159 | Here's a one-liner for all that: 160 | 161 | ```bash 162 | (git clone https://github.com/km-clay/vicut && \ 163 | cd vicut && \ 164 | cargo build --release && \ 165 | mkdir -p ~/.local/bin && \ 166 | install -Dm755 target/release/vicut ~/.local/bin && \ 167 | echo -e "\nInstalled the binary to ~/.local/bin — make sure that is in your \$PATH") 168 | ``` 169 | 170 | --- 171 | 172 | ## Notes 173 | 174 | `vicut` is experimental and still in early development. The core functionality is stable and usable, but many of Vim's more obscure motions and operators are not yet supported. The logic for executing the Vim commands is entirely home-grown, so there may be some small inconsistencies between Vim and vicut. The internal editor logic is adapted from the line editor I wrote for [`fern`](https://github.com/km-clay/fern), so some remnants of that may still appear in the codebase. Any and all contributions are welcome. 175 | -------------------------------------------------------------------------------- /src/modes/ex.rs: -------------------------------------------------------------------------------- 1 | use std::{iter::Peekable, path::PathBuf, str::Chars}; 2 | 3 | use bitflags::bitflags; 4 | use itertools::Itertools; 5 | 6 | use crate::{exec::Val, modes::{common_cmds, ModeReport, ViMode}, vicmd::{Anchor, CmdFlags, LineAddr, Motion, MotionCmd, ReadSrc, RegisterName, Verb, VerbCmd, ViCmd, WriteDest}}; 7 | 8 | bitflags! { 9 | #[derive(Debug,Clone,Copy,PartialEq,Eq)] 10 | pub struct SubFlags: u16 { 11 | const GLOBAL = 1 << 0; // g 12 | const CONFIRM = 1 << 1; // c (probably not implemented) 13 | const IGNORE_CASE = 1 << 2; // i 14 | const NO_IGNORE_CASE = 1 << 3; // I 15 | const SHOW_COUNT = 1 << 4; // n 16 | const PRINT_RESULT = 1 << 5; // p 17 | const PRINT_NUMBERED = 1 << 6; // # 18 | const PRINT_LEFT_ALIGN = 1 << 7; // l 19 | } 20 | } 21 | 22 | #[derive(Clone,Debug)] 23 | pub struct ViEx { 24 | pending_cmd: String, 25 | select_range: Option<(usize,usize)>, 26 | } 27 | 28 | impl ViEx { 29 | pub fn new(select_range: Option<(usize,usize)>) -> Self { 30 | Self { 31 | pending_cmd: Default::default(), 32 | select_range 33 | } 34 | } 35 | } 36 | 37 | impl ViMode for ViEx { 38 | // Ex mode can return errors, so we use this fallible method instead of the normal one 39 | fn handle_key_fallible(&mut self, key: crate::keys::KeyEvent) -> Result,String> { 40 | use crate::keys::{KeyEvent as E, KeyCode as C, ModKeys as M}; 41 | match key { 42 | E(C::Char('\r'), M::NONE) | 43 | E(C::Enter, M::NONE) => { 44 | match parse_ex_cmd(&self.pending_cmd, self.select_range) { 45 | Ok(cmd) => Ok(cmd), 46 | Err(e) => { 47 | let e = e.unwrap_or(format!("Not an editor command: {}",&self.pending_cmd)); 48 | Err(e) 49 | } 50 | } 51 | } 52 | E(C::Esc, M::NONE) => { 53 | Ok(Some(ViCmd { 54 | register: RegisterName::default(), 55 | verb: Some(VerbCmd(1, Verb::NormalMode)), 56 | motion: None, 57 | flags: CmdFlags::empty(), 58 | raw_seq: "".into(), 59 | })) 60 | } 61 | E(C::Char(ch), M::NONE) => { 62 | self.pending_cmd.push(ch); 63 | Ok(None) 64 | } 65 | _ => Ok(common_cmds(key)) 66 | } 67 | } 68 | fn handle_key(&mut self, key: crate::keys::KeyEvent) -> Option { 69 | self.handle_key_fallible(key).ok().flatten() 70 | } 71 | fn is_repeatable(&self) -> bool { 72 | false 73 | } 74 | 75 | fn as_replay(&self) -> Option { 76 | None 77 | } 78 | 79 | fn cursor_style(&self) -> String { 80 | "\x1b[2 q".to_string() 81 | } 82 | 83 | fn pending_seq(&self) -> Option { 84 | Some(self.pending_cmd.clone()) 85 | } 86 | 87 | fn move_cursor_on_undo(&self) -> bool { 88 | false 89 | } 90 | 91 | fn clamp_cursor(&self) -> bool { 92 | true 93 | } 94 | 95 | fn hist_scroll_start_pos(&self) -> Option { 96 | None 97 | } 98 | 99 | fn report_mode(&self) -> super::ModeReport { 100 | ModeReport::Ex 101 | } 102 | } 103 | 104 | fn get_path(path: &str) -> PathBuf { 105 | if let Some(stripped) = path.strip_prefix("~/") { 106 | if let Some(home) = std::env::var_os("HOME") { 107 | return PathBuf::from(home).join(stripped) 108 | } 109 | } 110 | if path == "~" { 111 | if let Some(home) = std::env::var_os("HOME") { 112 | return PathBuf::from(home) 113 | } 114 | } 115 | PathBuf::from(path) 116 | } 117 | 118 | 119 | fn parse_ex_cmd(raw: &str, select_range: Option<(usize,usize)>) -> Result,Option> { 120 | let raw = raw.trim(); 121 | if raw.is_empty() { 122 | return Ok(None) 123 | } 124 | let mut chars = raw.chars().peekable(); 125 | let mut motion = if let Some(motion) = parse_ex_address(&mut chars)?.map(|m| MotionCmd(1, m)) { 126 | Some(motion) 127 | } else { 128 | select_range.map(|range| MotionCmd(1,Motion::LineRange(LineAddr::Number(range.0),LineAddr::Number(range.1)))) 129 | }; 130 | let verb = { 131 | if chars.peek() == Some(&'g') { 132 | let mut cmd_name = String::new(); 133 | while let Some(ch) = chars.peek() { 134 | if ch.is_alphanumeric() { 135 | cmd_name.push(*ch); 136 | chars.next(); 137 | } else { 138 | break 139 | } 140 | } 141 | if !"global".starts_with(&cmd_name) { 142 | return Err(None) 143 | } 144 | let Some(result) = parse_global(&mut chars,motion.as_ref().map(|mcmd| &mcmd.1))? else { return Ok(None) }; 145 | motion = Some(MotionCmd(1,result.0)); 146 | Some(VerbCmd(1,result.1)) 147 | } else { 148 | parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)) 149 | } 150 | }; 151 | if motion.is_none() && !matches!(verb, Some(VerbCmd(_,Verb::Write(_)))) { 152 | motion = Some(MotionCmd(1,Motion::Line(LineAddr::Current))) 153 | } 154 | 155 | Ok(Some(ViCmd { 156 | register: RegisterName::default(), 157 | verb, 158 | motion, 159 | raw_seq: raw.to_string(), 160 | flags: CmdFlags::EXIT_CUR_MODE, 161 | })) 162 | } 163 | 164 | fn parse_ex_address(chars: &mut Peekable>) -> Result,Option> { 165 | if chars.peek() == Some(&'%') { 166 | chars.next(); 167 | return Ok(Some(Motion::LineRange(LineAddr::Number(1),LineAddr::Last))) 168 | } 169 | let mut chars_clone = chars.clone(); 170 | let Some(start) = parse_one_addr(&mut chars_clone)? else { return Ok(None) }; 171 | if let Some(&',') = chars_clone.peek() { 172 | chars_clone.next(); 173 | let Some(end) = parse_one_addr(&mut chars_clone)? else { return Ok(Some(Motion::Line(start))) }; 174 | *chars = chars_clone; 175 | Ok(Some(Motion::LineRange(start, end))) 176 | } else { 177 | *chars = chars_clone; 178 | Ok(Some(Motion::Line(start))) 179 | } 180 | } 181 | 182 | fn parse_one_addr(chars: &mut Peekable>) -> Result,Option> { 183 | let Some(first) = chars.next() else { return Ok(None) }; 184 | match first { 185 | '0'..='9' => { 186 | let mut digits = String::new(); 187 | digits.push(first); 188 | digits.extend(chars.peeking_take_while(|c| c.is_ascii_digit())); 189 | 190 | let number = digits.parse::() 191 | .map_err(|_| None)?; 192 | 193 | Ok(Some(LineAddr::Number(number))) 194 | } 195 | '+' | '-' => { 196 | let mut digits = String::new(); 197 | digits.push(first); 198 | digits.extend(chars.peeking_take_while(|c| c.is_ascii_digit())); 199 | 200 | let number = digits.parse::() 201 | .map_err(|_| None)?; 202 | 203 | Ok(Some(LineAddr::Offset(number))) 204 | } 205 | '/' | '?' => { 206 | let mut pattern = String::new(); 207 | while let Some(ch) = chars.next() { 208 | match ch { 209 | '\\' => { 210 | pattern.push('\\'); 211 | if let Some(esc_ch) = chars.next() { 212 | pattern.push(esc_ch) 213 | } 214 | } 215 | _ if ch == first => break, 216 | _ => pattern.push(ch) 217 | } 218 | } 219 | match first { 220 | '/' => Ok(Some(LineAddr::Pattern(pattern))), 221 | '?' => Ok(Some(LineAddr::PatternRev(pattern))), 222 | _ => unreachable!() 223 | } 224 | 225 | } 226 | '.' => Ok(Some(LineAddr::Current)), 227 | '$' => Ok(Some(LineAddr::Last)), 228 | _ => Ok(None) 229 | } 230 | } 231 | 232 | /// Unescape shell command arguments 233 | fn unescape_shell_cmd(cmd: &str) -> String { 234 | // The pest grammar uses double quotes for vicut commands 235 | // So shell commands need to escape double quotes 236 | // We will be removing a single layer of escaping from double quotes 237 | let mut result = String::new(); 238 | let mut chars = cmd.chars().peekable(); 239 | while let Some(ch) = chars.next() { 240 | if ch == '\\' { 241 | if let Some(&'"') = chars.peek() { 242 | chars.next(); 243 | result.push('"'); 244 | } else { 245 | result.push(ch); 246 | } 247 | } else { 248 | result.push(ch); 249 | } 250 | } 251 | result 252 | } 253 | 254 | fn parse_ex_command(chars: &mut Peekable>) -> Result,Option> { 255 | let mut cmd_name = String::new(); 256 | 257 | while let Some(ch) = chars.peek() { 258 | if ch == &'!' { 259 | cmd_name.push(*ch); 260 | chars.next(); 261 | break 262 | } else if !ch.is_alphanumeric() { 263 | break 264 | } 265 | cmd_name.push(*ch); 266 | chars.next(); 267 | } 268 | 269 | match cmd_name.as_str() { 270 | "!" => { 271 | let cmd = chars.collect::(); 272 | let cmd = unescape_shell_cmd(&cmd); 273 | Ok(Some(Verb::ShellCmd(cmd))) 274 | } 275 | "normal!" => parse_normal(chars), 276 | _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), 277 | _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), 278 | _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), 279 | _ if "read".starts_with(&cmd_name) => parse_read(chars), 280 | _ if "write".starts_with(&cmd_name) => parse_write(chars), 281 | _ if "substitute".starts_with(&cmd_name) => parse_substitute(chars), 282 | _ => Err(None) 283 | } 284 | } 285 | 286 | fn parse_normal(chars: &mut Peekable>) -> Result,Option> { 287 | chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); 288 | 289 | let seq: String = chars.collect(); 290 | Ok(Some(Verb::Normal(seq))) 291 | } 292 | 293 | fn parse_read(chars: &mut Peekable>) -> Result,Option> { 294 | chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); 295 | 296 | let is_shell_read = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; 297 | let arg: String = chars.collect(); 298 | 299 | if arg.trim().is_empty() { 300 | return Err(Some("Expected file path or shell command after ':r'".into())) 301 | } 302 | 303 | if is_shell_read { 304 | Ok(Some(Verb::Read(ReadSrc::Cmd(arg)))) 305 | } else { 306 | let arg_path = get_path(arg.trim()); 307 | Ok(Some(Verb::Read(ReadSrc::File(arg_path)))) 308 | } 309 | } 310 | 311 | fn parse_write(chars: &mut Peekable>) -> Result,Option> { 312 | chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); 313 | 314 | let is_shell_write = chars.peek() == Some(&'!'); 315 | if is_shell_write { 316 | chars.next(); // consume '!' 317 | let arg: String = chars.collect(); 318 | return Ok(Some(Verb::Write(WriteDest::Cmd(arg)))); 319 | } 320 | 321 | // Check for >> 322 | let mut append_check = chars.clone(); 323 | let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>'); 324 | if is_file_append { 325 | *chars = append_check; 326 | } 327 | 328 | let arg: String = chars.collect(); 329 | let arg_path = get_path(arg.trim()); 330 | 331 | let dest = if is_file_append { 332 | WriteDest::FileAppend(arg_path) 333 | } else { 334 | WriteDest::File(arg_path) 335 | }; 336 | 337 | Ok(Some(Verb::Write(dest))) 338 | } 339 | 340 | fn parse_global(chars: &mut Peekable>, constraint: Option<&Motion>) -> Result,Option> { 341 | let is_negated = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; 342 | 343 | chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace 344 | 345 | let Some(delimiter) = chars.next() else { 346 | return Ok(Some((Motion::Null,Verb::RepeatGlobal))) 347 | }; 348 | if delimiter.is_alphanumeric() { 349 | return Err(None) 350 | } 351 | let global_pat = parse_pattern(chars, delimiter)?; 352 | let Some(command) = parse_ex_command(chars)? else { 353 | return Err(Some("Expected a command after global pattern".into())) 354 | }; 355 | let constraint = Box::new(constraint.cloned().unwrap_or(Motion::LineRange(LineAddr::Number(1),LineAddr::Last))); 356 | if is_negated { 357 | Ok(Some((Motion::NotGlobal(constraint,Val::Str(global_pat)), command))) 358 | } else { 359 | Ok(Some((Motion::Global(constraint,Val::Str(global_pat)), command))) 360 | } 361 | } 362 | 363 | fn parse_substitute(chars: &mut Peekable>) -> Result,Option> { 364 | chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace 365 | 366 | let Some(delimiter) = chars.next() else { 367 | return Ok(Some(Verb::RepeatSubstitute)) 368 | }; 369 | if delimiter.is_alphanumeric() { 370 | return Err(None) 371 | } 372 | let old_pat = parse_pattern(chars, delimiter)?; 373 | let new_pat = parse_pattern(chars, delimiter)?; 374 | let mut flags = SubFlags::empty(); 375 | while let Some(ch) = chars.next() { 376 | match ch { 377 | 'g' => flags |= SubFlags::GLOBAL, 378 | 'i' => flags |= SubFlags::IGNORE_CASE, 379 | 'I' => flags |= SubFlags::NO_IGNORE_CASE, 380 | 'n' => flags |= SubFlags::SHOW_COUNT, 381 | _ => return Err(None) 382 | } 383 | } 384 | Ok(Some(Verb::Substitute(old_pat, new_pat, flags))) 385 | } 386 | 387 | fn parse_pattern(chars: &mut Peekable>, delimiter: char) -> Result> { 388 | let mut pat = String::new(); 389 | let mut closed = false; 390 | while let Some(ch) = chars.next() { 391 | match ch { 392 | '\\' => { 393 | if chars.peek().is_some_and(|c| *c == delimiter) { 394 | // We escaped the delimiter, so we consume the escape char and continue 395 | pat.push(chars.next().unwrap()); 396 | continue 397 | } else { 398 | // The escape char is probably for the regex in the pattern 399 | pat.push(ch); 400 | if let Some(esc_ch) = chars.next() { 401 | pat.push(esc_ch) 402 | } 403 | } 404 | } 405 | _ if ch == delimiter => { 406 | closed = true; 407 | break 408 | } 409 | _ => pat.push(ch) 410 | } 411 | } 412 | if !closed { 413 | Err(Some("Unclosed pattern in ex command".into())) 414 | } else { 415 | Ok(pat) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::iter::{Peekable, Skip}; 2 | use std::fmt::Write; 3 | 4 | use crate::exec::Val; 5 | use crate::vic::CmdArg; 6 | use crate::{linebuf::LineBuf, modes::{normal::ViNormal, ViMode}, Opts, Cmd}; 7 | use pretty_assertions::assert_eq; 8 | 9 | pub const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."; 10 | 11 | pub const LOREM_IPSUM_MULTILINE: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."; 12 | 13 | pub mod modes; 14 | pub mod linebuf; 15 | pub mod editor; 16 | pub mod files; 17 | pub mod pattern_match; 18 | pub mod wiki_examples; 19 | 20 | fn vicut_integration(input: &str, args: &[&str], expected: &str) { 21 | let output = call_main(args, input).unwrap(); 22 | let output = output.strip_suffix("\n").unwrap_or(&output); 23 | /* 24 | println!("got: {output:?}"); 25 | println!("expected: {expected:?}"); 26 | */ 27 | assert_eq!(output,expected) 28 | } 29 | 30 | 31 | fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String,usize) { 32 | let cmd = ViNormal::new() 33 | .cmds_from_raw(cmd) 34 | .pop() 35 | .unwrap(); 36 | let mut buf = LineBuf::new().with_initial(buf.to_string(), cursor); 37 | buf.exec_cmd(cmd).unwrap(); 38 | (buf.as_str().to_string(),buf.cursor.get()) 39 | } 40 | 41 | /* 42 | * Stuff down here is for testing 43 | */ 44 | 45 | /// Testing fixture 46 | /// Used to call the main logic internally 47 | #[cfg(any(test,debug_assertions))] 48 | pub fn call_main(args: &[&str], input: &str) -> Result { 49 | if args.is_empty() { 50 | let mut output = String::new(); 51 | write!(output,"USAGE:").ok(); 52 | write!(output,"\tvicut [OPTIONS] [COMMANDS]...").ok(); 53 | writeln!(output).ok(); 54 | write!(output,"use '--help' for more information").ok(); 55 | return Err(output) 56 | } 57 | if args.iter().any(|arg| *arg == "--help" || *arg == "-h") { 58 | return Ok(get_help()) 59 | } 60 | 61 | let mut args_iter = args.iter(); 62 | args_iter.find(|arg| **arg == "--script"); // let's find the --script flag 63 | let script = args_iter.next(); // If we found it, the next arg is the script name 64 | 65 | let args = if let Some(script) = script { 66 | let script = PathBuf::from(script); 67 | Opts::from_script(script).unwrap_or_else(|e| { 68 | eprintln!("{e}"); 69 | std::process::exit(1) 70 | }) 71 | } else { 72 | // Let's see if we got a literal in-line script instead then 73 | let mut flags = args.iter().take_while(|arg| **arg != "--"); 74 | let use_inline = flags.all(|arg| !arg.starts_with('-')); 75 | 76 | if use_inline { 77 | // We know that there's at least one argument, so we can safely unwrap 78 | let mut args = args.iter(); 79 | let maybe_script = args.next().unwrap(); 80 | let mut opts = if Opts::validate_filename(maybe_script).is_err() { 81 | // It's not a file... 82 | // Let's see if it's a valid in-line script 83 | Opts::from_raw(maybe_script).unwrap_or_else(|e| { 84 | eprintln!("{e}"); 85 | std::process::exit(1) 86 | }) 87 | } else { 88 | // It's a file, let's see if it's a valid script 89 | let script_path = PathBuf::from(maybe_script); 90 | Opts::from_script(script_path).unwrap_or_else(|e| { 91 | eprintln!("{e}"); 92 | std::process::exit(1) 93 | }) 94 | }; 95 | // Now let's grab the file names 96 | for arg in args { 97 | if let Err(e) = Opts::validate_filename(arg) { 98 | eprintln!("vicut: {e}"); 99 | std::process::exit(1); 100 | } 101 | opts.files.push(PathBuf::from(arg)); 102 | } 103 | opts 104 | } else { 105 | eprintln!("args: {args:?}"); 106 | // We're using command line arguments 107 | // boo 108 | Opts::parse_raw(args).unwrap_or_else(|e| { 109 | eprintln!("vicut: {e}"); 110 | std::process::exit(1) 111 | }) 112 | } 113 | }; 114 | 115 | use std::{io::{self, BufRead, Cursor}, path::PathBuf}; 116 | 117 | use crate::{execute, execute_linewise, format_output, get_help, get_lines, Opts}; 118 | if args.linewise { 119 | if args.single_thread { 120 | // We need to initialize stream in each branch, since Box does not implement send/sync 121 | // So using it in pool.install() doesn't work. We have to initialize it in the closure there. 122 | 123 | let mut stream: Box = Box::new(io::BufReader::new(Cursor::new(input))); 124 | let mut input = String::new(); 125 | stream.read_to_string(&mut input).unwrap(); 126 | let mut lines = vec![]; 127 | for line in get_lines(&input) { 128 | match execute(&args,line,None) { 129 | Ok(mut new_line) => { 130 | lines.append(&mut new_line); 131 | } 132 | Err(e) => { 133 | return Err(format!("vicut: {e}")); 134 | } 135 | } 136 | } 137 | let output = format_output(&args, lines); 138 | Ok(output) 139 | } else if let Some(num) = args.max_jobs { 140 | let pool = rayon::ThreadPoolBuilder::new() 141 | .num_threads(num as usize) 142 | .build() 143 | .unwrap_or_else(|e| { 144 | eprintln!("vicut: Failed to build thread pool: {e}"); 145 | std::process::exit(1) 146 | }); 147 | Ok(pool.install(|| { 148 | let stream: Box = Box::new(io::BufReader::new(Cursor::new(input.to_string()))); 149 | execute_linewise(stream, &args) 150 | })) 151 | } else { 152 | let stream: Box = Box::new(io::BufReader::new(Cursor::new(input.to_string()))); 153 | Ok(execute_linewise(stream, &args)) 154 | } 155 | } else { 156 | let mut stream: Box = Box::new(io::BufReader::new(Cursor::new(input))); 157 | let mut input = String::new(); 158 | let mut lines = vec![]; 159 | match stream.read_to_string(&mut input) { 160 | Ok(_) => {} 161 | Err(e) => { 162 | return Err(format!("vicut: {e}")); 163 | } 164 | } 165 | match execute(&args,input,None) { 166 | Ok(mut output) => { 167 | lines.append(&mut output); 168 | } 169 | Err(e) => eprintln!("vicut: {e}"), 170 | }; 171 | let output = format_output(&args, lines); 172 | Ok(output) 173 | } 174 | } 175 | #[cfg(any(test,debug_assertions))] 176 | impl Opts { 177 | pub fn parse_raw(args: &[&str]) -> Result { 178 | let mut new = Self::default(); 179 | let mut full_args = vec!["vicut"]; 180 | full_args.extend(args.iter()); 181 | let mut args = full_args.into_iter().map(|arg| arg.to_string()).collect::>(); 182 | let mut args = args.into_iter().skip(1).peekable(); 183 | while let Some(arg) = args.next() { 184 | match arg.as_str() { 185 | "--json" | "-j" => { 186 | new.json = true; 187 | } 188 | "--trace" => { 189 | new.trace = true; 190 | } 191 | "--linewise" => { 192 | new.linewise = true; 193 | } 194 | "--serial" => { 195 | new.single_thread = true; 196 | } 197 | "--trim-fields" => { 198 | new.trim_fields = true; 199 | } 200 | "--keep-mode" => { 201 | new.keep_mode = true; 202 | } 203 | "--backup" => { 204 | new.backup_files = true; 205 | } 206 | "--global-uses-line-numbers" => { 207 | new.global_uses_line_numbers = true; 208 | } 209 | "-i" => { 210 | new.edit_inplace = true; 211 | } 212 | "--template" | "-t" => { 213 | let Some(next_arg) = args.next() else { 214 | return Err(format!("Expected a format string after '{arg}'")) 215 | }; 216 | if next_arg.starts_with('-') { 217 | return Err(format!("Expected a format string after '{arg}', found {next_arg}")) 218 | } 219 | new.template = Some(next_arg.to_string()); 220 | } 221 | "--delimiter" | "-d" => { 222 | let Some(next_arg) = args.next() else { continue }; 223 | if next_arg.starts_with('-') { 224 | return Err(format!("Expected a delimiter after '{arg}', found {next_arg}")) 225 | } 226 | new.delimiter = Some(next_arg.to_string()); 227 | } 228 | "-n" | "--next" => new.cmds.push(Cmd::BreakGroup), 229 | "-r" | "--repeat" => { 230 | let cmd_count = args 231 | .next() 232 | .unwrap_or("1".into()) 233 | .parse::() 234 | .map_err(|_| format!("Expected a number after '{arg}'"))?; 235 | let repeat_count = args 236 | .next() 237 | .unwrap_or("1".into()) 238 | .parse::() 239 | .map_err(|_| format!("Expected a number after '{arg}'"))?; 240 | 241 | let mut body = vec![]; 242 | let drain_count = new.cmds.len().saturating_sub(cmd_count); 243 | body.extend(new.cmds.drain(drain_count..)); 244 | dbg!(&body); 245 | dbg!(&repeat_count); 246 | new.cmds.push(Cmd::Repeat{ body, count: CmdArg::Count(repeat_count + 1) }); 247 | } 248 | "-m" | "--move" => { 249 | let Some(arg) = args.next() else { continue }; 250 | if arg.starts_with('-') { 251 | return Err(format!("Expected a motion command after '-m', found {arg}")) 252 | } 253 | new.cmds.push(Cmd::Motion(CmdArg::Literal(Val::Str(arg.to_string())))); 254 | } 255 | "-c" | "--cut" => { 256 | let Some(arg) = args.next() else { continue }; 257 | if arg.starts_with("name=") { 258 | let name = arg.strip_prefix("name=").unwrap().to_string(); 259 | if name == "0" { 260 | // We use '0' as a sentinel value to say "We didn't slice any fields, so this field is the entire buffer" 261 | // So we can't let people use it arbitrarily, or weird shit starts happening 262 | return Err("Field name '0' is a reserved field name.".into()) 263 | } 264 | let Some(arg) = args.next() else { continue }; 265 | if arg.starts_with('-') { 266 | return Err(format!("Expected a selection command after '-c', found {arg}")) 267 | } 268 | new.cmds.push(Cmd::NamedField(name,CmdArg::Literal(Val::Str(arg.to_string())))); 269 | } else { 270 | if arg.starts_with('-') { 271 | return Err(format!("Expected a selection command after '-c', found {arg}")) 272 | } 273 | new.cmds.push(Cmd::Field(CmdArg::Literal(Val::Str(arg.to_string())))); 274 | } 275 | } 276 | "-v" | "--not-global" | 277 | "-g" | "--global" => { 278 | let global = Self::handle_global_arg(&arg, &mut args); 279 | new.cmds.push(global); 280 | } 281 | _ => new.handle_filename(arg.to_string()) 282 | } 283 | } 284 | Ok(new) 285 | } 286 | fn handle_global_arg_raw(arg: &str, args: &mut Peekable>>) -> Cmd { 287 | let polarity = match arg { 288 | "-v" | "--not-global" => false, 289 | "-g" | "--global" => true, 290 | _ => unreachable!("found arg: {arg}") 291 | }; 292 | let mut then_cmds = vec![]; 293 | let mut else_cmds = None; 294 | let Some(arg) = args.next() else { 295 | return Cmd::Global { 296 | pattern: CmdArg::Literal(Val::Str(arg.to_string())), 297 | then_cmds, 298 | else_cmds, 299 | polarity 300 | }; 301 | }; 302 | if arg.starts_with('-') { 303 | eprintln!("Expected a selection command after '-c', found {arg}"); 304 | std::process::exit(1) 305 | } 306 | while let Some(global_arg) = args.next() { 307 | match *global_arg { 308 | "-n" | "--next" => then_cmds.push(Cmd::BreakGroup), 309 | "-r" | "--repeat" => { 310 | let cmd_count = args 311 | .next() 312 | .unwrap_or(&"1") 313 | .parse::() 314 | .unwrap(); 315 | let repeat_count = args 316 | .next() 317 | .unwrap_or(&"1") 318 | .parse::() 319 | .unwrap(); 320 | 321 | let mut body = vec![]; 322 | for _ in 0..cmd_count { 323 | let cmds = if let Some(else_cmds) = else_cmds.as_mut() { 324 | else_cmds 325 | } else { 326 | &mut then_cmds 327 | }; 328 | let Some(cmd) = cmds.pop() else { break }; 329 | body.push(cmd); 330 | } 331 | let cmd = Cmd::Repeat{ body, count: CmdArg::Count(repeat_count) }; 332 | if let Some(cmds) = else_cmds.as_mut() { 333 | cmds.push(cmd); 334 | } else { 335 | then_cmds.push(cmd); 336 | } 337 | } 338 | "-m" | "--move" => { 339 | let Some(arg) = args.next() else { continue }; 340 | if arg.starts_with('-') { 341 | eprintln!("Expected a motion command after '-m', found {arg}"); 342 | std::process::exit(1); 343 | } 344 | if let Some(else_cmds) = else_cmds.as_mut() { 345 | else_cmds.push(Cmd::Motion(CmdArg::Literal(Val::Str(arg.to_string())))); 346 | } else { 347 | then_cmds.push(Cmd::Motion(CmdArg::Literal(Val::Str(arg.to_string())))); 348 | } 349 | } 350 | "-c" | "--cut" => { 351 | let Some(arg) = args.next() else { continue }; 352 | if arg.starts_with("name=") { 353 | let name = arg.strip_prefix("name=").unwrap().to_string(); 354 | let Some(arg) = args.next() else { continue }; 355 | if arg.starts_with('-') { 356 | eprintln!("Expected a selection command after '-c', found {arg}"); 357 | std::process::exit(1); 358 | } 359 | if let Some(cmds) = else_cmds.as_mut() { 360 | cmds.push(Cmd::NamedField(name,CmdArg::Literal(Val::Str(arg.to_string())))); 361 | } else { 362 | then_cmds.push(Cmd::NamedField(name,CmdArg::Literal(Val::Str(arg.to_string())))); 363 | } 364 | } else { 365 | if arg.starts_with('-') { 366 | eprintln!("Expected a selection command after '-c', found {arg}"); 367 | std::process::exit(1); 368 | } 369 | if let Some(cmds) = else_cmds.as_mut() { 370 | cmds.push(Cmd::Field(CmdArg::Literal(Val::Str(arg.to_string())))); 371 | } else { 372 | then_cmds.push(Cmd::Field(CmdArg::Literal(Val::Str(arg.to_string())))); 373 | } 374 | } 375 | } 376 | "-g" | "--global" | 377 | "-v" | "--not-global" => { 378 | let nested = Self::handle_global_arg_raw(global_arg, args); 379 | if let Some(cmds) = else_cmds.as_mut() { 380 | cmds.push(nested); 381 | } else { 382 | then_cmds.push(nested); 383 | } 384 | } 385 | "--else" => { 386 | // Now we start working on this 387 | else_cmds = Some(vec![]); 388 | } 389 | "--end" => { 390 | // We're done here 391 | return Cmd::Global { 392 | pattern: CmdArg::Literal(Val::Str(arg.to_string())), 393 | then_cmds, 394 | else_cmds, 395 | polarity 396 | }; 397 | } 398 | _ => { 399 | eprintln!("Expected command flag in '-g' scope\nDid you forget to close '-g' with '--end'?"); 400 | std::process::exit(1); 401 | } 402 | } 403 | if args.peek().is_some_and(|arg| !arg.starts_with('-')) { break } 404 | } 405 | 406 | // If we got here, we have run out of arguments 407 | // Let's just submit the current -g commands. 408 | // no need to be pressed about a missing '--end' when nothing would come after it 409 | Cmd::Global { 410 | pattern: CmdArg::Literal(Val::Str(arg.to_string())), 411 | then_cmds, 412 | else_cmds, 413 | polarity 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/vicmd.rs: -------------------------------------------------------------------------------- 1 | //! Logic describing Vim commands is held here. 2 | //! 3 | //! Parsing happens in the `modes` module. This module just holds the structs and enums. 4 | //! ViCmd is described in this module, and is probably *the single most load-bearing struct in the codebase*. 5 | use std::path::PathBuf; 6 | 7 | use bitflags::bitflags; 8 | 9 | use crate::{exec::Val, linebuf::SelectRange, modes::ex::SubFlags, register::{RegisterContent, REGISTERS}}; 10 | 11 | use super::register::{append_register, read_register, write_register}; 12 | 13 | //TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor 14 | 15 | #[derive(Clone,Copy,Debug,PartialEq)] 16 | pub struct RegisterName { 17 | name: Option, 18 | count: usize, 19 | append: bool 20 | } 21 | 22 | impl RegisterName { 23 | pub fn new(name: Option, count: Option) -> Self { 24 | let Some(ch) = name else { 25 | return Self::default() 26 | }; 27 | 28 | let append = ch.is_uppercase(); 29 | let name = ch.to_ascii_lowercase(); 30 | Self { 31 | name: Some(name), 32 | count: count.unwrap_or(1), 33 | append 34 | } 35 | } 36 | pub fn name(&self) -> Option { 37 | self.name 38 | } 39 | pub fn is_append(&self) -> bool { 40 | self.append 41 | } 42 | pub fn count(&self) -> usize { 43 | self.count 44 | } 45 | pub fn is_line(&self) -> bool { 46 | REGISTERS.with_borrow(|reg| reg.get_reg(self.name).is_some_and(|r| r.is_line())) 47 | } 48 | pub fn is_block(&self) -> bool { 49 | REGISTERS.with_borrow(|reg| reg.get_reg(self.name).is_some_and(|r| r.is_block())) 50 | } 51 | pub fn is_span(&self) -> bool { 52 | REGISTERS.with_borrow(|reg| reg.get_reg(self.name).is_some_and(|r| r.is_span())) 53 | } 54 | pub fn write_to_register(&self, buf: RegisterContent) { 55 | if self.append { 56 | append_register(self.name, buf); 57 | } else { 58 | write_register(self.name, buf); 59 | } 60 | } 61 | pub fn read_from_register(&self) -> Option { 62 | read_register(self.name) 63 | } 64 | } 65 | 66 | impl Default for RegisterName { 67 | fn default() -> Self { 68 | Self { 69 | name: None, 70 | count: 1, 71 | append: false 72 | } 73 | } 74 | } 75 | 76 | bitflags! { 77 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 78 | pub struct CmdFlags: u32 { 79 | const VISUAL = 1<<0; 80 | const VISUAL_LINE = 1<<1; 81 | const VISUAL_BLOCK = 1<<2; 82 | const EXIT_CUR_MODE = 1<<3; // for instance, when pressing enter during ex mode or search mode 83 | } 84 | } 85 | 86 | /// A Vim Command 87 | /// 88 | /// ## Fields 89 | /// `register`: The register to use for yank/delete/change/put. 90 | /// `verb`: The verb to execute, if any. 91 | /// `motion`: The motion to execute, if any. 92 | /// `raw_seq`: The raw sequence of characters that produced this ViCmd. 93 | /// `flags`: Bitflags which alter the execution properties of the command. 94 | /// 95 | /// Used extensively throughout the `exec`, `modes`, and `linebuf` modules. 96 | #[derive(Clone,Default,Debug,PartialEq)] 97 | pub struct ViCmd { 98 | pub register: RegisterName, 99 | pub verb: Option, 100 | pub motion: Option, 101 | pub raw_seq: String, 102 | pub flags: CmdFlags, 103 | } 104 | 105 | impl ViCmd { 106 | pub fn new() -> Self { 107 | Self::default() 108 | } 109 | pub fn set_motion(&mut self, motion: MotionCmd) { 110 | self.motion = Some(motion) 111 | } 112 | pub fn set_verb(&mut self, verb: VerbCmd) { 113 | self.verb = Some(verb) 114 | } 115 | pub fn verb(&self) -> Option<&VerbCmd> { 116 | self.verb.as_ref() 117 | } 118 | pub fn motion(&self) -> Option<&MotionCmd> { 119 | self.motion.as_ref() 120 | } 121 | pub fn verb_count(&self) -> usize { 122 | self.verb.as_ref().map(|v| v.0).unwrap_or(1) 123 | } 124 | pub fn motion_count(&self) -> usize { 125 | self.motion.as_ref().map(|m| m.0).unwrap_or(1) 126 | } 127 | /// Combine verb and motion counts 128 | /// 129 | /// This makes things easier to execute later. This is always executed when a ViCmd is parsed. 130 | pub fn normalize_counts(&mut self) { 131 | let Some(verb) = self.verb.as_mut() else { return }; 132 | let Some(motion) = self.motion.as_mut() else { return }; 133 | let VerbCmd(v_count, _) = verb; 134 | let MotionCmd(m_count, _) = motion; 135 | let product = *v_count * *m_count; 136 | verb.0 = 1; 137 | motion.0 = product; 138 | } 139 | pub fn is_repeatable(&self) -> bool { 140 | self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) 141 | } 142 | pub fn is_cmd_repeat(&self) -> bool { 143 | self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) 144 | } 145 | pub fn is_motion_repeat(&self) -> bool { 146 | self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev)) 147 | } 148 | pub fn is_char_search(&self) -> bool { 149 | self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) 150 | } 151 | pub fn should_submit(&self) -> bool { 152 | self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline)) 153 | } 154 | pub fn is_undo_op(&self) -> bool { 155 | self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) 156 | } 157 | pub fn is_inplace_edit(&self) -> bool { 158 | self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) && 159 | self.motion.is_none() 160 | } 161 | pub fn is_ex_normal(&self) -> bool { 162 | self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Normal(_))) 163 | } 164 | pub fn is_ex_global(&self) -> bool { 165 | self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::Global(_,_) | Motion::NotGlobal(_,_))) 166 | } 167 | pub fn is_line_motion(&self) -> bool { 168 | self.motion.as_ref().is_some_and(|m| { 169 | matches!(m.1, 170 | Motion::LineUp | 171 | Motion::LineDown | 172 | Motion::LineUpCharwise | 173 | Motion::LineDownCharwise 174 | ) 175 | }) 176 | } 177 | /// If a ViCmd has a linewise motion, but no verb, we change it to charwise 178 | pub fn alter_line_motion_if_no_verb(&mut self) { 179 | if self.is_line_motion() && self.verb.is_none() { 180 | if let Some(motion) = self.motion.as_mut() { 181 | match motion.1 { 182 | Motion::LineUp => motion.1 = Motion::LineUpCharwise, 183 | Motion::LineDown => motion.1 = Motion::LineDownCharwise, 184 | _ => unreachable!() 185 | } 186 | } 187 | } 188 | } 189 | pub fn is_mode_transition(&self) -> bool { 190 | self.verb.as_ref().is_some_and(|v| { 191 | matches!(v.1, 192 | Verb::Change | 193 | Verb::InsertMode | 194 | Verb::ExMode | 195 | Verb::SearchMode(_,_) | 196 | Verb::InsertModeLineBreak(_) | 197 | Verb::NormalMode | 198 | Verb::VisualModeSelectLast | 199 | Verb::VisualMode | 200 | Verb::VisualModeLine | 201 | Verb::VisualModeBlock | 202 | Verb::ReplaceMode 203 | ) 204 | }) 205 | } 206 | } 207 | 208 | /// A count, and a `Verb` 209 | #[derive(Clone,Debug,PartialEq)] 210 | pub struct VerbCmd(pub usize,pub Verb); 211 | /// A count, and a `Motion` 212 | #[derive(Clone,Debug,PartialEq)] 213 | pub struct MotionCmd(pub usize,pub Motion); 214 | 215 | impl MotionCmd { 216 | pub fn invert_char_motion(self) -> Self { 217 | let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else { 218 | unreachable!() 219 | }; 220 | let new_dir = match dir { 221 | Direction::Forward => Direction::Backward, 222 | Direction::Backward => Direction::Forward, 223 | }; 224 | MotionCmd(count,Motion::CharSearch(new_dir, dest, ch)) 225 | } 226 | } 227 | 228 | /// Vim operators 229 | /// 230 | /// This enum contains all of the currently supported Vim operators. These are parsed in `modes`, and executed in `linebuf` 231 | #[derive(Debug, Clone, PartialEq)] 232 | #[non_exhaustive] 233 | pub enum Verb { 234 | Delete, 235 | Change, 236 | Yank, 237 | Rot13, 238 | ReplaceChar(char), // char to replace with, number of chars to replace 239 | ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace 240 | ToggleCaseInplace(u16), // Number of chars to toggle 241 | ToggleCaseRange, 242 | ToLower, 243 | ToUpper, 244 | Complete, 245 | CompleteBackward, 246 | Undo, 247 | Redo, 248 | RepeatLast, 249 | Put(Anchor), 250 | /// (old_pat,new_pat,flags) 251 | Substitute(String,String,SubFlags), 252 | RepeatSubstitute, 253 | RepeatGlobal, 254 | ShellCmd(String), 255 | Read(ReadSrc), 256 | Write(WriteDest), 257 | SearchMode(usize,Direction), 258 | Normal(String), // ex mode 'normal!' 259 | ReplaceMode, 260 | ExMode, 261 | InsertMode, 262 | InsertModeLineBreak(Anchor), 263 | NormalMode, 264 | VisualMode, 265 | VisualModeLine, 266 | VisualModeBlock, 267 | VisualModeSelectLast, 268 | SwapVisualAnchor, 269 | JoinLines, 270 | InsertChar(char), 271 | Insert(String), 272 | Indent, 273 | Dedent, 274 | Equalize, 275 | AcceptLineOrNewline, 276 | EndOfFile 277 | } 278 | 279 | 280 | impl Verb { 281 | pub fn is_repeatable(&self) -> bool { 282 | matches!(self, 283 | Self::Delete | 284 | Self::Change | 285 | Self::ReplaceChar(_) | 286 | Self::ReplaceCharInplace(_,_) | 287 | Self::ToLower | 288 | Self::ToUpper | 289 | Self::ToggleCaseRange | 290 | Self::ToggleCaseInplace(_) | 291 | Self::Put(_) | 292 | Self::ReplaceMode | 293 | Self::InsertModeLineBreak(_) | 294 | Self::JoinLines | 295 | Self::InsertChar(_) | 296 | Self::Insert(_) | 297 | Self::Indent | 298 | Self::Dedent | 299 | Self::Equalize 300 | ) 301 | } 302 | pub fn is_edit(&self) -> bool { 303 | matches!(self, 304 | Self::Delete | 305 | Self::Change | 306 | Self::ReplaceChar(_) | 307 | Self::ReplaceCharInplace(_,_) | 308 | Self::ToggleCaseRange | 309 | Self::ToggleCaseInplace(_) | 310 | Self::ToLower | 311 | Self::ToUpper | 312 | Self::RepeatLast | 313 | Self::Put(_) | 314 | Self::ReplaceMode | 315 | Self::InsertModeLineBreak(_) | 316 | Self::JoinLines | 317 | Self::InsertChar(_) | 318 | Self::Insert(_) | 319 | Self::Rot13 | 320 | Self::EndOfFile 321 | ) 322 | } 323 | pub fn is_char_insert(&self) -> bool { 324 | matches!(self, 325 | Self::Change | 326 | Self::InsertChar(_) | 327 | Self::ReplaceChar(_) | 328 | Self::ReplaceCharInplace(_,_) 329 | ) 330 | } 331 | } 332 | 333 | /// Vim motions 334 | /// 335 | /// This enum contains all of the currently supported Vim motions. These are parsed in `modes`, and executed in `linebuf` 336 | #[derive(Debug, Clone, PartialEq)] 337 | pub enum Motion { 338 | WholeLine, 339 | WholeLineExclusive, 340 | TextObj(TextObj), 341 | EndOfLastWord, 342 | BeginningOfFirstWord, 343 | BeginningOfLine, 344 | EndOfLine, 345 | WordMotion(To,Word,Direction), 346 | CharSearch(Direction,Dest,char), 347 | Line(LineAddr), // x 348 | LineRange(LineAddr,LineAddr), // x,y 349 | PatternSearch(String), 350 | PatternSearchRev(String), 351 | /// The first field should *always* be `Line(_)` or `LineRange(_,_)` 352 | Global(Box,Val), 353 | /// The first field should *always* be `Line(_)` or `LineRange(_,_)` 354 | NotGlobal(Box,Val), 355 | NextMatch, 356 | PrevMatch, 357 | BackwardChar, 358 | ForwardChar, 359 | /// Can cross line boundaries 360 | BackwardCharForced, 361 | /// Can cross line boundaries 362 | ForwardCharForced, 363 | LineUp, 364 | LineDown, 365 | LineUpCharwise, 366 | LineDownCharwise, 367 | WholeBuffer, 368 | BeginningOfBuffer, 369 | EndOfBuffer, 370 | ToColumn, 371 | ToDelimMatch, 372 | ToBrace(Direction), 373 | ToBracket(Direction), 374 | ToParen(Direction), 375 | Range(SelectRange), 376 | RangeInclusive(SelectRange), 377 | RepeatMotion, 378 | RepeatMotionRev, 379 | 380 | // TODO: Not sure how to implement these in a non-interactive way... 381 | ScreenLineUp, 382 | ScreenLineUpCharwise, 383 | ScreenLineDown, 384 | ScreenLineDownCharwise, 385 | BeginningOfScreenLine, 386 | FirstGraphicalOnScreenLine, 387 | HalfOfScreen, 388 | HalfOfScreenLineText, 389 | Null 390 | } 391 | 392 | impl Motion { 393 | pub fn is_exclusive(&self) -> bool { 394 | matches!(&self, 395 | Self::BeginningOfLine | 396 | Self::BeginningOfFirstWord | 397 | Self::BeginningOfScreenLine | 398 | Self::FirstGraphicalOnScreenLine | 399 | Self::LineDownCharwise | 400 | Self::LineUpCharwise | 401 | Self::ScreenLineUpCharwise | 402 | Self::ScreenLineDownCharwise | 403 | Self::ToColumn | 404 | Self::TextObj(TextObj::Sentence(_)) | 405 | Self::TextObj(TextObj::Paragraph(_)) | 406 | Self::CharSearch(Direction::Backward, _, _) | 407 | Self::WordMotion(To::Start,_,_) | 408 | Self::ToBrace(_) | 409 | Self::ToBracket(_) | 410 | Self::ToParen(_) | 411 | Self::ScreenLineDown | 412 | Self::ScreenLineUp | 413 | Self::Range(_) 414 | ) 415 | } 416 | pub fn is_linewise(&self) -> bool { 417 | matches!(self, 418 | Self::WholeLine | 419 | Self::LineUp | 420 | Self::LineDown | 421 | Self::ScreenLineDown | 422 | Self::ScreenLineUp 423 | ) 424 | } 425 | } 426 | 427 | /// Apply a verb before, or after the target 428 | /// 429 | /// Used with stuff like `put` to choose where to perform the action 430 | #[derive(Debug, Clone, Eq, PartialEq)] 431 | pub enum Anchor { 432 | After, 433 | Before 434 | } 435 | 436 | /// Vim Text Objects 437 | /// 438 | /// Used with Motion::TextObj(_) 439 | #[derive(Debug, Clone, Eq, PartialEq)] 440 | pub enum TextObj { 441 | /// `iw`, `aw` — inner word, around word 442 | Word(Word, Bound), 443 | 444 | /// `)`, `(` — forward, backward 445 | Sentence(Direction), 446 | 447 | /// `}`, `{` — forward, backward 448 | Paragraph(Direction), 449 | 450 | WholeSentence(Bound), 451 | WholeParagraph(Bound), 452 | 453 | /// `i"`, `a"` — inner/around double quotes 454 | DoubleQuote(Bound), 455 | /// `i'`, `a'` 456 | SingleQuote(Bound), 457 | /// `i\``, `a\`` 458 | BacktickQuote(Bound), 459 | 460 | /// `i)`, `a)` — round parens 461 | Paren(Bound), 462 | /// `i]`, `a]` 463 | Bracket(Bound), 464 | /// `i}`, `a}` 465 | Brace(Bound), 466 | /// `i<`, `a<` 467 | Angle(Bound), 468 | 469 | /// `it`, `at` — HTML/XML tags 470 | Tag(Bound), 471 | 472 | /// Custom user-defined objects maybe? 473 | Custom(char), 474 | } 475 | 476 | /// The source to read from for ex mode's `:r` 477 | /// 478 | /// * `:r ` -> ReadSrc::File(_) 479 | /// * `:r !` -> ReadSrc::Cmd(_) 480 | #[derive(Debug, Clone, Eq, PartialEq)] 481 | pub enum ReadSrc { 482 | File(PathBuf), 483 | Cmd(String) 484 | } 485 | 486 | /// The target for ex mode's `:w` 487 | /// 488 | /// * `:w ` -> WriteDest::File(_) 489 | /// * `:w >> ` -> WriteDest::FileAppend(_) 490 | /// * `:w !` -> WriteDest::Cmd(_) 491 | #[derive(Debug, Clone, Eq, PartialEq)] 492 | pub enum WriteDest { 493 | File(PathBuf), 494 | FileAppend(PathBuf), 495 | Cmd(String), 496 | } 497 | 498 | /// Line Addresses used by ex mode 499 | #[derive(Debug, Clone, Eq, PartialEq)] 500 | pub enum LineAddr { 501 | Number(usize), 502 | Current, 503 | Last, 504 | Offset(isize), 505 | Pattern(String), 506 | PatternRev(String), 507 | } 508 | 509 | /// Word sizes for motions like 'w' and 'B' 510 | /// 511 | /// `Word::Big` counts any span of non-whitespace characters as a word 512 | /// `Word::Normal` counts a non-whitespace span of similar characters as a word 513 | /// a span of alphanumeric characters is a `Word::Normal,` and a span of symbols is a `Word::Normal` 514 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 515 | pub enum Word { 516 | Big, 517 | Normal 518 | } 519 | 520 | /// Text Object bounds 521 | /// 522 | /// Whether to take the inside of a text object or the entire thing 523 | /// For instance, 'di)' uses Bound::Inside, so `d` will only delete inside the parenthesis 524 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 525 | pub enum Bound { 526 | Inside, 527 | Around 528 | } 529 | 530 | /// Motion Direction 531 | /// 532 | /// Used mainly for Motions, but is also repurposed in some places where direction matters for the logic 533 | #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] 534 | pub enum Direction { 535 | #[default] 536 | Forward, 537 | Backward 538 | } 539 | 540 | /// Target destination for Char search motions 541 | /// 542 | /// `t` uses Dest::Before 543 | /// `f` uses Dest::On 544 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 545 | pub enum Dest { 546 | On, 547 | Before, 548 | } 549 | 550 | /// Target destination for Word motions 551 | /// 552 | /// `To::Start` attempts to move to the start of a Word 553 | /// `To::End` attempts to move to the end of a Word 554 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 555 | pub enum To { 556 | Start, 557 | End 558 | } 559 | -------------------------------------------------------------------------------- /src/modes/visual.rs: -------------------------------------------------------------------------------- 1 | use std::{iter::Peekable, str::Chars}; 2 | 3 | use crate::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word}; 4 | use crate::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; 5 | 6 | use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode}; 7 | 8 | #[derive(Default,Debug)] 9 | pub struct ViVisual { 10 | pending_seq: String, 11 | } 12 | 13 | impl ViVisual { 14 | pub fn new() -> Self { 15 | Self::default() 16 | } 17 | pub fn clear_cmd(&mut self) { 18 | self.pending_seq = String::new(); 19 | } 20 | pub fn take_cmd(&mut self) -> String { 21 | std::mem::take(&mut self.pending_seq) 22 | } 23 | 24 | #[allow(clippy::unnecessary_unwrap)] 25 | fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { 26 | if verb.is_none() { 27 | match motion { 28 | Some(_) => return CmdState::Complete, 29 | None => return CmdState::Pending 30 | } 31 | } 32 | if motion.is_none() && verb.is_some() { 33 | match verb.unwrap() { 34 | Verb::Put(_) => CmdState::Complete, 35 | _ => CmdState::Pending 36 | } 37 | } else { 38 | CmdState::Complete 39 | } 40 | } 41 | pub fn parse_count(&self, chars: &mut Peekable>) -> Option { 42 | let mut count = String::new(); 43 | let Some(_digit @ '1'..='9') = chars.peek() else { 44 | return None 45 | }; 46 | count.push(chars.next().unwrap()); 47 | while let Some(_digit @ '0'..='9') = chars.peek() { 48 | count.push(chars.next().unwrap()); 49 | } 50 | if !count.is_empty() { 51 | count.parse::().ok() 52 | } else { 53 | None 54 | } 55 | } 56 | /// End the parse and clear the pending sequence 57 | #[track_caller] 58 | pub fn quit_parse(&mut self) -> Option { 59 | self.clear_cmd(); 60 | None 61 | } 62 | pub fn try_parse(&mut self, ch: char) -> Option { 63 | self.pending_seq.push(ch); 64 | let mut chars = self.pending_seq.chars().peekable(); 65 | 66 | let register = 'reg_parse: { 67 | let mut chars_clone = chars.clone(); 68 | let count = self.parse_count(&mut chars_clone); 69 | 70 | let Some('"') = chars_clone.next() else { 71 | break 'reg_parse RegisterName::default() 72 | }; 73 | 74 | let Some(reg_name) = chars_clone.next() else { 75 | return None // Pending register name 76 | }; 77 | match reg_name { 78 | 'a'..='z' | 79 | 'A'..='Z' => { /* proceed */ } 80 | _ => return self.quit_parse() 81 | } 82 | 83 | chars = chars_clone; 84 | RegisterName::new(Some(reg_name), count) 85 | }; 86 | 87 | let verb = 'verb_parse: { 88 | let mut chars_clone = chars.clone(); 89 | let count = self.parse_count(&mut chars_clone).unwrap_or(1); 90 | 91 | let Some(ch) = chars_clone.next() else { 92 | break 'verb_parse None 93 | }; 94 | match ch { 95 | 'g' => { 96 | if let Some(ch) = chars_clone.peek() { 97 | match ch { 98 | 'v' => { 99 | return Some( 100 | ViCmd { 101 | register, 102 | verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), 103 | motion: None, 104 | raw_seq: self.take_cmd(), 105 | flags: CmdFlags::empty() 106 | } 107 | ) 108 | } 109 | '?' => { 110 | return Some( 111 | ViCmd { 112 | register, 113 | verb: Some(VerbCmd(1, Verb::Rot13)), 114 | motion: None, 115 | raw_seq: self.take_cmd(), 116 | flags: CmdFlags::empty() 117 | } 118 | ) 119 | } 120 | _ => break 'verb_parse None 121 | } 122 | } else { 123 | break 'verb_parse None 124 | } 125 | } 126 | ':' => { 127 | return Some( 128 | ViCmd { 129 | register, 130 | verb: Some(VerbCmd(1, Verb::ExMode)), 131 | motion: None, 132 | raw_seq: self.take_cmd(), 133 | flags: CmdFlags::empty(), 134 | } 135 | ) 136 | } 137 | '.' => { 138 | return Some( 139 | ViCmd { 140 | register, 141 | verb: Some(VerbCmd(count, Verb::RepeatLast)), 142 | motion: None, 143 | raw_seq: self.take_cmd(), 144 | flags: CmdFlags::empty() 145 | } 146 | ) 147 | } 148 | 'x' => { 149 | chars = chars_clone; 150 | break 'verb_parse Some(VerbCmd(count, Verb::Delete)); 151 | } 152 | 'X' => { 153 | return Some( 154 | ViCmd { 155 | register, 156 | verb: Some(VerbCmd(1, Verb::Delete)), 157 | motion: Some(MotionCmd(1, Motion::WholeLine)), 158 | raw_seq: self.take_cmd(), 159 | flags: CmdFlags::empty() 160 | } 161 | ) 162 | } 163 | 'Y' => { 164 | return Some( 165 | ViCmd { 166 | register, 167 | verb: Some(VerbCmd(1, Verb::Yank)), 168 | motion: Some(MotionCmd(1, Motion::WholeLine)), 169 | raw_seq: self.take_cmd(), 170 | flags: CmdFlags::empty() 171 | } 172 | ) 173 | } 174 | 'D' => { 175 | return Some( 176 | ViCmd { 177 | register, 178 | verb: Some(VerbCmd(1, Verb::Delete)), 179 | motion: Some(MotionCmd(1, Motion::WholeLine)), 180 | raw_seq: self.take_cmd(), 181 | flags: CmdFlags::empty() 182 | } 183 | ) 184 | } 185 | 'R' | 186 | 'C' => { 187 | return Some( 188 | ViCmd { 189 | register, 190 | verb: Some(VerbCmd(1, Verb::Change)), 191 | motion: Some(MotionCmd(1, Motion::WholeLine)), 192 | raw_seq: self.take_cmd(), 193 | flags: CmdFlags::empty() 194 | } 195 | ) 196 | } 197 | '>' => { 198 | return Some( 199 | ViCmd { 200 | register, 201 | verb: Some(VerbCmd(1, Verb::Indent)), 202 | motion: Some(MotionCmd(1, Motion::WholeLine)), 203 | raw_seq: self.take_cmd(), 204 | flags: CmdFlags::empty() 205 | } 206 | ) 207 | } 208 | '<' => { 209 | return Some( 210 | ViCmd { 211 | register, 212 | verb: Some(VerbCmd(1, Verb::Dedent)), 213 | motion: Some(MotionCmd(1, Motion::WholeLine)), 214 | raw_seq: self.take_cmd(), 215 | flags: CmdFlags::empty() 216 | } 217 | ) 218 | } 219 | '=' => { 220 | return Some( 221 | ViCmd { 222 | register, 223 | verb: Some(VerbCmd(1, Verb::Equalize)), 224 | motion: Some(MotionCmd(1, Motion::WholeLine)), 225 | raw_seq: self.take_cmd(), 226 | flags: CmdFlags::empty() 227 | } 228 | ) 229 | } 230 | 'p' | 231 | 'P' => { 232 | chars = chars_clone; 233 | break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); 234 | } 235 | '/' => { 236 | return Some( 237 | ViCmd { 238 | register, 239 | verb: Some(VerbCmd(1, Verb::SearchMode(count, Direction::Forward))), 240 | motion: None, 241 | raw_seq: self.take_cmd(), 242 | flags: CmdFlags::empty() 243 | } 244 | ) 245 | } 246 | '?' => { 247 | return Some( 248 | ViCmd { 249 | register, 250 | verb: Some(VerbCmd(1, Verb::SearchMode(count, Direction::Backward))), 251 | motion: None, 252 | raw_seq: self.take_cmd(), 253 | flags: CmdFlags::empty() 254 | } 255 | ) 256 | } 257 | 'r' => { 258 | let ch = chars_clone.next()?; 259 | return Some( 260 | ViCmd { 261 | register, 262 | verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), 263 | motion: None, 264 | raw_seq: self.take_cmd(), 265 | flags: CmdFlags::empty() 266 | } 267 | ) 268 | } 269 | '~' => { 270 | return Some( 271 | ViCmd { 272 | register, 273 | verb: Some(VerbCmd(1, Verb::ToggleCaseRange)), 274 | motion: None, 275 | raw_seq: self.take_cmd(), 276 | flags: CmdFlags::empty() 277 | } 278 | ) 279 | } 280 | 'u' => { 281 | return Some( 282 | ViCmd { 283 | register, 284 | verb: Some(VerbCmd(count, Verb::ToLower)), 285 | motion: None, 286 | raw_seq: self.take_cmd(), 287 | flags: CmdFlags::empty() 288 | } 289 | ) 290 | } 291 | 'U' => { 292 | return Some( 293 | ViCmd { 294 | register, 295 | verb: Some(VerbCmd(count, Verb::ToUpper)), 296 | motion: None, 297 | raw_seq: self.take_cmd(), 298 | flags: CmdFlags::empty() 299 | } 300 | ) 301 | } 302 | 'O' | 303 | 'o' => { 304 | return Some( 305 | ViCmd { 306 | register, 307 | verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), 308 | motion: None, 309 | raw_seq: self.take_cmd(), 310 | flags: CmdFlags::empty() 311 | } 312 | ) 313 | } 314 | 'A' => { 315 | return Some( 316 | ViCmd { 317 | register, 318 | verb: Some(VerbCmd(count, Verb::InsertMode)), 319 | motion: Some(MotionCmd(1, Motion::ForwardChar)), 320 | raw_seq: self.take_cmd(), 321 | flags: CmdFlags::empty() 322 | } 323 | ) 324 | } 325 | 'I' => { 326 | return Some( 327 | ViCmd { 328 | register, 329 | verb: Some(VerbCmd(count, Verb::InsertMode)), 330 | motion: Some(MotionCmd(1, Motion::BeginningOfLine)), 331 | raw_seq: self.take_cmd(), 332 | flags: CmdFlags::empty() 333 | } 334 | ) 335 | } 336 | 'J' => { 337 | return Some( 338 | ViCmd { 339 | register, 340 | verb: Some(VerbCmd(count, Verb::JoinLines)), 341 | motion: None, 342 | raw_seq: self.take_cmd(), 343 | flags: CmdFlags::empty() 344 | } 345 | ) 346 | } 347 | 'y' => { 348 | chars = chars_clone; 349 | break 'verb_parse Some(VerbCmd(count, Verb::Yank)) 350 | } 351 | 'd' => { 352 | chars = chars_clone; 353 | break 'verb_parse Some(VerbCmd(count, Verb::Delete)) 354 | } 355 | 'c' => { 356 | chars = chars_clone; 357 | break 'verb_parse Some(VerbCmd(count, Verb::Change)) 358 | } 359 | _ => break 'verb_parse None 360 | } 361 | }; 362 | 363 | if let Some(verb) = verb { 364 | return Some(ViCmd { 365 | register, 366 | verb: Some(verb), 367 | motion: None, 368 | raw_seq: self.take_cmd(), 369 | flags: CmdFlags::empty() 370 | }) 371 | } 372 | 373 | let motion = 'motion_parse: { 374 | let mut chars_clone = chars.clone(); 375 | let count = self.parse_count(&mut chars_clone).unwrap_or(1); 376 | 377 | let Some(ch) = chars_clone.next() else { 378 | break 'motion_parse None 379 | }; 380 | match (ch, &verb) { 381 | ('d', Some(VerbCmd(_,Verb::Delete))) | 382 | ('c', Some(VerbCmd(_,Verb::Change))) | 383 | ('y', Some(VerbCmd(_,Verb::Yank))) | 384 | ('=', Some(VerbCmd(_,Verb::Equalize))) | 385 | ('>', Some(VerbCmd(_,Verb::Indent))) | 386 | ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), 387 | _ => {} 388 | } 389 | match ch { 390 | 'g' => { 391 | if let Some(ch) = chars_clone.peek() { 392 | match ch { 393 | 'g' => break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)), 394 | 'e' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward))), 395 | 'E' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))), 396 | 'k' => break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)), 397 | 'j' => break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)), 398 | _ => return self.quit_parse() 399 | } 400 | } else { 401 | break 'motion_parse None 402 | } 403 | } 404 | ']' => { 405 | let Some(ch) = chars_clone.peek() else { 406 | break 'motion_parse None 407 | }; 408 | match ch { 409 | ')' => break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward))), 410 | '}' => break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward))), 411 | _ => return self.quit_parse() 412 | } 413 | } 414 | '[' => { 415 | let Some(ch) = chars_clone.peek() else { 416 | break 'motion_parse None 417 | }; 418 | match ch { 419 | '(' => break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward))), 420 | '{' => break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward))), 421 | _ => return self.quit_parse() 422 | } 423 | } 424 | '%' => break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch)), 425 | 'f' => { 426 | let Some(ch) = chars_clone.peek() else { 427 | break 'motion_parse None 428 | }; 429 | 430 | break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch))) 431 | } 432 | 'F' => { 433 | let Some(ch) = chars_clone.peek() else { 434 | break 'motion_parse None 435 | }; 436 | 437 | break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch))) 438 | } 439 | 't' => { 440 | let Some(ch) = chars_clone.peek() else { 441 | break 'motion_parse None 442 | }; 443 | 444 | break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch))) 445 | } 446 | 'T' => { 447 | let Some(ch) = chars_clone.peek() else { 448 | break 'motion_parse None 449 | }; 450 | 451 | break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch))) 452 | } 453 | 'G' => break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer)), 454 | 'n' => break 'motion_parse Some(MotionCmd(count, Motion::NextMatch)), 455 | 'N' => break 'motion_parse Some(MotionCmd(count, Motion::PrevMatch)), 456 | ';' => break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)), 457 | ',' => break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)), 458 | '|' => break 'motion_parse Some(MotionCmd(count, Motion::ToColumn)), 459 | '0' => break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)), 460 | '$' => break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine)), 461 | 'k' => break 'motion_parse Some(MotionCmd(count, Motion::LineUp)), 462 | 'j' => break 'motion_parse Some(MotionCmd(count, Motion::LineDown)), 463 | 'h' => break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar)), 464 | 'l' => break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar)), 465 | 'w' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Forward))), 466 | 'W' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward))), 467 | 'e' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward))), 468 | 'E' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))), 469 | 'b' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))), 470 | 'B' => break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward))), 471 | ')' => break 'motion_parse Some(MotionCmd(count, Motion::TextObj(TextObj::Sentence(Direction::Forward)))), 472 | '(' => break 'motion_parse Some(MotionCmd(count, Motion::TextObj(TextObj::Sentence(Direction::Backward)))), 473 | '}' => break 'motion_parse Some(MotionCmd(count, Motion::TextObj(TextObj::Paragraph(Direction::Forward)))), 474 | '{' => break 'motion_parse Some(MotionCmd(count, Motion::TextObj(TextObj::Paragraph(Direction::Backward)))), 475 | ch if ch == 'i' || ch == 'a' => { 476 | let bound = match ch { 477 | 'i' => Bound::Inside, 478 | 'a' => Bound::Around, 479 | _ => unreachable!() 480 | }; 481 | if chars_clone.peek().is_none() { 482 | break 'motion_parse None 483 | } 484 | let obj = match chars_clone.next().unwrap() { 485 | 'w' => TextObj::Word(Word::Normal,bound), 486 | 'W' => TextObj::Word(Word::Big,bound), 487 | 's' => TextObj::WholeSentence(bound), 488 | 'p' => TextObj::WholeParagraph(bound), 489 | '"' => TextObj::DoubleQuote(bound), 490 | '\'' => TextObj::SingleQuote(bound), 491 | '`' => TextObj::BacktickQuote(bound), 492 | '(' | ')' | 'b' => TextObj::Paren(bound), 493 | '{' | '}' | 'B' => TextObj::Brace(bound), 494 | '[' | ']' => TextObj::Bracket(bound), 495 | '<' | '>' => TextObj::Angle(bound), 496 | _ => return self.quit_parse() 497 | }; 498 | break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj))) 499 | } 500 | _ => return self.quit_parse(), 501 | } 502 | }; 503 | 504 | let verb_ref = verb.as_ref().map(|v| &v.1); 505 | let motion_ref = motion.as_ref().map(|m| &m.1); 506 | 507 | match self.validate_combination(verb_ref, motion_ref) { 508 | CmdState::Complete => { 509 | Some( 510 | ViCmd { 511 | register, 512 | verb, 513 | motion, 514 | raw_seq: std::mem::take(&mut self.pending_seq), 515 | flags: CmdFlags::empty() 516 | } 517 | ) 518 | } 519 | CmdState::Pending => { 520 | None 521 | } 522 | CmdState::Invalid => { 523 | self.pending_seq.clear(); 524 | None 525 | } 526 | } 527 | } 528 | } 529 | 530 | impl ViMode for ViVisual { 531 | fn handle_key(&mut self, key: E) -> Option { 532 | let mut cmd = match key { 533 | E(K::Char(ch), M::NONE) => self.try_parse(ch), 534 | E(K::Backspace, M::NONE) => { 535 | Some(ViCmd { 536 | register: Default::default(), 537 | verb: None, 538 | motion: Some(MotionCmd(1, Motion::BackwardChar)), 539 | raw_seq: "".into(), 540 | flags: CmdFlags::empty() 541 | }) 542 | } 543 | E(K::Char('R'), M::CTRL) => { 544 | let mut chars = self.pending_seq.chars().peekable(); 545 | let count = self.parse_count(&mut chars).unwrap_or(1); 546 | Some( 547 | ViCmd { 548 | register: RegisterName::default(), 549 | verb: Some(VerbCmd(count,Verb::Redo)), 550 | motion: None, 551 | raw_seq: self.take_cmd(), 552 | flags: CmdFlags::empty() 553 | } 554 | ) 555 | } 556 | E(K::Esc, M::NONE) => { 557 | Some( 558 | ViCmd { 559 | register: Default::default(), 560 | verb: Some(VerbCmd(1, Verb::NormalMode)), 561 | motion: Some(MotionCmd(1, Motion::Null)), 562 | raw_seq: self.take_cmd(), 563 | flags: CmdFlags::empty() 564 | }) 565 | } 566 | _ => { 567 | if let Some(cmd) = common_cmds(key) { 568 | self.clear_cmd(); 569 | Some(cmd) 570 | } else { 571 | None 572 | } 573 | } 574 | }; 575 | 576 | if let Some(cmd) = cmd.as_mut() { 577 | cmd.normalize_counts(); 578 | }; 579 | cmd 580 | } 581 | 582 | fn is_repeatable(&self) -> bool { 583 | true 584 | } 585 | 586 | fn as_replay(&self) -> Option { 587 | None 588 | } 589 | 590 | fn cursor_style(&self) -> String { 591 | "\x1b[2 q".to_string() 592 | } 593 | 594 | fn pending_seq(&self) -> Option { 595 | Some(self.pending_seq.clone()) 596 | } 597 | 598 | fn move_cursor_on_undo(&self) -> bool { 599 | true 600 | } 601 | 602 | fn clamp_cursor(&self) -> bool { 603 | false 604 | } 605 | 606 | fn hist_scroll_start_pos(&self) -> Option { 607 | None 608 | } 609 | 610 | fn report_mode(&self) -> ModeReport { 611 | ModeReport::Visual 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.19" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.11" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.7" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.9" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.9.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 69 | 70 | [[package]] 71 | name = "block-buffer" 72 | version = "0.10.4" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 75 | dependencies = [ 76 | "generic-array", 77 | ] 78 | 79 | [[package]] 80 | name = "cc" 81 | version = "1.2.27" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" 84 | dependencies = [ 85 | "shlex", 86 | ] 87 | 88 | [[package]] 89 | name = "cfg-if" 90 | version = "1.0.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 93 | 94 | [[package]] 95 | name = "colorchoice" 96 | version = "1.0.4" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 99 | 100 | [[package]] 101 | name = "console" 102 | version = "0.15.11" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 105 | dependencies = [ 106 | "encode_unicode", 107 | "libc", 108 | "once_cell", 109 | "windows-sys", 110 | ] 111 | 112 | [[package]] 113 | name = "cpufeatures" 114 | version = "0.2.17" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 117 | dependencies = [ 118 | "libc", 119 | ] 120 | 121 | [[package]] 122 | name = "crossbeam-deque" 123 | version = "0.8.6" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 126 | dependencies = [ 127 | "crossbeam-epoch", 128 | "crossbeam-utils", 129 | ] 130 | 131 | [[package]] 132 | name = "crossbeam-epoch" 133 | version = "0.9.18" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 136 | dependencies = [ 137 | "crossbeam-utils", 138 | ] 139 | 140 | [[package]] 141 | name = "crossbeam-utils" 142 | version = "0.8.21" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 145 | 146 | [[package]] 147 | name = "crypto-common" 148 | version = "0.1.6" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 151 | dependencies = [ 152 | "generic-array", 153 | "typenum", 154 | ] 155 | 156 | [[package]] 157 | name = "diff" 158 | version = "0.1.13" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 161 | 162 | [[package]] 163 | name = "digest" 164 | version = "0.10.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 167 | dependencies = [ 168 | "block-buffer", 169 | "crypto-common", 170 | ] 171 | 172 | [[package]] 173 | name = "either" 174 | version = "1.15.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 177 | 178 | [[package]] 179 | name = "encode_unicode" 180 | version = "1.0.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 183 | 184 | [[package]] 185 | name = "env_filter" 186 | version = "0.1.3" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 189 | dependencies = [ 190 | "log", 191 | "regex", 192 | ] 193 | 194 | [[package]] 195 | name = "env_logger" 196 | version = "0.11.8" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 199 | dependencies = [ 200 | "anstream", 201 | "anstyle", 202 | "env_filter", 203 | "jiff", 204 | "log", 205 | ] 206 | 207 | [[package]] 208 | name = "errno" 209 | version = "0.3.13" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 212 | dependencies = [ 213 | "libc", 214 | "windows-sys", 215 | ] 216 | 217 | [[package]] 218 | name = "fastrand" 219 | version = "2.3.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 222 | 223 | [[package]] 224 | name = "generic-array" 225 | version = "0.14.7" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 228 | dependencies = [ 229 | "typenum", 230 | "version_check", 231 | ] 232 | 233 | [[package]] 234 | name = "getrandom" 235 | version = "0.3.3" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 238 | dependencies = [ 239 | "cfg-if", 240 | "libc", 241 | "r-efi", 242 | "wasi", 243 | ] 244 | 245 | [[package]] 246 | name = "glob" 247 | version = "0.3.2" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 250 | 251 | [[package]] 252 | name = "insta" 253 | version = "1.43.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" 256 | dependencies = [ 257 | "console", 258 | "once_cell", 259 | "similar", 260 | ] 261 | 262 | [[package]] 263 | name = "is_terminal_polyfill" 264 | version = "1.70.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 267 | 268 | [[package]] 269 | name = "itertools" 270 | version = "0.14.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 273 | dependencies = [ 274 | "either", 275 | ] 276 | 277 | [[package]] 278 | name = "itoa" 279 | version = "1.0.15" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 282 | 283 | [[package]] 284 | name = "jiff" 285 | version = "0.2.14" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 288 | dependencies = [ 289 | "jiff-static", 290 | "log", 291 | "portable-atomic", 292 | "portable-atomic-util", 293 | "serde", 294 | ] 295 | 296 | [[package]] 297 | name = "jiff-static" 298 | version = "0.2.14" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 301 | dependencies = [ 302 | "proc-macro2", 303 | "quote", 304 | "syn", 305 | ] 306 | 307 | [[package]] 308 | name = "libc" 309 | version = "0.2.172" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 312 | 313 | [[package]] 314 | name = "linux-raw-sys" 315 | version = "0.9.4" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 318 | 319 | [[package]] 320 | name = "log" 321 | version = "0.4.27" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 324 | 325 | [[package]] 326 | name = "memchr" 327 | version = "2.7.4" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 330 | 331 | [[package]] 332 | name = "once_cell" 333 | version = "1.21.3" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 336 | 337 | [[package]] 338 | name = "once_cell_polyfill" 339 | version = "1.70.1" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 342 | 343 | [[package]] 344 | name = "pest" 345 | version = "2.8.1" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 348 | dependencies = [ 349 | "memchr", 350 | "thiserror", 351 | "ucd-trie", 352 | ] 353 | 354 | [[package]] 355 | name = "pest_derive" 356 | version = "2.8.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 359 | dependencies = [ 360 | "pest", 361 | "pest_generator", 362 | ] 363 | 364 | [[package]] 365 | name = "pest_generator" 366 | version = "2.8.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 369 | dependencies = [ 370 | "pest", 371 | "pest_meta", 372 | "proc-macro2", 373 | "quote", 374 | "syn", 375 | ] 376 | 377 | [[package]] 378 | name = "pest_meta" 379 | version = "2.8.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 382 | dependencies = [ 383 | "pest", 384 | "sha2", 385 | ] 386 | 387 | [[package]] 388 | name = "portable-atomic" 389 | version = "1.11.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 392 | 393 | [[package]] 394 | name = "portable-atomic-util" 395 | version = "0.2.4" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 398 | dependencies = [ 399 | "portable-atomic", 400 | ] 401 | 402 | [[package]] 403 | name = "pretty_assertions" 404 | version = "1.4.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 407 | dependencies = [ 408 | "diff", 409 | "yansi", 410 | ] 411 | 412 | [[package]] 413 | name = "proc-macro2" 414 | version = "1.0.95" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 417 | dependencies = [ 418 | "unicode-ident", 419 | ] 420 | 421 | [[package]] 422 | name = "quote" 423 | version = "1.0.40" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 426 | dependencies = [ 427 | "proc-macro2", 428 | ] 429 | 430 | [[package]] 431 | name = "r-efi" 432 | version = "5.3.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 435 | 436 | [[package]] 437 | name = "rayon" 438 | version = "1.10.0" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 441 | dependencies = [ 442 | "either", 443 | "rayon-core", 444 | ] 445 | 446 | [[package]] 447 | name = "rayon-core" 448 | version = "1.12.1" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 451 | dependencies = [ 452 | "crossbeam-deque", 453 | "crossbeam-utils", 454 | ] 455 | 456 | [[package]] 457 | name = "regex" 458 | version = "1.11.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 461 | dependencies = [ 462 | "aho-corasick", 463 | "memchr", 464 | "regex-automata", 465 | "regex-syntax", 466 | ] 467 | 468 | [[package]] 469 | name = "regex-automata" 470 | version = "0.4.9" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 473 | dependencies = [ 474 | "aho-corasick", 475 | "memchr", 476 | "regex-syntax", 477 | ] 478 | 479 | [[package]] 480 | name = "regex-syntax" 481 | version = "0.8.5" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 484 | 485 | [[package]] 486 | name = "rustix" 487 | version = "1.0.7" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 490 | dependencies = [ 491 | "bitflags", 492 | "errno", 493 | "libc", 494 | "linux-raw-sys", 495 | "windows-sys", 496 | ] 497 | 498 | [[package]] 499 | name = "ryu" 500 | version = "1.0.20" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 503 | 504 | [[package]] 505 | name = "serde" 506 | version = "1.0.219" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 509 | dependencies = [ 510 | "serde_derive", 511 | ] 512 | 513 | [[package]] 514 | name = "serde_derive" 515 | version = "1.0.219" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 518 | dependencies = [ 519 | "proc-macro2", 520 | "quote", 521 | "syn", 522 | ] 523 | 524 | [[package]] 525 | name = "serde_json" 526 | version = "1.0.140" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 529 | dependencies = [ 530 | "itoa", 531 | "memchr", 532 | "ryu", 533 | "serde", 534 | ] 535 | 536 | [[package]] 537 | name = "sha2" 538 | version = "0.10.9" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 541 | dependencies = [ 542 | "cfg-if", 543 | "cpufeatures", 544 | "digest", 545 | ] 546 | 547 | [[package]] 548 | name = "shlex" 549 | version = "1.3.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 552 | 553 | [[package]] 554 | name = "similar" 555 | version = "2.7.0" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 558 | 559 | [[package]] 560 | name = "syn" 561 | version = "2.0.101" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 564 | dependencies = [ 565 | "proc-macro2", 566 | "quote", 567 | "unicode-ident", 568 | ] 569 | 570 | [[package]] 571 | name = "tempfile" 572 | version = "3.20.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 575 | dependencies = [ 576 | "fastrand", 577 | "getrandom", 578 | "once_cell", 579 | "rustix", 580 | "windows-sys", 581 | ] 582 | 583 | [[package]] 584 | name = "thiserror" 585 | version = "2.0.12" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 588 | dependencies = [ 589 | "thiserror-impl", 590 | ] 591 | 592 | [[package]] 593 | name = "thiserror-impl" 594 | version = "2.0.12" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 597 | dependencies = [ 598 | "proc-macro2", 599 | "quote", 600 | "syn", 601 | ] 602 | 603 | [[package]] 604 | name = "tikv-jemalloc-sys" 605 | version = "0.5.4+5.3.0-patched" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "9402443cb8fd499b6f327e40565234ff34dbda27460c5b47db0db77443dd85d1" 608 | dependencies = [ 609 | "cc", 610 | "libc", 611 | ] 612 | 613 | [[package]] 614 | name = "tikv-jemallocator" 615 | version = "0.5.4" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "965fe0c26be5c56c94e38ba547249074803efd52adfb66de62107d95aab3eaca" 618 | dependencies = [ 619 | "libc", 620 | "tikv-jemalloc-sys", 621 | ] 622 | 623 | [[package]] 624 | name = "typenum" 625 | version = "1.18.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 628 | 629 | [[package]] 630 | name = "ucd-trie" 631 | version = "0.1.7" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 634 | 635 | [[package]] 636 | name = "unicode-ident" 637 | version = "1.0.18" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 640 | 641 | [[package]] 642 | name = "unicode-segmentation" 643 | version = "1.12.0" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 646 | 647 | [[package]] 648 | name = "unicode-width" 649 | version = "0.2.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 652 | 653 | [[package]] 654 | name = "utf8parse" 655 | version = "0.2.2" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 658 | 659 | [[package]] 660 | name = "version_check" 661 | version = "0.9.5" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 664 | 665 | [[package]] 666 | name = "vicut" 667 | version = "0.4.2" 668 | dependencies = [ 669 | "bitflags", 670 | "env_logger", 671 | "glob", 672 | "insta", 673 | "itertools", 674 | "log", 675 | "pest", 676 | "pest_derive", 677 | "pretty_assertions", 678 | "rayon", 679 | "regex", 680 | "serde", 681 | "serde_json", 682 | "tempfile", 683 | "tikv-jemallocator", 684 | "unicode-segmentation", 685 | "unicode-width", 686 | ] 687 | 688 | [[package]] 689 | name = "wasi" 690 | version = "0.14.2+wasi-0.2.4" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 693 | dependencies = [ 694 | "wit-bindgen-rt", 695 | ] 696 | 697 | [[package]] 698 | name = "windows-sys" 699 | version = "0.59.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 702 | dependencies = [ 703 | "windows-targets", 704 | ] 705 | 706 | [[package]] 707 | name = "windows-targets" 708 | version = "0.52.6" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 711 | dependencies = [ 712 | "windows_aarch64_gnullvm", 713 | "windows_aarch64_msvc", 714 | "windows_i686_gnu", 715 | "windows_i686_gnullvm", 716 | "windows_i686_msvc", 717 | "windows_x86_64_gnu", 718 | "windows_x86_64_gnullvm", 719 | "windows_x86_64_msvc", 720 | ] 721 | 722 | [[package]] 723 | name = "windows_aarch64_gnullvm" 724 | version = "0.52.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 727 | 728 | [[package]] 729 | name = "windows_aarch64_msvc" 730 | version = "0.52.6" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 733 | 734 | [[package]] 735 | name = "windows_i686_gnu" 736 | version = "0.52.6" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 739 | 740 | [[package]] 741 | name = "windows_i686_gnullvm" 742 | version = "0.52.6" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 745 | 746 | [[package]] 747 | name = "windows_i686_msvc" 748 | version = "0.52.6" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 751 | 752 | [[package]] 753 | name = "windows_x86_64_gnu" 754 | version = "0.52.6" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 757 | 758 | [[package]] 759 | name = "windows_x86_64_gnullvm" 760 | version = "0.52.6" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 763 | 764 | [[package]] 765 | name = "windows_x86_64_msvc" 766 | version = "0.52.6" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 769 | 770 | [[package]] 771 | name = "wit-bindgen-rt" 772 | version = "0.39.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 775 | dependencies = [ 776 | "bitflags", 777 | ] 778 | 779 | [[package]] 780 | name = "yansi" 781 | version = "1.0.1" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 784 | -------------------------------------------------------------------------------- /src/tests/modes/normal.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::{vicut_integration, LOREM_IPSUM_MULTILINE}; 2 | 3 | use super::*; 4 | 5 | /* 6 | * Verbs: 7 | gv 8 | g~ 9 | gu 10 | gU 11 | g? 12 | . 13 | x 14 | X 15 | s 16 | S 17 | p 18 | P 19 | > 20 | < 21 | : 22 | / 23 | ? 24 | r 25 | R 26 | ~ 27 | u 28 | v 29 | V 30 | o 31 | O 32 | a 33 | A 34 | i 35 | I 36 | J 37 | y 38 | d 39 | c 40 | Y 41 | D 42 | C 43 | = 44 | */ 45 | 46 | /* 47 | * Motions: 48 | Doubled verbs for whole line motion 49 | b 50 | B 51 | f 52 | F 53 | t 54 | T 55 | n 56 | N 57 | % 58 | G 59 | ; 60 | , 61 | | 62 | ^ 63 | 0 64 | $ 65 | k 66 | j 67 | h 68 | l 69 | w 70 | W 71 | e 72 | E 73 | v 74 | V 75 | ]) 76 | ]} 77 | [) 78 | [} 79 | ) 80 | ( 81 | } 82 | { 83 | iw 84 | aw 85 | iW 86 | aW 87 | is 88 | as 89 | ip 90 | ap 91 | i" 92 | a" 93 | i' 94 | a' 95 | i` 96 | a` 97 | i) 98 | a) 99 | i} 100 | a} 101 | i] 102 | a] 103 | i> 104 | a> 105 | gg 106 | ge 107 | gE 108 | gk 109 | gj 110 | g_ 111 | g0 112 | g^ 113 | */ 114 | 115 | #[test] 116 | fn vimode_normal_structures() { 117 | let raw = "d2wg?5b2P5x"; 118 | let mut mode = ViNormal::new(); 119 | let cmds = mode.cmds_from_raw(raw); 120 | insta::assert_debug_snapshot!(cmds) 121 | } 122 | 123 | #[test] 124 | fn forward_word() { 125 | vicut_integration( 126 | "foo bar biz", 127 | &[ 128 | "-c", "w" 129 | ], 130 | "foo b", 131 | ); 132 | } 133 | 134 | #[test] 135 | fn end_of_word() { 136 | vicut_integration( 137 | "foo bar biz", 138 | &[ 139 | "-c", "e" 140 | ], 141 | "foo", 142 | ); 143 | } 144 | 145 | #[test] 146 | fn end_of_prev_word() { 147 | vicut_integration( 148 | "foo bar biz", 149 | &[ 150 | "-m", "w", 151 | "-c", "ge" 152 | ], 153 | "o b", 154 | ); 155 | } 156 | 157 | #[test] 158 | fn start_of_prev_word() { 159 | vicut_integration( 160 | "foo bar biz", 161 | &[ 162 | "-m", "w", 163 | "-c", "b" 164 | ], 165 | "foo b", 166 | ); 167 | } 168 | 169 | //f 170 | 171 | #[test] 172 | fn normal_rot13_findchar() { 173 | vicut_integration( 174 | 175 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 176 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 177 | Duis aute irure dolor in reprehenderit in voluptate iryit esse cillum dolore eu fugiat nulla pariatur. 178 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 179 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 180 | 181 | &[ 182 | "-m", "2j8w", 183 | "-m", "g?fg", 184 | ], 185 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 186 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 187 | Duis aute irure dolor in reprehenderit in voluptate velvg rffr pvyyhz qbyber rh shtiat nulla pariatur. 188 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 189 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 190 | ); 191 | } 192 | 193 | //F 194 | 195 | #[test] 196 | fn normal_to_upper_findcharrev() { 197 | vicut_integration( 198 | 199 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 200 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 201 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 202 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 203 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 204 | 205 | &[ 206 | "-m", "2j8w", 207 | "-m", "gUFh", 208 | ], 209 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 210 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 211 | Duis aute irure dolor in repreHENDERIT IN VOLUPTATE velit esse cillum dolore eu fugiat nulla pariatur. 212 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 213 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 214 | ); 215 | } 216 | 217 | 218 | //t 219 | 220 | #[test] 221 | fn normal_togglecase_tochar() { 222 | vicut_integration( 223 | 224 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 225 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 226 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 227 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 228 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 229 | 230 | &[ 231 | "-m", "2j8w", 232 | "-m", "g~ta", 233 | ], 234 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 235 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 236 | Duis aute irure dolor in reprehenderit in voluptate VELIT ESSE CILLUM DOLORE EU FUGIat nulla pariatur. 237 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 238 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 239 | ); 240 | } 241 | 242 | //T 243 | 244 | #[test] 245 | fn normal_delete_tocharrev() { 246 | vicut_integration( 247 | 248 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 249 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 250 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 251 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 252 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 253 | 254 | &[ 255 | "-m", "2j8w", 256 | "-m", "dTD", 257 | ], 258 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 259 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 260 | Dvelit esse cillum dolore eu fugiat nulla pariatur. 261 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 262 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 263 | ); 264 | } 265 | 266 | //n 267 | 268 | #[test] 269 | fn normal_delete_to_next_match() { 270 | vicut_integration( 271 | 272 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 273 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 274 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 275 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 276 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 277 | 278 | &[ 279 | "-m", "/velitgg0", 280 | "-m", "dn", 281 | ], 282 | "velit esse cillum dolore eu fugiat nulla pariatur. 283 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 284 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 285 | ); 286 | } 287 | //N 288 | 289 | #[test] 290 | fn normal_prev_match() { 291 | vicut_integration( 292 | 293 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 294 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 295 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 296 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 297 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 298 | 299 | &[ 300 | "-m", "/velitG$", 301 | "-m", "dN", 302 | ], 303 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 304 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 305 | Duis aute irure dolor in reprehenderit in voluptate .", 306 | ); 307 | } 308 | //% 309 | 310 | #[test] 311 | fn normal_matching_delim() { 312 | vicut_integration( 313 | 314 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 315 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 316 | Duis aute irure dolor in reprehenderit in voluptate velit esse { cillum dolore eu fugiat nulla } pariatur. 317 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 318 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 319 | 320 | &[ 321 | "-m", "2j8w", 322 | "-m", "d%", 323 | ], 324 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 325 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 326 | Duis aute irure dolor in reprehenderit in voluptate pariatur. 327 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 328 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 329 | ); 330 | } 331 | //G 332 | 333 | #[test] 334 | fn normal_end_of_buffer() { 335 | vicut_integration( 336 | 337 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 338 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 339 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 340 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 341 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 342 | 343 | &[ 344 | "-m", "2j8w", 345 | "-m", "dG", 346 | ], 347 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 348 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 349 | 350 | ); 351 | } 352 | //; 353 | 354 | #[test] 355 | fn normal_search_repeat() { 356 | vicut_integration( 357 | 358 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 359 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 360 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 361 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 362 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 363 | 364 | &[ 365 | "-m", "2j8wfe", 366 | "-m", "d;", 367 | ], 368 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 369 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 370 | Duis aute irure dolor in reprehenderit in voluptate vsse cillum dolore eu fugiat nulla pariatur. 371 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 372 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 373 | ); 374 | } 375 | //, 376 | 377 | #[test] 378 | fn normal_search_repeat_rev() { 379 | vicut_integration( 380 | 381 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 382 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 383 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 384 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 385 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 386 | 387 | &[ 388 | "-m", "2j8wfe", 389 | "-m", "d,", 390 | ], 391 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 392 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 393 | Duis aute irure dolor in reprehenderit in voluptatelit esse cillum dolore eu fugiat nulla pariatur. 394 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 395 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 396 | ); 397 | } 398 | //| 399 | 400 | #[test] 401 | fn normal_to_column() { 402 | vicut_integration( 403 | 404 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 405 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 406 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 407 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 408 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 409 | 410 | &[ 411 | "-m", "2j8w", 412 | "-m", "d5|", 413 | ], 414 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 415 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 416 | Duisvelit esse cillum dolore eu fugiat nulla pariatur. 417 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 418 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 419 | ); 420 | } 421 | //^ 422 | 423 | #[test] 424 | fn normal_to_first_non_whitespace() { 425 | vicut_integration( 426 | 427 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 428 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 429 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 430 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 431 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 432 | 433 | &[ 434 | "-m", "2j8w", 435 | "-m", "d^", 436 | ], 437 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 438 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 439 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. 440 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 441 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 442 | ); 443 | } 444 | //0 445 | 446 | #[test] 447 | fn normal_to_start_of_line() { 448 | vicut_integration( 449 | 450 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 451 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 452 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 453 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 454 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 455 | 456 | &[ 457 | "-m", "2j8w", 458 | "-m", "d0", 459 | ], 460 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 461 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 462 | velit esse cillum dolore eu fugiat nulla pariatur. 463 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 464 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 465 | ); 466 | } 467 | //$ 468 | 469 | #[test] 470 | fn normal_to_end_of_line() { 471 | vicut_integration( 472 | 473 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 474 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 475 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 476 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 477 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 478 | 479 | &[ 480 | "-m", "2j8w", 481 | "-m", "d$", 482 | ], 483 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 484 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 485 | Duis aute irure dolor in reprehenderit in voluptate \nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 486 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 487 | ); 488 | } 489 | //k 490 | 491 | #[test] 492 | fn normal_up_line() { 493 | vicut_integration( 494 | 495 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 496 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 497 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 498 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 499 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 500 | 501 | &[ 502 | "-m", "2j8w", 503 | "-m", "dk", 504 | ], 505 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 506 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 507 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 508 | ); 509 | } 510 | //j 511 | 512 | #[test] 513 | fn normal_down_line() { 514 | vicut_integration( 515 | 516 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 517 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 518 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 519 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 520 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 521 | 522 | &[ 523 | "-m", "2j8w", 524 | "-m", "dj", 525 | ], 526 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 527 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 528 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 529 | ); 530 | } 531 | //h 532 | 533 | #[test] 534 | fn normal_backward_char() { 535 | vicut_integration( 536 | 537 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 538 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 539 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 540 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 541 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 542 | 543 | &[ 544 | "-m", "2j8w", 545 | "-m", "dh", 546 | ], 547 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 548 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 549 | Duis aute irure dolor in reprehenderit in voluptatevelit esse cillum dolore eu fugiat nulla pariatur. 550 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 551 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 552 | ); 553 | } 554 | //l 555 | 556 | #[test] 557 | fn normal_forward_char() { 558 | vicut_integration( 559 | 560 | LOREM_IPSUM_MULTILINE, 561 | &[ 562 | "-m", "2j8w", 563 | "-m", "dl", 564 | ], 565 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 566 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 567 | Duis aute irure dolor in reprehenderit in voluptate elit esse cillum dolore eu fugiat nulla pariatur. 568 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 569 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 570 | ); 571 | } 572 | //v 573 | 574 | #[test] 575 | fn normal_visual_selection() { 576 | vicut_integration( 577 | 578 | LOREM_IPSUM_MULTILINE, 579 | &[ 580 | "-m", "2j8w", 581 | "-c", "v2w3l", 582 | ], 583 | "velit esse cill", 584 | ); 585 | } 586 | //]) 587 | 588 | #[test] 589 | fn normal_to_unmatched_paren() { 590 | vicut_integration( 591 | 592 | "This text (has stuff inside) of parenthesis", 593 | &[ 594 | "-m", "4w", 595 | "-m", "d])", 596 | ], 597 | "This text (has ) of parenthesis" 598 | ); 599 | } 600 | //]} 601 | 602 | #[test] 603 | fn normal_to_unmatched_brace() { 604 | vicut_integration( 605 | 606 | "This text {has stuff inside} of braces", 607 | &[ 608 | "-m", "4w", 609 | "-m", "d]}", 610 | ], 611 | "This text {has } of braces" 612 | ); 613 | } 614 | //[) 615 | 616 | #[test] 617 | fn normal_to_unmatched_paren_rev() { 618 | vicut_integration( 619 | 620 | "This text (has stuff inside) of parenthesis", 621 | &[ 622 | "-m", "5w", 623 | "-m", "d[)", 624 | ], 625 | "This text inside) of parenthesis", 626 | ); 627 | } 628 | //[} 629 | 630 | #[test] 631 | fn normal_to_unmatched_brace_rev() { 632 | vicut_integration( 633 | 634 | "This text {has stuff inside} of braces", 635 | &[ 636 | "-m", "5w", 637 | "-m", "d[}", 638 | ], 639 | "This text inside} of braces" 640 | ); 641 | } 642 | //) 643 | 644 | #[test] 645 | fn normal_sentence_forward() { 646 | vicut_integration( 647 | 648 | LOREM_IPSUM_MULTILINE, 649 | &[ 650 | "-m", "2j8w", 651 | "-m", "d)", 652 | ], 653 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 654 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 655 | Duis aute irure dolor in reprehenderit in voluptate \nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 656 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 657 | ); 658 | } 659 | //( 660 | 661 | #[test] 662 | fn normal_sentence_backward() { 663 | vicut_integration( 664 | 665 | LOREM_IPSUM_MULTILINE, 666 | &[ 667 | "-m", "2j8w", 668 | "-m", "d(", 669 | ], 670 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 671 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 672 | velit esse cillum dolore eu fugiat nulla pariatur. 673 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 674 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 675 | ); 676 | } 677 | //} 678 | 679 | #[test] 680 | #[ignore] 681 | fn normal_paragraph_forward() { 682 | vicut_integration( 683 | 684 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 685 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 686 | 687 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 688 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 689 | 690 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 691 | &[ 692 | "-m", "3j8w", 693 | "-m", "d}", 694 | ], 695 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 696 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 697 | 698 | Duis aute irure dolor in reprehenderit in voluptate 699 | 700 | Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.", 701 | ); 702 | } 703 | --------------------------------------------------------------------------------