Skip to content

Commit

Permalink
Fix #506 - Part 1 - Insert-mode paste (#572)
Browse files Browse the repository at this point in the history
* Add paste command + effect handler

* Wire up clipboard API

* Formatting

* Add API for setting clipboard in tests

* Add Clipboard tests

* Fix multi-line test

* formatting

* Add an 'insertMode' to the when clause for keybindings

* Add when condition for insertMode

* Fix formatting
  • Loading branch information
bryphe committed Aug 5, 2019
1 parent 8eb5efc commit 71e6e3b
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 16 deletions.
35 changes: 35 additions & 0 deletions integration_test/ClipboardPasteEmptyTest.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
open Oni_Model;
open Oni_IntegrationTestLib;

runTest(
~name="InsertMode test - effects batched to runEffects",
(dispatch, wait, runEffects) => {
wait(~name="Initial mode is normal", (state: State.t) =>
state.mode == Vim.Types.Normal
);

dispatch(KeyboardInput("i"));

wait(~name="Mode switches to insert", (state: State.t) =>
state.mode == Vim.Types.Insert
);

setClipboard(None);

/* Simulate multiple events getting dispatched before running effects */
dispatch(KeyboardInput("A"));
dispatch(Command("editor.action.clipboardPasteAction"));
dispatch(KeyboardInput("B"));

runEffects();

wait(~name="Buffer shows AB", (state: State.t) =>
switch (Selectors.getActiveBuffer(state)) {
| None => false
| Some(buf) =>
let line = Buffer.getLine(buf, 0);
Log.info("Current line is: |" ++ line ++ "|");
String.equal(line, "AB");
}
);
});
36 changes: 36 additions & 0 deletions integration_test/ClipboardPasteMultiLineTest.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
open Oni_Model;
open Oni_IntegrationTestLib;

runTest(
~name="InsertMode test - effects batched to runEffects",
(dispatch, wait, runEffects) => {
wait(~name="Initial mode is normal", (state: State.t) =>
state.mode == Vim.Types.Normal
);

dispatch(KeyboardInput("i"));

wait(~name="Mode switches to insert", (state: State.t) =>
state.mode == Vim.Types.Insert
);

setClipboard(Some("def\nghi"));

dispatch(KeyboardInput("A"));
dispatch(Command("editor.action.clipboardPasteAction"));
dispatch(KeyboardInput("B"));

runEffects();

wait(~name="Buffer is correct", (state: State.t) =>
switch (Selectors.getActiveBuffer(state)) {
| None => false
| Some(buf) =>
let line1 = Buffer.getLine(buf, 0);
let line2 = Buffer.getLine(buf, 1);
Log.info("Line1 is: " ++ line1 ++ "|");
Log.info("Line2 is: " ++ line2 ++ "|");
String.equal(line1, "Adef") && String.equal(line2, "ghiB");
}
);
});
35 changes: 35 additions & 0 deletions integration_test/ClipboardPasteSingleLineTest.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
open Oni_Model;
open Oni_IntegrationTestLib;

runTest(
~name="InsertMode test - effects batched to runEffects",
(dispatch, wait, runEffects) => {
wait(~name="Initial mode is normal", (state: State.t) =>
state.mode == Vim.Types.Normal
);

dispatch(KeyboardInput("i"));

wait(~name="Mode switches to insert", (state: State.t) =>
state.mode == Vim.Types.Insert
);

setClipboard(Some("def"));

/* Simulate multiple events getting dispatched before running effects */
dispatch(KeyboardInput("A"));
dispatch(Command("editor.action.clipboardPasteAction"));
dispatch(KeyboardInput("B"));

runEffects();

wait(~name="Buffer shows AdefB", (state: State.t) =>
switch (Selectors.getActiveBuffer(state)) {
| None => false
| Some(buf) =>
let line = Buffer.getLine(buf, 0);
Log.info("Current line is: |" ++ line ++ "|");
String.equal(line, "AdefB");
}
);
});
46 changes: 46 additions & 0 deletions integration_test/RegressionNonExistentDirectory.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
open Oni_Model;
open Oni_IntegrationTestLib;

