5

Lets say we are editing this totally made up JSON file:

[{
  "id_4f7xg4egb": "<some_random_guid",
  // ... other fields
}, {
  "id_h34k3": "<another_id>",
  // ... different fields than prev object potentially
  "nested": {
    "id_j3h": "<nested_obj_id>",
    // ... nested obj fields
  }
},
// ...
]

It contains N objects (including the nested ones), and we'd like to replace the id value with the string coming appended to the field key itself. The result would be something like:

[{
  "id": "4f7xg4egb",
  // ... other fields
}, {
  "id": "h34k3",
  // ... different fields than prev object potentially
  "nested": {
    "id": "j3h",
    // ... nested obj fields
  }
},
// ...
]

Now, here is what I would do in VSCode using multiple cursors:

With VSCode Ctrl+d

  • Select first occurrence of "id_ with Shift+x4
  • Select (and create a cursor on) every occurrence of the remaining N-1 "id_ with Ctrl+dx(N-1)
  • Select the id string(s) following "id_ with , (to deselect), Ctrl+Shift+
  • Cut them with Ctrl+x (yes, each cursor gets its own "clipboard")
  • Delete the _ with Backspace
  • Select the portion we want to replace ("<every_diff_id>") with x4,Shift+End, x2
  • Replace the value with Ctrl+v and Esc to get rid of the extra cursors

So we are talking about 5+N+5+2+1+8+3=N+24=o(N) keystrokes (taking into account the fact that, for example, the Ctrl key is counted only once and then held down for the rest of the N-1 Ctrl+d commands).

With the Power of Vim

