Skip to content

Commit

Permalink
Feature: Command line completion (onivim#357)
Browse files Browse the repository at this point in the history
* Print command line matches

* More tweaks

* Fix up

* Show completions in wildmenu

* Hook up menu bindings to work with command mode too

* Progress on auto-completion

* Hook up auto-completion

* Handle previous event

* Add tab/shift tab to default key bindings

* Clean up logging

* Formatting

* Remove unused open
  • Loading branch information
bryphe authored Jul 9, 2019
1 parent 46f4bf6 commit e9a6198
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 27 deletions.
8 changes: 7 additions & 1 deletion assets/configuration/keybindings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
{ "key": "<D-S-P>", "command": "commandPalette.open", "when": [["editorTextFocus"]] },
{ "key": "<ESC>", "command": "menu.close", "when": [["menuFocus"]] },
{ "key": "<C-N>", "command": "menu.next", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<C-P>", "command": "menu.previous", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<C-P>", "command": "menu.previous", "when": [["menuFocus"], ["textInputFocus"]]},
{ "key": "<D-N>", "command": "menu.next", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<D-P>", "command": "menu.previous", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<C-N>", "command": "wildmenu.next", "when": [["commandLineFocus"]] },
{ "key": "<C-P>", "command": "wildmenu.previous", "when": [["commandLineFocus"]] },
{ "key": "<D-N>", "command": "wildmenu.next", "when": [["commandLineFocus"]] },
{ "key": "<D-P>", "command": "wildmenu.previous", "when": [["commandLineFocus"]] },
{ "key": "<TAB>", "command": "wildmenu.next", "when": [["commandLineFocus"]] },
{ "key": "<S-TAB>", "command": "wildmenu.previous", "when": [["commandLineFocus"]] },
{ "key": "<CR>", "command": "menu.select", "when": [["menuFocus"], ["textInputFocus"]] },
{ "key": "<S-C-B>", "command": "explorer.toggle", "when": [["editorTextFocus"]]}
]
Expand Down
9 changes: 1 addition & 8 deletions src/editor/Core/Types.re
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,6 @@ module UiFont = {
let create = (~fontFile, ~fontSize, ()) => {fontFile, fontSize};
};

[@deriving show({with_path: false})]
type wildmenu = {
items: list(string),
show: bool,
selected: int,
};

