From 9f9226780a3a3dfbbff28f9d7d8a10398617ec83 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 19 Jan 2021 12:13:51 -0800 Subject: [PATCH] fix(vim/#1633): Visual-block I and A implementation (#3011) __Issue:__ `I` and `A` in visual-block mode should allow inserting, or appending, characters across all linewise selections in the visual block __Fix:__ Implement using our in-progress multi-cursor support: ![2021-01-19 12 02 59](https://user-images.githubusercontent.com/13532591/105086839-7746a700-5a4e-11eb-9a44-c984d27ae6bc.gif) Functionally, this should behave the same as ``+`I`/`A`, but show all insertions during the gesture (as opposed to Vim, which would only show a single insertion, and then propagate them on the `` press). A baby step towards snippets & multiple cursors Fixes #1633 Related #1551 --- CHANGES_CURRENT.md | 1 + bench.esy.lock/index.json | 12 +-- esy.lock/index.json | 12 +-- integrationtest.esy.lock/index.json | 12 +-- package.json | 2 +- release.esy.lock/index.json | 12 +-- src/Feature/Editor/Editor.re | 6 +- src/Feature/Editor/Editor.rei | 7 +- src/Feature/Editor/Feature_Editor.re | 8 +- src/Feature/Editor/Msg.re | 2 + src/Feature/Editor/SurfaceView.re | 36 +++++--- src/reason-libvim/GlobalState.re | 2 + src/reason-libvim/Vim.re | 33 +++++-- src/reason-libvim/bindings.c | 14 +++ test.esy.lock/index.json | 12 +-- test/reason-libvim/ModeTest.re | 18 ++++ test/reason-libvim/MultiCursorTest.re | 123 +++++++++++++++++++++++++- 17 files changed, 255 insertions(+), 57 deletions(-) diff --git a/CHANGES_CURRENT.md b/CHANGES_CURRENT.md index 7a7d689bcd..107ea997bb 100644 --- a/CHANGES_CURRENT.md +++ b/CHANGES_CURRENT.md @@ -4,6 +4,7 @@ - #3008 - SCM: Fix index-out-of-bound exception when rendering diff markers - #3007 - Extensions: Show 'missing dependency' activation error to user +- #3011 - Vim: Visual Block - Handle 'I' and 'A' in visual block mode (fixes #1633) ### Performance diff --git a/bench.esy.lock/index.json b/bench.esy.lock/index.json index 1b9ad2012e..f4f6a2ccd6 100644 --- a/bench.esy.lock/index.json +++ b/bench.esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "1a150dab488cd4565363c1ab83fed405", + "checksum": "c0c492ffdbdf69a629c8ac01d427079b", "root": "Oni2@link-dev:./package.json", "node": { "yarn-pkg-config@github:esy-ocaml/yarn-pkg-config#db3a0b63883606dd57c54a7158d560d6cba8cd79@d41d8cd9": { @@ -429,14 +429,14 @@ "dependencies": [], "devDependencies": [] }, - "libvim@8.10869.83@d41d8cd9": { - "id": "libvim@8.10869.83@d41d8cd9", + "libvim@8.10869.85@d41d8cd9": { + "id": "libvim@8.10869.85@d41d8cd9", "name": "libvim", - "version": "8.10869.83", + "version": "8.10869.85", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.83.tgz#sha1:dc8f49e86d26d085ece7fbe2418e6a5f0ff63a33" + "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.85.tgz#sha1:2666c88555e94e56c8415789e8947670e6e299a8" ] }, "overrides": [], @@ -1002,7 +1002,7 @@ "refmterr@3.3.2@d41d8cd9", "reason-native-crash-utils@github:onivim/reason-native-crash-utils#38c8f00@d41d8cd9", "reason-fzy@github:bryphe/reason-fzy#ab172e1@d41d8cd9", - "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.83@d41d8cd9", + "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.85@d41d8cd9", "isolinear@github:revery-ui/isolinear#53fc4eb@d41d8cd9", "esy-tree-sitter@1.4.1@d41d8cd9", "esy-skia@github:revery-ui/esy-skia#91c98f6@d41d8cd9", diff --git a/esy.lock/index.json b/esy.lock/index.json index e0da7ffd09..94873b7069 100644 --- a/esy.lock/index.json +++ b/esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "1a150dab488cd4565363c1ab83fed405", + "checksum": "c0c492ffdbdf69a629c8ac01d427079b", "root": "Oni2@link-dev:./package.json", "node": { "yarn-pkg-config@github:esy-ocaml/yarn-pkg-config#db3a0b63883606dd57c54a7158d560d6cba8cd79@d41d8cd9": { @@ -429,14 +429,14 @@ "dependencies": [], "devDependencies": [] }, - "libvim@8.10869.83@d41d8cd9": { - "id": "libvim@8.10869.83@d41d8cd9", + "libvim@8.10869.85@d41d8cd9": { + "id": "libvim@8.10869.85@d41d8cd9", "name": "libvim", - "version": "8.10869.83", + "version": "8.10869.85", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.83.tgz#sha1:dc8f49e86d26d085ece7fbe2418e6a5f0ff63a33" + "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.85.tgz#sha1:2666c88555e94e56c8415789e8947670e6e299a8" ] }, "overrides": [], @@ -1001,7 +1001,7 @@ "refmterr@3.3.2@d41d8cd9", "reason-native-crash-utils@github:onivim/reason-native-crash-utils#38c8f00@d41d8cd9", "reason-fzy@github:bryphe/reason-fzy#ab172e1@d41d8cd9", - "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.83@d41d8cd9", + "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.85@d41d8cd9", "isolinear@github:revery-ui/isolinear#53fc4eb@d41d8cd9", "esy-tree-sitter@1.4.1@d41d8cd9", "esy-skia@github:revery-ui/esy-skia#91c98f6@d41d8cd9", diff --git a/integrationtest.esy.lock/index.json b/integrationtest.esy.lock/index.json index 415cbf2287..567348035a 100644 --- a/integrationtest.esy.lock/index.json +++ b/integrationtest.esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "1a150dab488cd4565363c1ab83fed405", + "checksum": "c0c492ffdbdf69a629c8ac01d427079b", "root": "Oni2@link-dev:./package.json", "node": { "yarn-pkg-config@github:esy-ocaml/yarn-pkg-config#db3a0b63883606dd57c54a7158d560d6cba8cd79@d41d8cd9": { @@ -429,14 +429,14 @@ "dependencies": [], "devDependencies": [] }, - "libvim@8.10869.83@d41d8cd9": { - "id": "libvim@8.10869.83@d41d8cd9", + "libvim@8.10869.85@d41d8cd9": { + "id": "libvim@8.10869.85@d41d8cd9", "name": "libvim", - "version": "8.10869.83", + "version": "8.10869.85", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.83.tgz#sha1:dc8f49e86d26d085ece7fbe2418e6a5f0ff63a33" + "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.85.tgz#sha1:2666c88555e94e56c8415789e8947670e6e299a8" ] }, "overrides": [], @@ -1001,7 +1001,7 @@ "refmterr@3.3.2@d41d8cd9", "reason-native-crash-utils@github:onivim/reason-native-crash-utils#38c8f00@d41d8cd9", "reason-fzy@github:bryphe/reason-fzy#ab172e1@d41d8cd9", - "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.83@d41d8cd9", + "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.85@d41d8cd9", "isolinear@github:revery-ui/isolinear#53fc4eb@d41d8cd9", "esy-tree-sitter@1.4.1@d41d8cd9", "esy-skia@github:revery-ui/esy-skia#91c98f6@d41d8cd9", diff --git a/package.json b/package.json index c207e76b8d..0920c87972 100644 --- a/package.json +++ b/package.json @@ -243,7 +243,7 @@ "esy-skia": "*", "esy-tree-sitter": "^1.4.1", "isolinear": "^3.0.0", - "libvim": "8.10869.83", + "libvim": "8.10869.85", "ocaml": "4.10.0", "reason-native-crash-utils": "onivim/reason-native-crash-utils#38c8f00", "reason-fzy": "bryphe/reason-fzy#ab172e1", diff --git a/release.esy.lock/index.json b/release.esy.lock/index.json index 0eb86da537..4d7f151a0a 100644 --- a/release.esy.lock/index.json +++ b/release.esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "1a150dab488cd4565363c1ab83fed405", + "checksum": "c0c492ffdbdf69a629c8ac01d427079b", "root": "Oni2@link-dev:./package.json", "node": { "yarn-pkg-config@github:esy-ocaml/yarn-pkg-config#db3a0b63883606dd57c54a7158d560d6cba8cd79@d41d8cd9": { @@ -429,14 +429,14 @@ "dependencies": [], "devDependencies": [] }, - "libvim@8.10869.83@d41d8cd9": { - "id": "libvim@8.10869.83@d41d8cd9", + "libvim@8.10869.85@d41d8cd9": { + "id": "libvim@8.10869.85@d41d8cd9", "name": "libvim", - "version": "8.10869.83", + "version": "8.10869.85", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.83.tgz#sha1:dc8f49e86d26d085ece7fbe2418e6a5f0ff63a33" + "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.85.tgz#sha1:2666c88555e94e56c8415789e8947670e6e299a8" ] }, "overrides": [], @@ -1001,7 +1001,7 @@ "refmterr@3.3.2@d41d8cd9", "reason-native-crash-utils@github:onivim/reason-native-crash-utils#38c8f00@d41d8cd9", "reason-fzy@github:bryphe/reason-fzy#ab172e1@d41d8cd9", - "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.83@d41d8cd9", + "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.85@d41d8cd9", "isolinear@github:revery-ui/isolinear#53fc4eb@d41d8cd9", "esy-tree-sitter@1.4.1@d41d8cd9", "esy-skia@github:revery-ui/esy-skia#91c98f6@d41d8cd9", diff --git a/src/Feature/Editor/Editor.re b/src/Feature/Editor/Editor.re index 42f81dfdd7..75e8af8754 100644 --- a/src/Feature/Editor/Editor.re +++ b/src/Feature/Editor/Editor.re @@ -1623,7 +1623,8 @@ let moveScreenLines = (~position, ~count, editor) => { ); }; -let mouseDown = (~time, ~pixelX, ~pixelY, editor) => { +let mouseDown = (~altKey, ~time, ~pixelX, ~pixelY, editor) => { + ignore(altKey); ignore(time); let bytePosition: BytePosition.t = Slow.pixelPositionToBytePosition( @@ -1673,7 +1674,8 @@ let getCharacterUnderMouse = editor => { }); }; -let mouseUp = (~time, ~pixelX, ~pixelY, editor) => { +let mouseUp = (~altKey, ~time, ~pixelX, ~pixelY, editor) => { + ignore(altKey); ignore(pixelX); ignore(pixelY); diff --git a/src/Feature/Editor/Editor.rei b/src/Feature/Editor/Editor.rei index 0814a4e5ba..cfc5ea3b52 100644 --- a/src/Feature/Editor/Editor.rei +++ b/src/Feature/Editor/Editor.rei @@ -83,6 +83,7 @@ let getCharacterAtPosition: (~position: CharacterPosition.t, t) => option(Uchar.t); let getPrimaryCursor: t => CharacterPosition.t; let getPrimaryCursorByte: t => BytePosition.t; +let cursors: t => list(BytePosition.t); let getVisibleView: t => int; let getTotalHeightInPixels: t => int; let getTotalWidthInPixels: t => float; @@ -116,8 +117,10 @@ let setMinimap: (~enabled: bool, ~maxColumn: int, t) => t; let isMinimapEnabled: t => bool; // Mouse interactions -let mouseDown: (~time: Revery.Time.t, ~pixelX: float, ~pixelY: float, t) => t; -let mouseUp: (~time: Revery.Time.t, ~pixelX: float, ~pixelY: float, t) => t; +let mouseDown: + (~altKey: bool, ~time: Revery.Time.t, ~pixelX: float, ~pixelY: float, t) => t; +let mouseUp: + (~altKey: bool, ~time: Revery.Time.t, ~pixelX: float, ~pixelY: float, t) => t; let mouseMove: (~time: Revery.Time.t, ~pixelX: float, ~pixelY: float, t) => t; let mouseEnter: t => t; let mouseLeave: t => t; diff --git a/src/Feature/Editor/Feature_Editor.re b/src/Feature/Editor/Feature_Editor.re index 6d2612cf48..331e64821b 100644 --- a/src/Feature/Editor/Feature_Editor.re +++ b/src/Feature/Editor/Feature_Editor.re @@ -99,12 +99,12 @@ let update = (editor, msg) => { | HorizontalScrollbarMouseRelease | VerticalScrollbarMouseRelease | VerticalScrollbarMouseDown => (editor, Nothing) - | EditorMouseDown({time, pixelX, pixelY}) => ( - editor |> Editor.mouseDown(~time, ~pixelX, ~pixelY), + | EditorMouseDown({altKey, time, pixelX, pixelY}) => ( + editor |> Editor.mouseDown(~altKey, ~time, ~pixelX, ~pixelY), Nothing, ) - | EditorMouseUp({time, pixelX, pixelY}) => ( - editor |> Editor.mouseUp(~time, ~pixelX, ~pixelY), + | EditorMouseUp({altKey, time, pixelX, pixelY}) => ( + editor |> Editor.mouseUp(~altKey, ~time, ~pixelX, ~pixelY), Nothing, ) | InlineElementSizeChanged({key, line, uniqueId, height}) => ( diff --git a/src/Feature/Editor/Msg.re b/src/Feature/Editor/Msg.re index 8691b42ae7..b8bb741639 100644 --- a/src/Feature/Editor/Msg.re +++ b/src/Feature/Editor/Msg.re @@ -23,6 +23,7 @@ type t = | PreviewChanged(bool) | EditorMouseEnter | EditorMouseDown({ + altKey: bool, time: [@opaque] Revery.Time.t, pixelX: float, pixelY: float, @@ -33,6 +34,7 @@ type t = pixelY: float, }) | EditorMouseUp({ + altKey: bool, time: [@opaque] Revery.Time.t, pixelX: float, pixelY: float, diff --git a/src/Feature/Editor/SurfaceView.re b/src/Feature/Editor/SurfaceView.re index 5fc643ff30..64523862c7 100644 --- a/src/Feature/Editor/SurfaceView.re +++ b/src/Feature/Editor/SurfaceView.re @@ -139,14 +139,18 @@ let%component make = let onMouseDown = (evt: NodeEvents.mouseButtonEventParams) => { getMaybeLocationFromMousePosition(evt.mouseX, evt.mouseY) |> Option.iter(((pixelX, pixelY, time)) => { - dispatch(Msg.EditorMouseDown({time, pixelX, pixelY})) + dispatch( + Msg.EditorMouseDown({altKey: evt.ctrlKey, time, pixelX, pixelY}), + ) }); }; let onMouseUp = (evt: NodeEvents.mouseButtonEventParams) => { getMaybeLocationFromMousePosition(evt.mouseX, evt.mouseY) |> Option.iter(((pixelX, pixelY, time)) => { - dispatch(Msg.EditorMouseUp({time, pixelX, pixelY})) + dispatch( + Msg.EditorMouseUp({altKey: evt.ctrlKey, time, pixelX, pixelY}), + ) }); }; @@ -165,6 +169,23 @@ let%component make = ) |> Option.value(~default=React.empty); + let cursors = + Editor.cursors(editor) + |> List.filter_map(pos => Editor.byteToCharacter(pos, editor)) + |> List.map(cursorPosition => { + + }) + |> React.listToElement; + maybeBbox := Some(bbox)} style={Styles.bufferViewClipped( @@ -248,15 +269,6 @@ let%component make = /> {lensElements |> React.listToElement} yankHighlightElement - + cursors ; }; diff --git a/src/reason-libvim/GlobalState.re b/src/reason-libvim/GlobalState.re index fdecda8dfb..449b8a16c1 100644 --- a/src/reason-libvim/GlobalState.re +++ b/src/reason-libvim/GlobalState.re @@ -36,3 +36,5 @@ let screenPositionMotion: let toggleComments: ref(option(array(string) => array(string))) = ref(None); + +let additionalCursors: ref(list(BytePosition.t)) = ref([]); diff --git a/src/reason-libvim/Vim.re b/src/reason-libvim/Vim.re index da8a21d573..1c64692528 100644 --- a/src/reason-libvim/Vim.re +++ b/src/reason-libvim/Vim.re @@ -182,6 +182,7 @@ let runWith = (~context: Context.t, f) => { GlobalState.screenPositionMotion := Some(context.screenCursorMotion); GlobalState.effects := []; GlobalState.toggleComments := Some(context.toggleComments); + GlobalState.additionalCursors := []; let mode = f(); @@ -191,14 +192,22 @@ let runWith = (~context: Context.t, f) => { GlobalState.toggleComments := None; let newBuf = Buffer.getCurrent(); - let newMode = Mode.current(); + //let newMode = Mode.current(); let newModified = Buffer.isModified(newBuf); let newLineEndings = Buffer.getLineEndings(newBuf); + // Apply additional cursors + let mode = + switch (mode) { + | Mode.Insert({cursors}) => + Mode.Insert({cursors: cursors @ GlobalState.additionalCursors^}) + | mode => mode + }; + BufferInternal.checkCurrentBufferForUpdate(); - if (newMode != prevMode) { - if (newMode == CommandLine) { + if (mode != prevMode) { + if (mode == CommandLine) { Event.dispatch( CommandLineInternal.getState(), Listeners.commandLineEnter, @@ -206,7 +215,7 @@ let runWith = (~context: Context.t, f) => { } else if (prevMode == CommandLine) { Event.dispatch((), Listeners.commandLineLeave); }; - } else if (newMode == CommandLine) { + } else if (mode == CommandLine) { Event.dispatch( CommandLineInternal.getState(), Listeners.commandLineUpdate, @@ -513,6 +522,17 @@ let _onToggleComments = (buf: Buffer.t, startLine: int, endLine: int) => { |> Option.value(~default=currentLines); }; +let _onCursorAdd = (oneBasedLine: int, column: int) => { + GlobalState.additionalCursors := + [ + BytePosition.{ + line: LineNumber.ofOneBased(oneBasedLine), + byte: ByteIndex.ofInt(column), + }, + ...GlobalState.additionalCursors^, + ]; +}; + let _onGetChar = mode => { let mode' = switch (mode) { @@ -567,6 +587,7 @@ let init = () => { Callback.register("lv_onToggleComments", _onToggleComments); Callback.register("lv_onGetChar", _onGetChar); Callback.register("lv_onOutput", _onOutput); + Callback.register("lv_onCursorAdd", _onCursorAdd); Native.vimInit(); @@ -581,7 +602,9 @@ let inputCommon = (~inputFn, ~context=Context.current(), v: string) => { () => { // Special auto-closing pairs handling... - let runCursor = cursor => { + let runCursor = (cursor: BytePosition.t) => { + let lineNumber = EditorCoreTypes.LineNumber.toOneBased(cursor.line); + Undo.saveRegion(lineNumber - 1, lineNumber + 1); Cursor.set(cursor); if (Mode.current() |> Mode.isInsert) { let position: BytePosition.t = Cursor.get(); diff --git a/src/reason-libvim/bindings.c b/src/reason-libvim/bindings.c index 8079d49bb2..6ee6bb9158 100644 --- a/src/reason-libvim/bindings.c +++ b/src/reason-libvim/bindings.c @@ -824,6 +824,19 @@ colnr_T srcColumn, colnr_T wantColumn, linenr_T *destLine, colnr_T *destColumn) CAMLreturn0; } +void onCursorAdd(pos_T newCursorPosition) { + CAMLparam0(); + static const value *lv_onCursorAdd = NULL; + if (lv_onCursorAdd == NULL) { + lv_onCursorAdd = caml_named_value("lv_onCursorAdd"); + } + caml_callback2(*lv_onCursorAdd, + Val_int(newCursorPosition.lnum), + Val_int(newCursorPosition.col) + ); + CAMLreturn0; +} + void onScrollCallback(scrollDirection_T dir, long count) { CAMLparam0(); @@ -921,6 +934,7 @@ CAMLprim value libvim_vimInit(value unit) { vimSetInputMapCallback(&onInputMap); vimSetInputUnmapCallback(&onInputUnmap); vimSetToggleCommentsCallback(&onToggleComments); + vimSetCursorAddCallback(&onCursorAdd); vimSetFunctionGetCharCallback(&onGetChar); vimSetOutputCallback(&onOutput); char *args[0]; diff --git a/test.esy.lock/index.json b/test.esy.lock/index.json index ee597d5acb..542090b148 100644 --- a/test.esy.lock/index.json +++ b/test.esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "5ad923d3038bcd4718b1872d244be5e0", + "checksum": "9df68a1a0e664fe73be6c4ac681465e0", "root": "Oni2@link-dev:./package.json", "node": { "yarn-pkg-config@github:esy-ocaml/yarn-pkg-config#db3a0b63883606dd57c54a7158d560d6cba8cd79@d41d8cd9": { @@ -429,14 +429,14 @@ "dependencies": [], "devDependencies": [] }, - "libvim@8.10869.83@d41d8cd9": { - "id": "libvim@8.10869.83@d41d8cd9", + "libvim@8.10869.85@d41d8cd9": { + "id": "libvim@8.10869.85@d41d8cd9", "name": "libvim", - "version": "8.10869.83", + "version": "8.10869.85", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.83.tgz#sha1:dc8f49e86d26d085ece7fbe2418e6a5f0ff63a33" + "archive:https://registry.npmjs.org/libvim/-/libvim-8.10869.85.tgz#sha1:2666c88555e94e56c8415789e8947670e6e299a8" ] }, "overrides": [], @@ -1001,7 +1001,7 @@ "refmterr@3.3.2@d41d8cd9", "reason-native-crash-utils@github:onivim/reason-native-crash-utils#38c8f00@d41d8cd9", "reason-fzy@github:bryphe/reason-fzy#ab172e1@d41d8cd9", - "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.83@d41d8cd9", + "ocaml@4.10.0@d41d8cd9", "libvim@8.10869.85@d41d8cd9", "isolinear@github:revery-ui/isolinear#53fc4eb@d41d8cd9", "esy-tree-sitter@1.4.1@d41d8cd9", "esy-skia@github:revery-ui/esy-skia#91c98f6@d41d8cd9", diff --git a/test/reason-libvim/ModeTest.re b/test/reason-libvim/ModeTest.re index 881e420e3e..94c89c3e9a 100644 --- a/test/reason-libvim/ModeTest.re +++ b/test/reason-libvim/ModeTest.re @@ -110,6 +110,24 @@ describe("Mode", ({describe, _}) => { expect.equal(Vim.Mode.isReplace(Vim.Mode.current()), true); }) }); + describe("visual mode", ({test, _}) => { + test("linewise visual -> insert ('I')", ({expect, _}) => { + let _ = resetBuffer(); + + let _ = Vim.input("V"); + let _ = Vim.input("I"); + + expect.equal(Vim.Mode.isInsert(Vim.Mode.current()), true); + }); + test("characterwise visual -> insert ('A')", ({expect, _}) => { + let _ = resetBuffer(); + + let _ = Vim.key(""); + let _ = Vim.input("A"); + + expect.equal(Vim.Mode.isInsert(Vim.Mode.current()), true); + }); + }); describe("select mode", ({test, _}) => { test("select mode is reported correctly", ({expect, _}) => { let _ = resetBuffer(); diff --git a/test/reason-libvim/MultiCursorTest.re b/test/reason-libvim/MultiCursorTest.re index 2b22fb36af..1428b4fa42 100644 --- a/test/reason-libvim/MultiCursorTest.re +++ b/test/reason-libvim/MultiCursorTest.re @@ -33,6 +33,73 @@ let key = }; describe("Multi-cursor", ({describe, _}) => { + describe("visual block mode", ({test, _}) => { + let hasCursorMatching = (~lineIndex, ~byteIndex, cursors) => { + EditorCoreTypes.( + cursors + |> List.exists((cursor: BytePosition.t) => + LineNumber.toZeroBased(cursor.line) == lineIndex + && ByteIndex.toInt(cursor.byte) == byteIndex + ) + ); + }; + test("expand to multiple cursors with 'I'", ({expect, _}) => { + let _: Buffer.t = resetBuffer(); + let (context, _) = Vim.key(""); + let (context, _) = Vim.input(~context, "j"); + let (context, _) = Vim.input(~context, "j"); + + expect.equal(Vim.Mode.isVisual(context.mode), true); + + let (context, _) = Vim.input(~context, "I"); + + expect.equal(Vim.Mode.isInsert(context.mode), true); + + let cursors = Vim.Mode.cursors(context.mode); + expect.equal(List.length(cursors), 3); + + expect.equal( + cursors |> hasCursorMatching(~lineIndex=0, ~byteIndex=0), + true, + ); + expect.equal( + cursors |> hasCursorMatching(~lineIndex=1, ~byteIndex=0), + true, + ); + expect.equal( + cursors |> hasCursorMatching(~lineIndex=2, ~byteIndex=0), + true, + ); + }); + test("expand to multiple cursors with 'A'", ({expect, _}) => { + let _: Buffer.t = resetBuffer(); + let (context, _) = Vim.key(""); + let (context, _) = Vim.input(~context, "j"); + let (context, _) = Vim.input(~context, "j"); + + expect.equal(Vim.Mode.isVisual(context.mode), true); + + let (context, _) = Vim.input(~context, "A"); + + expect.equal(Vim.Mode.isInsert(context.mode), true); + + let cursors = Vim.Mode.cursors(context.mode); + expect.equal(List.length(cursors), 3); + + expect.equal( + cursors |> hasCursorMatching(~lineIndex=0, ~byteIndex=1), + true, + ); + expect.equal( + cursors |> hasCursorMatching(~lineIndex=1, ~byteIndex=1), + true, + ); + expect.equal( + cursors |> hasCursorMatching(~lineIndex=2, ~byteIndex=1), + true, + ); + }); + }); describe("normal mode", ({describe, _}) => { describe("single cursor", ({test, _}) => { test("set cursor works as expected", ({expect, _}) => { @@ -87,7 +154,61 @@ describe("Multi-cursor", ({describe, _}) => { }) }) }); - describe("insert mode", ({test, _}) => { + describe("insert mode", ({describe, test, _}) => { + describe("undo", ({test, _}) => { + test("undo multiple lines", ({expect, _}) => { + let buf = resetBuffer(); + let mode = + input( + ~mode= + Vim.Mode.Insert({ + cursors: [ + BytePosition.{line: LineNumber.zero, byte: ByteIndex.zero}, + BytePosition.{ + line: LineNumber.(zero + 1), + byte: ByteIndex.zero, + }, + BytePosition.{ + line: LineNumber.(zero + 2), + byte: ByteIndex.zero, + }, + ], + }), + "a", + ); + + let line1 = Buffer.getLine(buf, LineNumber.zero); + let line2 = Buffer.getLine(buf, LineNumber.(zero + 1)); + let line3 = Buffer.getLine(buf, LineNumber.(zero + 2)); + + expect.string(line1).toEqual( + "aThis is the first line of a test file", + ); + expect.string(line2).toEqual( + "aThis is the second line of a test file", + ); + expect.string(line3).toEqual( + "aThis is the third line of a test file", + ); + + let mode' = key(~mode, ""); + let _mode'' = key(~mode=mode', "u"); + + let line1 = Buffer.getLine(buf, LineNumber.zero); + let line2 = Buffer.getLine(buf, LineNumber.(zero + 1)); + let line3 = Buffer.getLine(buf, LineNumber.(zero + 2)); + + expect.string(line1).toEqual( + "This is the first line of a test file", + ); + expect.string(line2).toEqual( + "This is the second line of a test file", + ); + expect.string(line3).toEqual( + "This is the third line of a test file", + ); + }) + }); test("multi-cursor auto-closing pairs", ({expect, _}) => { let buf = resetBuffer();