diff options
-rw-r--r-- | posts/rapid_refactoring_with_vim.md | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/posts/rapid_refactoring_with_vim.md b/posts/rapid_refactoring_with_vim.md new file mode 100644 index 0000000..66ca0e3 --- /dev/null +++ b/posts/rapid_refactoring_with_vim.md | |||
@@ -0,0 +1,198 @@ | |||
1 | Last weekend, I was tasked with refactoring the 96 unit | ||
2 | tests on | ||
3 | [ruma-events](https://github.com/ruma/ruma-events/pull/70) | ||
4 | to use strictly typed json objects using `serde_json::json!` | ||
5 | instead of raw strings. It was rather painless thanks to | ||
6 | vim :) | ||
7 | |||
8 | Here's a small sample of what had to be done (note the lines | ||
9 | prefixed with the arrow): | ||
10 | |||
11 | ``` | ||
12 | → use serde_json::{from_str}; | ||
13 | |||
14 | #[test] | ||
15 | fn deserialize() { | ||
16 | assert_eq!( | ||
17 | → from_str::<Action>(r#"{"set_tweak": "highlight"}"#), | ||
18 | Action::SetTweak(Tweak::Highlight { value: true }) | ||
19 | ); | ||
20 | } | ||
21 | ``` | ||
22 | |||
23 | had to be converted to: | ||
24 | |||
25 | ``` | ||
26 | → use serde_json::{from_value}; | ||
27 | |||
28 | #[test] | ||
29 | fn deserialize() { | ||
30 | assert_eq!( | ||
31 | → from_value::<Action>(json!({"set_tweak": "highlight"})), | ||
32 | Action::SetTweak(Tweak::Highlight { value: true }) | ||
33 | ); | ||
34 | } | ||
35 | ``` | ||
36 | |||
37 | ## The arglist | ||
38 | |||
39 | For the initial pass, I decided to handle imports, this was | ||
40 | a simple find and replace operation, done to all the files | ||
41 | containing tests. Luckily, modules (and therefore files) | ||
42 | containing tests in Rust are annotated with the | ||
43 | `#[cfg(test)]` attribute. I opened all such files: | ||
44 | |||
45 | ``` | ||
46 | # `grep -l pattern files` lists all the files | ||
47 | # matching the pattern | ||
48 | |||
49 | vim $(grep -l 'cfg\(test\)' ./**/*.rs) | ||
50 | |||
51 | # expands to something like: | ||
52 | vim push_rules.rs room/member.rs key/verification/lib.rs | ||
53 | ``` | ||
54 | |||
55 | Starting vim with more than one file at the shell prompt | ||
56 | populates the arglist. Hit `:args` to see the list of | ||
57 | files currently ready to edit. The square [brackets] | ||
58 | indicate the current file. Navigate through the arglist | ||
59 | with `:next` and `:prev`. I use tpope's vim-unimpaired | ||
60 | [^un], which adds `]a` and `[a`, mapped to `:next` and | ||
61 | `:prev`. | ||
62 | |||
63 | [^un]: https://github.com/tpope/vim-unimpaired | ||
64 | It also handles various other mappings, `]q` and `[q` to | ||
65 | navigate the quickfix list for example | ||
66 | |||
67 | All that's left to do is the find and replace, for which we | ||
68 | will be using vim's `argdo`, applying a substitution to | ||
69 | every file in the arglist: | ||
70 | |||
71 | ``` | ||
72 | :argdo s/from_str/from_value/g | ||
73 | ``` | ||
74 | |||
75 | ## The quickfix list | ||
76 | |||
77 | Next up, replacing `r#" ... "#` with `json!( ... )`. I | ||
78 | couldn't search and replace that trivially, so I went with a | ||
79 | macro call [^macro] instead, starting with the cursor on | ||
80 | 'r', represented by the caret, in my attempt to breakdown | ||
81 | the process: | ||
82 | |||
83 | [^macro]: `:help recording` | ||
84 | |||
85 | ``` | ||
86 | BUFFER: r#" ... "#; | ||
87 | ^ | ||
88 | |||
89 | ACTION: vllsjson!( | ||
90 | |||
91 | BUFFER json!( ... "#; | ||
92 | ^ | ||
93 | |||
94 | ACTION: <esc>$F# | ||
95 | |||
96 | BUFFER: json!( ... "#; | ||
97 | ^ | ||
98 | |||
99 | ACTION: vhs)<esc> | ||
100 | |||
101 | BUFFER: json!( ... ); | ||
102 | ``` | ||
103 | |||
104 | Here's the recorded [^rec] macro in all its glory: | ||
105 | `vllsjson!(<esc>$F#vhs)<esc>`. | ||
106 | |||
107 | [^rec]: When I'm recording a macro, I prefer starting out by | ||
108 | storing it in register `q`, and then copying it over to | ||
109 | another register if it works as intended. I think of `qq` as | ||
110 | 'quick record'. | ||
111 | |||
112 | Great! So now we just go ahead, find every occurrence of | ||
113 | `r#` and apply the macro right? Unfortunately, there were | ||
114 | more than a few occurrences of raw strings that had to stay | ||
115 | raw strings. Enter, the quickfix list. | ||
116 | |||
117 | The idea behind the quickfix list is to jump from one | ||
118 | position in a file to another (maybe in a different file), | ||
119 | much like how the arglist lets you jump from one file to | ||
120 | another. | ||
121 | |||
122 | One of the easiest ways to populate this list with a bunch | ||
123 | of positions is to use `vimgrep`: | ||
124 | |||
125 | ``` | ||
126 | # basic usage | ||
127 | :vimgrep pattern files | ||
128 | |||
129 | # search for raw strings | ||
130 | :vimgrep 'r#' ./**/*.rs | ||
131 | ``` | ||
132 | |||
133 | Like `:next` and `:prev`, you can navigate the quickfix list | ||
134 | with `:cnext` and `:cprev`. Every time you move up or down | ||
135 | the list, vim indicates your index: | ||
136 | |||
137 | ``` | ||
138 | (1 of 131): r#"{"set_tweak": "highlight"}"#; | ||
139 | ``` | ||
140 | |||
141 | And just like `argdo`, you can `cdo` to apply commands to | ||
142 | *every* match in the quickfix list: | ||
143 | |||
144 | ``` | ||
145 | :cdo norm! @q | ||
146 | ``` | ||
147 | |||
148 | But, I had to manually pick out matches, and it involved | ||
149 | some button mashing. | ||
150 | |||
151 | ## External Filtering | ||
152 | |||
153 | Some code reviews later, I was asked to format all the json | ||
154 | inside the `json!` macro. All you have to do is pass a | ||
155 | visual selection through a pretty json printer. Select the | ||
156 | range to be formatted in visual mode, and hit `:`, you will | ||
157 | notice the command line displaying what seems to be | ||
158 | gibberish: | ||
159 | |||
160 | ``` | ||
161 | :'<,'> | ||
162 | ``` | ||
163 | |||
164 | `'<` and `'>` are *marks* [^mark-motions]. More | ||
165 | specifically, they are marks that vim sets automatically | ||
166 | every time you make a visual selection, denoting the start | ||
167 | and end of the selection. | ||
168 | |||
169 | [^mark-motions]: `:help mark-motions` | ||
170 | |||
171 | A range is one or more line specifiers separated by a `,`: | ||
172 | |||
173 | ``` | ||
174 | :1,7 lines 1 through 7 | ||
175 | :32 just line 32 | ||
176 | :. the current line | ||
177 | :.,$ the current line to the last line | ||
178 | :'a,'b mark 'a' to mark 'b' | ||
179 | ``` | ||
180 | |||
181 | Most `:` commands can be prefixed by ranges. `:help | ||
182 | usr_10.txt` for more on that. | ||
183 | |||
184 | Alright, lets pass json through `python -m json.tool`, a | ||
185 | json formatter that accepts `stdin` (note the use of `!` to | ||
186 | make use of an external program): | ||
187 | |||
188 | ``` | ||
189 | :'<,'>!python -m json.tool | ||
190 | ``` | ||
191 | |||
192 | Unfortunately that didn't quite work for me because the | ||
193 | range included some non-json text as well, a mix of regex | ||
194 | and macros helped fix that. I think you get the drift. | ||
195 | |||
196 | Another fun filter I use from time to time is `:!sort`, to | ||
197 | sort css attributes, or `:!uniq` to remove repeated imports. | ||
198 | |||