/* [@deriving show({with_path: false})] */
type commandline = {
text: string,
Expand All @@ -194,7 +187,7 @@ module Input = {
| [@name "menuFocus"] MenuFocus
| [@name "textInputFocus"] TextInputFocus
| [@name "editorTextFocus"] EditorTextFocus
| [@name "neovimMenuFocus"] NeovimMenuFocus;
| [@name "commandLineFocus"] CommandLineFocus;

[@deriving show({with_path: false})]
type keyBindings = {
Expand Down
41 changes: 41 additions & 0 deletions src/editor/Core/Utility.re
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,44 @@ let filterMap = (f, l) => {

inner(l);
};

type commandLineCompletionMeet = {
prefix: string,
position: int,
};

let getCommandLineCompletionsMeet = (str: string, position: int) => {
let len = String.length(str);

if (len == 0 || position < len) {
None;
} else {
/* Look backwards for '/' or ' ' */
let found = ref(false);
let meet = ref(position);

while (meet^ > 0 && ! found^) {
let pos = meet^ - 1;
let c = str.[pos];
if (c == ' ') {
found := true;
} else {
decr(meet);
};
};

let pos = meet^;
Some({prefix: String.sub(str, pos, len - pos), position: pos});
};
};

let trimTrailingSlash = (item: string) => {
let len = String.length(item);
let lastC = item.[len - 1];
/* Remove trailing slashes */
if (lastC == '\\' || lastC == '/') {
String.sub(item, 0, len - 1);
} else {
item;
};
};
8 changes: 5 additions & 3 deletions src/editor/Model/Actions.re
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ type t =
| CommandlineHide
| CommandlineUpdate(Vim.Types.cmdline)
| KeyboardInput(string)
| WildmenuShow(wildmenu)
| WildmenuHide(wildmenu)
| WildmenuSelected(int)
| WildmenuShow(list(string))
| WildmenuNext
| WildmenuPrevious
| WildmenuSelect
| WildmenuHide
| EditorGroupAdd(editorGroup)
| EditorGroupSetSize(int, EditorSize.t)
| EditorGroupSetActive(int)
Expand Down
2 changes: 1 addition & 1 deletion src/editor/Model/Reducer.re
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ let reduce: (State.t, Actions.t) => State.t =
ret;
| SetEditorFont(font) => {...s, editorFont: font}
| SetInputControlMode(m) => {...s, inputControlMode: m}
| CommandlineShow(_) => {...s, inputControlMode: NeovimMenuFocus}
| CommandlineShow(_) => {...s, inputControlMode: CommandLineFocus}
| CommandlineHide => {...s, inputControlMode: EditorTextFocus}
| RegisterDockItem(dock) =>
switch (dock) {
Expand Down
34 changes: 27 additions & 7 deletions src/editor/Model/Wildmenu.re
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,38 @@
* Wildmenu.re
*
*/
open Oni_Core.Types;
open Actions;

[@deriving show]
type t = wildmenu;
[@deriving show({with_path: false})]
type t = {
items: list(string),
show: bool,
selected: int,
count: int,
};

let create = () => {items: [], selected: 0, show: false};
let empty = [];

let create = () => {items: empty, selected: 0, show: false, count: 0};

let getSelectedItem = (v: t) =>
if (v.count <= 0 || v.selected < 0 || v.selected > v.count - 1) {
None;
} else {
Some(List.nth(v.items, v.selected));
};

let reduce = (s, action) =>
switch (action) {
| WildmenuShow(wildmenu) => wildmenu
| WildmenuHide(wildmenu) => wildmenu
| WildmenuSelected(selected) => {...s, selected}
| WildmenuShow(items) => {
show: true,
selected: (-1),
items,
count: List.length(items),
}
| CommandlineHide
| WildmenuHide => {show: false, selected: 0, items: empty, count: 0}
| WildmenuNext => {...s, selected: (s.selected + 1) mod s.count}
| WildmenuPrevious => {...s, selected: (s.selected - 1) mod s.count}
| _ => s
};
2 changes: 2 additions & 0 deletions src/editor/Store/CommandStoreConnector.re
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ let start = _ => {
("view.closeEditor", state => closeEditorEffect(state)),
("view.splitVertical", state => splitEditorEffect(state, Vertical)),
("view.splitHorizontal", state => splitEditorEffect(state, Horizontal)),
("wildmenu.next", _ => singleActionEffect(WildmenuNext)),
("wildmenu.previous", _ => singleActionEffect(WildmenuPrevious)),
("explorer.toggle", state => toggleExplorerEffect(state)),
];

Expand Down
70 changes: 66 additions & 4 deletions src/editor/Store/VimStoreConnector.re
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* vimStoreConnector.re
* VimStoreConnector.re
*
* This module connects vim to the Store:
* - Translates incoming vim notifications into Actions
Expand Down Expand Up @@ -108,12 +108,38 @@ let start = () => {
dispatch(Model.Actions.CommandlineShow(c.cmdType))
);

let lastCompletionMeet = ref(None);
let isCompleting = ref(false);

let checkCommandLineCompletions = () => {
Log.info("VimStoreConnector::checkCommandLineCompletions");
let completions = Vim.CommandLine.getCompletions() |> Array.to_list;
Log.info(
"VimStoreConnector::checkCommandLineCompletions - got "
++ string_of_int(List.length(completions))
++ " completions.",
);
dispatch(Model.Actions.WildmenuShow(completions));
};

let _ =
Vim.CommandLine.onUpdate(c => {
dispatch(Model.Actions.CommandlineUpdate(c));

let cmdlineType = Vim.CommandLine.getType();
switch (cmdlineType) {
| Ex =>
();
let text =
switch (Vim.CommandLine.getText()) {
| Some(v) => v
| None => ""
};
let position = Vim.CommandLine.getPosition();
let meet = Core.Utility.getCommandLineCompletionsMeet(text, position);
lastCompletionMeet := meet;

isCompleting^ ? () : checkCommandLineCompletions();
| SearchForward
| SearchReverse =>
let highlights = Vim.Search.getHighlights();
Expand All @@ -138,15 +164,17 @@ let start = () => {
|> Array.to_list
|> List.filter(sameLineFilter)
|> List.map(toOniRange);

dispatch(SearchSetHighlights(id, highlightList));

| _ => ()
};
});

let _ =
Vim.CommandLine.onLeave(() => dispatch(Model.Actions.CommandlineHide));
Vim.CommandLine.onLeave(() => {
lastCompletionMeet := None;
isCompleting := false;
dispatch(Model.Actions.CommandlineHide);
});

let _ =
Vim.Window.onTopLineChanged(t =>
Expand Down Expand Up @@ -190,6 +218,26 @@ let start = () => {
Vim.Buffer.openFile(filePath) |> ignore
);

let applyCompletionEffect = completion =>
Isolinear.Effect.create(~name="vim.applyCommandlineCompletion", () =>
Core.Utility.(
switch (lastCompletionMeet^) {
| None => ()
| Some({position, _}) =>
isCompleting := true;
let currentPos = ref(Vim.CommandLine.getPosition());
while (currentPos^ > position) {
Vim.input("<bs>");
currentPos := Vim.CommandLine.getPosition();
};

let completion = Core.Utility.trimTrailingSlash(completion);
String.iter(c => Vim.input(String.make(1, c)), completion);
isCompleting := false;
}
)
);

let synchronizeIndentationEffect = (indentation: Core.IndentationSettings.t) =>
Isolinear.Effect.create(~name="vim.setIndentation", () => {
let insertSpaces =
Expand Down Expand Up @@ -291,6 +339,20 @@ let start = () => {

let updater = (state: Model.State.t, action) => {
switch (action) {
| Model.Actions.WildmenuNext =>
let eff =
switch (Model.Wildmenu.getSelectedItem(state.wildmenu)) {
| None => Isolinear.Effect.none
| Some(v) => applyCompletionEffect(v)
};
(state, eff);
| Model.Actions.WildmenuPrevious =>
let eff =
switch (Model.Wildmenu.getSelectedItem(state.wildmenu)) {
| None => Isolinear.Effect.none
| Some(v) => applyCompletionEffect(v)
};
(state, eff);
| Model.Actions.Init => (state, initEffect)
| Model.Actions.OpenFileByPath(path) => (
state,
Expand Down
10 changes: 7 additions & 3 deletions src/editor/bin_editor/Input.re
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ let getActionsForBinding =
*/
let handle = (~state: State.t, ~commands: Keybindings.t, inputKey) => {
switch (state.inputControlMode) {
| NeovimMenuFocus
| CommandLineFocus
| EditorTextFocus =>
switch (getActionsForBinding(inputKey, commands, state)) {
| [] => [Actions.KeyboardInput(inputKey)]
| actions => actions
| [] =>
Log.info("Input::handle - sending raw input: " ++ inputKey);
[Actions.KeyboardInput(inputKey)];
| actions =>
Log.info("Input::handle - sending bound actions.");
actions;
}
| TextInputFocus
| MenuFocus => getActionsForBinding(inputKey, commands, state)
Expand Down

0 comments on commit e9a6198

Please sign in to comment.