runTest(~name="RegressionVspEmpty", (_, wait, _) => {
wait(~name="Wait for split to be created 1", (state: State.t) => {
let splitCount =
state.windowManager.windowTree |> WindowTree.getSplits |> List.length;
splitCount == 1;
});

Vim.command("e test.txt");

/* :vsp with no arguments should create a second split w/ same buffer */
Vim.command("vsp");

wait(~name="Wait for split to be created", (state: State.t) => {
let splitCount =
state.windowManager.windowTree |> WindowTree.getSplits |> List.length;

splitCount == 2;
});

/* Validate the editors all have same buffer id */
wait(~name="Wait for split to be created", (state: State.t) => {
let splits = WindowTree.getSplits(state.windowManager.windowTree);

let firstSplit = List.nth(splits, 0);
let secondSplit = List.nth(splits, 1);

let firstActiveEditor =
Selectors.getEditorGroupById(state, firstSplit.editorGroupId)
|> Selectors.getActiveEditor;

let secondActiveEditor =
Selectors.getEditorGroupById(state, secondSplit.editorGroupId)
|> Selectors.getActiveEditor;

switch (firstActiveEditor, secondActiveEditor) {
| (Some(e1), Some(e2)) =>
print_endline("e1 buffer id: " ++ string_of_int(e1.bufferId));
print_endline("e2 buffer id: " ++ string_of_int(e2.bufferId));
e1.bufferId == e2.bufferId;
| _ => false
};
});
});
6 changes: 6 additions & 0 deletions integration_test/dune
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
(executables
(names InsertModeTest
ClipboardPasteMultiLineTest
ClipboardPasteSingleLineTest
ClipboardPasteEmptyTest
RegressionCommandLineNoCompletionTest
RegressionVspEmpty
AddRemoveSplitTest
Expand All @@ -17,6 +20,9 @@
(files
run-tests.sh
InsertModeTest.exe
ClipboardPasteMultiLineTest.exe
ClipboardPasteSingleLineTest.exe
ClipboardPasteEmptyTest.exe
RegressionCommandLineNoCompletionTest.exe
RegressionVspEmpty.exe
AddRemoveSplitTest.exe
Expand Down
5 changes: 5 additions & 0 deletions integration_test/lib/Oni_IntegrationTestLib.re
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ type waitForState = (~name: string, waiter) => unit;
type testCallback =
(dispatchFunction, waitForState, runEffectsFunction) => unit;

let _currentClipboard: ref(option(string)) = ref(None);

let setClipboard = v => _currentClipboard := v;

let runTest = (~name="AnonymousTest", test: testCallback) => {
Printexc.record_backtrace(true);
Log.enablePrinting();
Expand All @@ -32,6 +36,7 @@ let runTest = (~name="AnonymousTest", test: testCallback) => {
let (dispatch, runEffects) =
Store.StoreThread.start(
~setup,
~getClipboardText=() => _currentClipboard^,
~executingDirectory=Revery.Environment.getExecutingDirectory(),
~onStateChanged,
(),
Expand Down
2 changes: 2 additions & 0 deletions src/editor/Core/ConfigurationDefaults.re
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ let getDefaultConfigString = configName =>
{
"bindings": [
{ "key": "<C-P>", "command": "quickOpen.open", "when": [["editorTextFocus"]] },
{ "key": "<C-V>", "command": "editor.action.clipboardPasteAction", "when": [["insertMode"]] },
{ "key": "<D-V>", "command": "editor.action.clipboardPasteAction", "when": [["insertMode"]] },
{ "key": "<D-P>", "command": "quickOpen.open", "when": [["editorTextFocus"]] },
{ "key": "<S-C-P>", "command": "commandPalette.open", "when": [["editorTextFocus"]] },
{ "key": "<D-S-P>", "command": "commandPalette.open", "when": [["editorTextFocus"]] },
Expand Down
8 changes: 7 additions & 1 deletion src/editor/Core/Types.re
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,17 @@ module Input = {
[@deriving
(show({with_path: false}), yojson({strict: false, exn: false}))
]
// This type is overloaded - describing both the current 'input mode'
// the UI is in, as well as the state of 'when' conditions in the input
// bindings. Need to decouple these.
type controlMode =
// VSCode-compatible when parameters
| [@name "menuFocus"] MenuFocus
| [@name "textInputFocus"] TextInputFocus
| [@name "editorTextFocus"] EditorTextFocus
| [@name "commandLineFocus"] CommandLineFocus;
| [@name "commandLineFocus"] CommandLineFocus
// Onivim extensions to the 'when' syntax
| [@name "insertMode"] InsertMode;

[@deriving show({with_path: false})]
type keyBindings = {
Expand Down
12 changes: 10 additions & 2 deletions src/editor/Store/StoreThread.re
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ let discoverExtensions = (setup: Core.Setup.t) => {
extensions;
};

let start = (~setup: Core.Setup.t, ~executingDirectory, ~onStateChanged, ()) => {
let start =
(
~setup: Core.Setup.t,
~executingDirectory,
~onStateChanged,
~getClipboardText,
(),
) => {
ignore(executingDirectory);

let state = Model.State.create();
Expand All @@ -49,7 +56,8 @@ let start = (~setup: Core.Setup.t, ~executingDirectory, ~onStateChanged, ()) =>
let languageInfo = Model.LanguageInfo.ofExtensions(extensions);

let commandUpdater = CommandStoreConnector.start(getState);
let (vimUpdater, vimStream) = VimStoreConnector.start(getState);
let (vimUpdater, vimStream) =
VimStoreConnector.start(getState, getClipboardText);

let (textmateUpdater, textmateStream) =
TextmateClientStoreConnector.start(languageInfo, setup);
Expand Down
21 changes: 20 additions & 1 deletion src/editor/Store/VimStoreConnector.re
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ module Extensions = Oni_Extensions;
module Model = Oni_Model;

module Log = Core.Log;
module Zed_utf8 = Core.ZedBundled;

let start = (getState: unit => Model.State.t) => {
let start = (getState: unit => Model.State.t, getClipboardText) => {
let (stream, dispatch) = Isolinear.Stream.create();

let _ =
Expand Down Expand Up @@ -471,8 +472,26 @@ let start = (getState: unit => Model.State.t) => {
}
);

let pasteIntoEditorAction =
Isolinear.Effect.create(~name="vim.clipboardPaste", () =>
if (Vim.Mode.getCurrent() == Vim.Types.Insert) {
switch (getClipboardText()) {
| Some(text) =>
Vim.command("set paste");
Zed_utf8.iter(s => Vim.input(Zed_utf8.singleton(s)), text);

Vim.command("set nopaste");
| None => ()
};
}
);

let updater = (state: Model.State.t, action) => {
switch (action) {
| Model.Actions.Command("editor.action.clipboardPasteAction") => (
state,
pasteIntoEditorAction,
)
| Model.Actions.WildmenuNext =>
let eff =
switch (Model.Wildmenu.getSelectedItem(state.wildmenu)) {
Expand Down
61 changes: 49 additions & 12 deletions src/editor/bin_editor/Input.re
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,65 @@ let keyPressToCommand =
};
};

module Conditions = {
type t = Hashtbl.t(Types.Input.controlMode, bool);

let getBooleanCondition = (v: t, condition: Types.Input.controlMode) => {
switch (Hashtbl.find_opt(v, condition)) {
| Some(v) => v
| None => false
};
};

let ofState = (state: State.t) => {
// Not functional, but we'll use the hashtable for performance
let ret: t = Hashtbl.create(16);

Hashtbl.add(ret, state.inputControlMode, true);

// HACK: Because we don't have AND conditions yet for input
// (the conditions array are OR's), we are making `insertMode`
// only true when the editor is insert mode AND we are in the
// editor (editorTextFocus is set)
switch (state.inputControlMode, state.mode) {
| (Types.Input.EditorTextFocus, Vim.Types.Insert) =>
Hashtbl.add(ret, Types.Input.InsertMode, true)
| _ => ()
};

ret;
};
};

/**
Search if any of the matching "when" conditions in the Keybindings.json
match the current condition in state
*/
let matchesCondition = (conditions, currentMode, input, key) =>
List.fold_left(
(prevMatch, condition) => prevMatch || condition == currentMode,
false,
conditions,
)
|> (&&)(input == key);

let getActionsForBinding =
(inputKey, commands, {inputControlMode, _}: State.t) =>
let matchesCondition = (commandConditions, currentConditions, input, key) =>
if (input != key) {
false;
} else {
List.fold_left(
(prevMatch, condition) =>
prevMatch
|| Conditions.getBooleanCondition(currentConditions, condition),
false,
commandConditions,
);
};

let getActionsForBinding = (inputKey, commands, state: State.t) => {
let currentConditions = Conditions.ofState(state);
Keybindings.(
List.fold_left(
(defaultAction, {key, command, condition}) =>
matchesCondition(condition, inputControlMode, inputKey, key)
matchesCondition(condition, currentConditions, inputKey, key)
? [Actions.Command(command)] : defaultAction,
[],
commands,
)
);
};

/**
Handle Input from Oni or Neovim
Expand All @@ -130,6 +166,7 @@ let handle = (~state: State.t, ~commands: Keybindings.t, inputKey) => {
actions;
}
| TextInputFocus
| MenuFocus => getActionsForBinding(inputKey, commands, state)
| MenuFocus
| _ => getActionsForBinding(inputKey, commands, state)
};
};
2 changes: 2 additions & 0 deletions src/editor/bin_editor/Oni2_editor.re
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ let init = app => {
let (dispatch, runEffects) =
Store.StoreThread.start(
~setup,
~getClipboardText=
() => Reglfw.Glfw.glfwGetClipboardString(w.glfwWindow),
~executingDirectory=Core.Utility.executingDirectory,
~onStateChanged,
(),
Expand Down

0 comments on commit 71e6e3b

Please sign in to comment.