diff --git a/assets/configuration/keybindings.json b/assets/configuration/keybindings.json new file mode 100644 index 0000000000..0e35346529 --- /dev/null +++ b/assets/configuration/keybindings.json @@ -0,0 +1,9 @@ +{ + "bindings": [ + { "key": "", "command": "commandPalette.open", "when": ["editorTextFocus"] }, + { "key": "", "command": "commandPalette.close", "when": ["commandPaletteFocus"] }, + { "key": "", "command": "commandPalette.next", "when": ["commandPaletteFocus"] }, + { "key": "", "command": "commandPalette.previous", "when": ["commandPaletteFocus"] }, + { "key": "", "command": "commandPalette.select", "when": ["commandPaletteFocus"] } + ] +} diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index de8bf44d22..dc74603a92 100644 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -37,13 +37,15 @@ case "${machine}" in TEXTMATE_SERVICE_PATH="$(pwd)/src/textmate_service/lib/src/index.js" EXTENSIONS_PATH="$(pwd)/extensions" NEOVIM_PATH="$(pwd)/vendor/neovim-0.3.3/nvim-linux64/bin/nvim" - CONFIGURATION_PATH="$(pwd)/assets/configuration/configuration.json";; + CONFIGURATION_PATH="$(pwd)/assets/configuration/configuration.json" + KEYBINDINGS_PATH="$(pwd)/assets/configuration/keybindings.json";; Mac) NODE_PATH="$(pwd)/vendor/node-v10.15.1/osx/node" TEXTMATE_SERVICE_PATH="$(pwd)/src/textmate_service/lib/src/index.js" EXTENSIONS_PATH="$(pwd)/extensions" NEOVIM_PATH="$(pwd)/vendor/neovim-0.3.3/nvim-osx64/bin/nvim" - CONFIGURATION_PATH="$(pwd)/assets/configuration/configuration.json";; + CONFIGURATION_PATH="$(pwd)/assets/configuration/configuration.json" + KEYBINDINGS_PATH="$(pwd)/assets/configuration/keybindings.json";; *) NEOVIM_PATH="$(pwd)/vendor/neovim-0.3.3/nvim-win64/bin/nvim.exe" NEOVIM_PATH="$(cygpath -m "$NEOVIM_PATH")" @@ -54,10 +56,12 @@ case "${machine}" in NODE_PATH="$(pwd)/vendor/node-v10.15.1/win-x64/node.exe" NODE_PATH="$(cygpath -m "$NODE_PATH")" CONFIGURATION_PATH="$(pwd)/assets/configuration/configuration.json" - CONFIGURATION_PATH="$(cygpath -m "$CONFIGURATION_PATH")";; + CONFIGURATION_PATH="$(cygpath -m "$CONFIGURATION_PATH")" + KEYBINDINGS_PATH="$(pwd)/assets/configuration/keybindings.json" + KEYBINDINGS_PATH="$(cygpath -m "$KEYBINDINGS_PATH")";; esac -oni_bin_path="{neovim:\"$NEOVIM_PATH\",node:\"$NODE_PATH\",configuration:\"$CONFIGURATION_PATH\",textmateService:\"$TEXTMATE_SERVICE_PATH\",bundledExtensions:\"$EXTENSIONS_PATH\"}" +oni_bin_path="{neovim:\"$NEOVIM_PATH\",node:\"$NODE_PATH\",configuration:\"$CONFIGURATION_PATH\",textmateService:\"$TEXTMATE_SERVICE_PATH\",bundledExtensions:\"$EXTENSIONS_PATH\",keybindings:\"$KEYBINDINGS_PATH\"}" # create the current bin path as this might not exist yet if [ ! -d "$config_path" ]; then diff --git a/src/editor/Core/Actions.re b/src/editor/Core/Actions.re index 887780ceda..eb6b716a8f 100644 --- a/src/editor/Core/Actions.re +++ b/src/editor/Core/Actions.re @@ -34,4 +34,10 @@ type t = | EditorMoveCursorToBottom(Cursor.move) | SyntaxHighlightColorMap(ColorMap.t) | SyntaxHighlightTokens(TextmateClient.TokenizationResult.t) + | CommandPaletteStart(list(Palette.command)) + | CommandPaletteOpen + | CommandPaletteClose + | CommandPaletteSelect + | CommandPalettePosition(int) + | SetInputControlMode(Input.controlMode) | Noop; diff --git a/src/editor/Core/CommandPalette.re b/src/editor/Core/CommandPalette.re new file mode 100644 index 0000000000..f0b6953749 --- /dev/null +++ b/src/editor/Core/CommandPalette.re @@ -0,0 +1,78 @@ +open Types; + +type t = Palette.t; +open Palette; + +let join = paths => { + let sep = Filename.dir_sep; + List.fold_left((accum, p) => accum ++ sep ++ p, "", paths); +}; + +let openConfigurationFile = (effects: Effects.t) => { + let path = + join([ + Revery.Environment.getWorkingDirectory(), + "assets", + "configuration", + "configuration.json", + ]); + effects.openFile(~path, ()); +}; + +let openKeybindingsFile = (effects: Effects.t) => { + let path = + join([ + Revery.Environment.getWorkingDirectory(), + "assets", + "configuration", + "keybindings.json", + ]); + effects.openFile(~path, ()); +}; + +let commandPaletteCommands = (effects: Effects.t) => [ + { + name: "Open configuration file", + command: () => openConfigurationFile(effects), + }, + { + name: "Open keybindings file", + command: () => openKeybindingsFile(effects), + }, +]; + +let create = (~effects: option(Effects.t)=?, ()) => + switch (effects) { + | Some(e) => { + isOpen: false, + commands: commandPaletteCommands(e), + selectedItem: 0, + } + | None => {isOpen: false, commands: [], selectedItem: 0} + }; + +let make = (~effects: Effects.t) => + create(~effects, ()) |> (c => Actions.CommandPaletteStart(c.commands)); + +let position = (selectedItem, change, commands: list(command)) => + selectedItem + change >= List.length(commands) ? 0 : selectedItem + change; + +let reduce = (state: t, action: Actions.t) => + switch (action) { + | CommandPaletteStart(commands) => {...state, commands} + | CommandPalettePosition(pos) => { + ...state, + selectedItem: position(state.selectedItem, pos, state.commands), + } + | CommandPaletteOpen => {...state, isOpen: true} + | CommandPaletteClose => {...state, isOpen: false} + | CommandPaletteSelect => + /** + TODO: Refactor this to middleware so this action is handled like a redux side-effect + as this makes the reducer impure + */ + let selected = List.nth(state.commands, state.selectedItem); + selected.command(); + {...state, isOpen: false}; + | _ => state + }; diff --git a/src/editor/Core/Commands.re b/src/editor/Core/Commands.re new file mode 100644 index 0000000000..b6a29542ab --- /dev/null +++ b/src/editor/Core/Commands.re @@ -0,0 +1,43 @@ +type oniCommand = { + name: string, + command: unit => list(Actions.t), +}; + +type t = list(oniCommand); + +let oniCommands = [ + { + name: "commandPalette.open", + command: _ => [ + CommandPaletteOpen, + SetInputControlMode(CommandPaletteFocus), + ], + }, + { + name: "commandPalette.close", + command: _ => [ + CommandPaletteClose, + SetInputControlMode(EditorTextFocus), + ], + }, + {name: "commandPalette.next", command: _ => [CommandPalettePosition(1)]}, + { + name: "commandPalette.previous", + command: _ => [CommandPalettePosition(-1)], + }, + { + name: "commandPalette.select", + command: _ => [ + CommandPaletteSelect, + SetInputControlMode(EditorTextFocus), + ], + }, +]; + +let handleCommand = (~commands=oniCommands, name) => { + let matchingCmd = List.find_opt(cmd => name == cmd.name, commands); + switch (matchingCmd) { + | Some(c) => c.command() + | None => [Noop] + }; +}; diff --git a/src/editor/Core/Editor.re b/src/editor/Core/Editor.re index 45c65c58c7..c1b1c986de 100644 --- a/src/editor/Core/Editor.re +++ b/src/editor/Core/Editor.re @@ -1,6 +1,7 @@ open Actions; open Types; +[@deriving show] type t = { id: int, scrollX: int, diff --git a/src/editor/Core/Keybindings.re b/src/editor/Core/Keybindings.re new file mode 100644 index 0000000000..0dfc97d347 --- /dev/null +++ b/src/editor/Core/Keybindings.re @@ -0,0 +1,28 @@ +open Types.Input; + +[@deriving (show, yojson({strict: false, exn: false}))] +type keyBindings = { + key: string, + command: string, + [@key "when"] + condition: controlMode, +}; + +[@deriving (show, yojson({strict: false, exn: false}))] +type t = list(keyBindings); + +[@deriving (show, yojson({strict: false, exn: false}))] +type json_keybindings = {bindings: t}; + +let ofFile = filePath => + Yojson.Safe.from_file(filePath) |> json_keybindings_of_yojson; + +let get = () => { + let {keybindingsPath, _}: Setup.t = Setup.init(); + switch (ofFile(keybindingsPath)) { + | Ok(b) => b.bindings + | Error(e) => + print_endline("Error parsing keybindings file ------- " ++ e); + []; + }; +}; diff --git a/src/editor/Core/Oni_Core.re b/src/editor/Core/Oni_Core.re index 657e85226e..3024ff6787 100644 --- a/src/editor/Core/Oni_Core.re +++ b/src/editor/Core/Oni_Core.re @@ -28,3 +28,6 @@ module Types = Types; module Utility = Utility; module Wildmenu = Wildmenu; module Configuration = Configuration; +module CommandPalette = CommandPalette; +module Commands = Commands; +module Keybindings = Keybindings; diff --git a/src/editor/Core/Reducer.re b/src/editor/Core/Reducer.re index e8898c09f2..3a9163445f 100644 --- a/src/editor/Core/Reducer.re +++ b/src/editor/Core/Reducer.re @@ -70,6 +70,7 @@ let reduce: (State.t, Actions.t) => State.t = syntaxHighlighting: SyntaxHighlighting.reduce(s.syntaxHighlighting, a), wildmenu: Wildmenu.reduce(s.wildmenu, a), commandline: Commandline.reduce(s.commandline, a), + commandPalette: CommandPalette.reduce(s.commandPalette, a), }; switch (a) { @@ -103,6 +104,7 @@ let reduce: (State.t, Actions.t) => State.t = ...s, tabs: updateTabs(activeBufferId, modified, s.tabs), } + | SetInputControlMode(m) => {...s, inputControlMode: m} | _ => s }; }; diff --git a/src/editor/Core/Setup.re b/src/editor/Core/Setup.re index ba2f183a12..5f42da497d 100644 --- a/src/editor/Core/Setup.re +++ b/src/editor/Core/Setup.re @@ -16,6 +16,8 @@ type t = { bundledExtensionsPath: string, [@key "configuration"] configPath: string, + [@key "keybindings"] + keybindingsPath: string, }; let ofString = str => Yojson.Safe.from_string(str) |> of_yojson_exn; diff --git a/src/editor/Core/State.re b/src/editor/Core/State.re index fc54c06077..9367d15596 100644 --- a/src/editor/Core/State.re +++ b/src/editor/Core/State.re @@ -23,18 +23,21 @@ type t = { buffers: BufferMap.t, activeBufferId: int, editorFont: EditorFont.t, + commandPalette: CommandPalette.t, commandline: Commandline.t, wildmenu: Wildmenu.t, configuration: Configuration.t, syntaxHighlighting: SyntaxHighlighting.t, theme: Theme.t, editor: Editor.t, + inputControlMode: Input.controlMode, }; let create: unit => t = () => { configuration: Configuration.create(), mode: Insert, + commandPalette: CommandPalette.create(), commandline: Commandline.create(), wildmenu: Wildmenu.create(), activeBufferId: 0, @@ -51,4 +54,5 @@ let create: unit => t = tabs: [Tab.create(0, "[No Name]")], theme: Theme.create(), editor: Editor.create(), + inputControlMode: EditorTextFocus, }; diff --git a/src/editor/Core/Types.re b/src/editor/Core/Types.re index f998a0c3b3..ba55e09f67 100644 --- a/src/editor/Core/Types.re +++ b/src/editor/Core/Types.re @@ -18,6 +18,7 @@ module Index = { }; module EditorSize = { + [@deriving show] type t = { pixelWidth: int, pixelHeight: int, @@ -76,6 +77,7 @@ type openMethod = | Buffer; module BufferPosition = { + [@deriving show] type t = { line: Index.t, character: Index.t, @@ -223,3 +225,29 @@ type commandline = { prompt: string, show: bool, }; + +module Palette = { + [@deriving show] + type command = { + name: string, + command: unit => unit, + }; + + [@deriving show] + type t = { + isOpen: bool, + commands: list(command), + selectedItem: int, + }; +}; + +module Input = { + [@deriving (show, yojson({strict: false, exn: false}))] + type controlMode = + | [@name "commandPaletteFocus"] CommandPaletteFocus + | [@name "editorTextFocus"] EditorTextFocus; +}; + +module Effects = { + type t = {openFile: Views.viewOperation}; +}; diff --git a/src/editor/UI/CommandPaletteView.re b/src/editor/UI/CommandPaletteView.re new file mode 100644 index 0000000000..fbb3ae56fe --- /dev/null +++ b/src/editor/UI/CommandPaletteView.re @@ -0,0 +1,52 @@ +open Revery; +open Oni_Core; +open Revery.UI; +open Revery.UI.Components; + +let component = React.component("commandPalette"); + +let paletteWidth = 400; + +let containerStyles = (theme: Theme.t) => + Style.[ + backgroundColor(theme.colors.editorMenuBackground), + color(theme.colors.editorMenuForeground), + width(paletteWidth), + height(300), + boxShadow( + ~xOffset=-15., + ~yOffset=5., + ~blurRadius=30., + ~spreadRadius=5., + ~color=Color.rgba(0., 0., 0., 0.2), + ), + ]; + +let paletteItemStyle = Style.[fontSize(14)]; + +let createElement = + (~children as _, ~commandPalette: CommandPalette.t, ~theme: Theme.t, ()) => + component(hooks => + ( + hooks, + commandPalette.isOpen + ? + /* */ + + + ...{List.mapi( + (index, cmd: Types.Palette.command) => + , + commandPalette.commands, + )} + + + : React.listToElement([]), + ) + ); diff --git a/src/editor/UI/Root.re b/src/editor/UI/Root.re index ec9d513931..5ac1b63ee7 100644 --- a/src/editor/UI/Root.re +++ b/src/editor/UI/Root.re @@ -59,6 +59,7 @@ let createElement = (~state: State.t, ~children as _, ()) => + + Keybindings.( + List.fold_left( + (defaultAction, {key, command, condition}) => + if (inputKey == key && condition == state.inputControlMode) { + Commands.handleCommand(command); + } else { + defaultAction; + }, + [], + commands, + ) + ); + +/** + Handle Input from Oni or Neovim + + */ +let handle = + ( + ~api: Oni_Neovim.NeovimProtocol.t, + ~state: State.t, + ~commands: Keybindings.t, + inputKey, + ) => + switch (state.inputControlMode) { + | EditorTextFocus => + switch (getActionsForBinding(inputKey, commands, state)) { + | [] as default => + api.input(inputKey) |> ignore; + default; + | actions => actions + } + | _ => getActionsForBinding(inputKey, commands, state) + }; diff --git a/src/editor/bin/Oni2.re b/src/editor/bin/Oni2.re index 46a3c91877..ec646ef733 100644 --- a/src/editor/bin/Oni2.re +++ b/src/editor/bin/Oni2.re @@ -22,6 +22,7 @@ switch (Sys.getenv_opt("REVERY_DEBUG")) { | None => () }; +let state = Core.State.create(); /* The 'main' function for our app */ let init = app => { let w = @@ -38,7 +39,7 @@ let init = app => { let initVimPath = Revery.Environment.getExecutingDirectory() ++ "init.vim"; Core.Log.debug("initVimPath: " ++ initVimPath); - let setup: Oni_Core.Setup.t = Oni_Core.Setup.init(); + let setup = Oni_Core.Setup.init(); let nvim = NeovimProcess.start( @@ -64,9 +65,8 @@ let init = app => { let onColorMap = cm => App.dispatch(app, Core.Actions.SyntaxHighlightColorMap(cm)); - let onTokens = tr => { + let onTokens = tr => App.dispatch(app, Core.Actions.SyntaxHighlightTokens(tr)); - }; let tmClient = Oni_Core.TextmateClient.start( @@ -153,19 +153,20 @@ let init = app => { setFont("FiraCode-Regular.ttf", 14); - /* let _ = */ - /* Event.subscribe( */ - /* w.onKeyPress, */ - /* event => { */ - /* let c = event.character; */ - /* neovimProtocol.input(c); */ - /* }, */ - /* ); */ + let commands = Core.Keybindings.get(); + + Core.CommandPalette.make(~effects={openFile: neovimProtocol.openFile}) + |> App.dispatch(app) + |> ignore; + + let inputHandler = Input.handle(~api=neovimProtocol, ~commands); Reglfw.Glfw.glfwSetCharModsCallback(w.glfwWindow, (_w, codepoint, mods) => switch (Input.charToCommand(codepoint, mods)) { | None => () - | Some(v) => ignore(neovimProtocol.input(v)) + | Some(v) => + inputHandler(~state=App.getState(app), v) + |> List.iter(App.dispatch(app)) } ); @@ -173,7 +174,9 @@ let init = app => { w.glfwWindow, (_w, key, _scancode, buttonState, mods) => switch (Input.keyPressToCommand(key, buttonState, mods)) { | None => () - | Some(v) => ignore(neovimProtocol.input(v)) + | Some(v) => + inputHandler(~state=App.getState(app), v) + |> List.iter(App.dispatch(app)) } ); @@ -295,4 +298,4 @@ let init = app => { }; /* Let's get this party started! */ -App.startWithState(Core.State.create(), Core.Reducer.reduce, init); +App.startWithState(state, Core.Reducer.reduce, init); diff --git a/test/editor/Core/SetupTests.re b/test/editor/Core/SetupTests.re index d884d091d1..daf0c28d7c 100644 --- a/test/editor/Core/SetupTests.re +++ b/test/editor/Core/SetupTests.re @@ -3,7 +3,7 @@ open TestFramework; describe("Setup", ({test, _}) => test("ofString", ({expect}) => { - let setupInfo = "{neovim:\"/path/to/neovim\",node:\"/path/to/node\",textmateService:\"/path/to/textmate\",bundledExtensions:\"/path/to/extensions\",configuration:\"/path/to/config\"}"; + let setupInfo = "{neovim:\"/path/to/neovim\",node:\"/path/to/node\",textmateService:\"/path/to/textmate\",bundledExtensions:\"/path/to/extensions\",configuration:\"/path/to/config\",keybindings:\"/path/to/keybindings\"}"; let setup = Setup.ofString(setupInfo); expect.string(setup.neovimPath).toEqual("/path/to/neovim"); expect.string(setup.nodePath).toEqual("/path/to/node"); @@ -12,5 +12,6 @@ describe("Setup", ({test, _}) => "/path/to/extensions", ); expect.string(setup.configPath).toEqual("/path/to/config"); + expect.string(setup.keybindingsPath).toEqual("/path/to/keybindings"); }) );