And... my question is: How to accomplish the same result using Vim (in =< # of operations, ofc)?! I'm a noob, one week old vimmer, and I'm loving it so far! I've been using . and basic macros for similar tasks, but I'm not sure what would be the most efficient way to tackle this one :(. I'd also prefer a solution NOT using plugins or involving adding some complicated mappings/functions to my .vimrc. After all, the VSCode solution is vanilla VSCode ;).

Thanks!

joeveiga
  • 78
  • 1
  • 5
  • 1
    Not an answer, but [kakoune](https://kakoune.org/) is a vim-like editor that makes extensive use of multiple cursors (as does [micro](https://micro-editor.github.io/)). – larsks Dec 24 '20 at 04:15
  • For simple text substitutions, you can just do a global substitution using :%s/old/new/g. For more complex operations, just recording and executing a macro is probably the way to go. – MAK Dec 24 '20 at 04:17
  • If willing to use vim keybindings in vscode using the vscodevim plugin then you can edit like in vim with this feature added. More details: https://stackoverflow.com/a/76358638/2826818 – alexbhandari May 29 '23 at 15:46

4 Answers4

3

By recording a macro and replaying it N times.

For example:

qa/"id_/e<CR>s": "<Esc>f:dt,q2@a

That's a total of 25 keystrokes.

Breaking it down:

  • qa: Start recording macro @a.
  • /"id_/e: Search for pattern id_ and place cursor at the end of the pattern. The <CR> executes the search.
  • s: Replace the character under the cursor (the _), starting insert mode. Then use ": " as the replacement text (produces "id": "... and the <Esc> leaves insert mode.
  • f: Move forward to the : after the field. dt,: Delete until the comma (preserving it.)
  • q: Stop recording the macro.
  • 2@a: Execute macro @a two times. (If there were more id's to replace, execute it more times. Use 999@a to execute up to 999 times, or until there's an error, whatever comes first.)

I agree that multiple cursors are pretty visual and might be easier to grasp... Vim macros can be somewhat abstract and require you to think of what these operations would do to the other positions you apply the macro to...

For this particular situation, perhaps using a :s command would be easier. But that's a good thing of Vim, there are often multiple ways to solve a problem and you can choose the one you prefer.

filbranden
  • 8,522
  • 2
  • 16
  • 32
  • 1
    Thanks @filbranden, that's sorta what I was trying to do with a macro approach. The count before the `@a` was a great tip! I totally forgot about that one. And it makes it a fixed number of operations regardless of the N occurrences ;). – joeveiga Dec 24 '20 at 11:31
  • We'll, technically it's O(log N), log base 10. It's 2 keystrokes for a two digit N, three for a 3 digit N, etc. – filbranden Dec 24 '20 at 14:18
2

I would do it in two intuitive passes and not worry about counting keystrokes or beating the wall clock.

First pass:

:g/id/norm f_s": "
  • Use :global to execute a command on every line matching a pattern.
  • The pattern is id.
  • The command is :normal followed by a normal mode macro.
  • The macro…
    • moves the cursor to the first _ on the line with f_,
    • and substitutes it with ": ".

See :help :global, :help :normal, :help f, and :help s.

Second pass:

:g//norm f:;C,
  • Use :global to execute a command on every line matching a pattern.
  • The pattern is the same so it can be omitted.
  • The command is :normal followed by a normal mode macro.
  • The macro…
    • moves the cursor to the first : on the line with f:,
    • then repeats the move to get to the second : with ;,
    • replaces the rest of the line with a ,.

See :help ; and :help C.

romainl
  • 186,200
  • 21
  • 280
  • 313
  • 1
    Thanks @romainl. That `:normal` after a search was mind blowing, I'll check it out! And keystrokes aren't as much a concern as just a way to measure productivity, I guess :). – joeveiga Dec 24 '20 at 11:45
  • Nice one! Inverting the order of operations would get you to do it in a single step: `:g/id/norm f:dt,F_s": "`. Using `:normal` with a range is amazingly powerful! – filbranden Dec 24 '20 at 16:09
  • 1
    Thanks @filbranden, your inverting trick is a good idea. I generally favour intuitiveness over cleverness, though. Progressively splitting the task at hand in *n* easy to figure problems makes for a less bumpy resolution in my opinion. – romainl Dec 24 '20 at 17:00
  • @romainl Totally agreed. In my case, I guess it's professional deformation from playing too much [Vimgolf](https://www.vimgolf.com/) – filbranden Dec 24 '20 at 17:14
  • @romainl I haven't had the chance to check `:help :global` (I'm afk right now and the web version is not what one would call mobile friendly ). But I did find an article called Power of G that was pretty cool, and also this SO answer that I found to be very enlightening: https://stackoverflow.com/a/61240475/14881244 – joeveiga Dec 24 '20 at 19:55
  • 1
    @filbranden, Vimgolf, why did you bring that to my attention? Why?! Now all my productivity gains from switching to Vim will be offset by the time I'll waste on that one! NOOO! – joeveiga Dec 24 '20 at 20:03
1

I would probably just do something like this:

:% s/"id_\([^"]*\)": .*,/"id": "\1"/

That's a global search-and-replace, matching the "id_<something>": <stuff>, and replacing it with "id": "<something>". I think of that as a single "operation", but not sure if that's what you were looking for.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • 1
    Thanks @larsks! Looks like the cleanest solution, however I accepted the one offered by @filbranden for a couple of reasons: 1) by my weird keystrokes metric, that one does better lol, 2) for a noob like myself it's easier to replay a set of steps with a macro than to figure out the correct regex replace pattern (something I need to work on), and 3) he explained how it worked xD. – joeveiga Dec 24 '20 at 12:02
  • My solution is similar `:%s/"id_\v([^"]*)":.*\ze,/"id": "\1"` – SergioAraujo Dec 25 '20 at 22:39
0

Using global command becomes:

:g/"id/exe 'norm! f_xdt"f<vi"p'

where we find "id
normal mode
f_  .................... jump to underscore
x ...................... cut
dt" .................... deletes until " (goes to unnamed register)
f<  .................... jumps to <
vi" .................... visual select inner "
p ...................... paste default register

I have posted this solution but I think filbranden's is better.

SergioAraujo
  • 11,069
  • 3
  • 50
  • 40