diff --git a/browser/src/Editor/NeovimEditor/NeovimEditor.tsx b/browser/src/Editor/NeovimEditor/NeovimEditor.tsx index 2ef1588c4..5bd8ff995 100644 --- a/browser/src/Editor/NeovimEditor/NeovimEditor.tsx +++ b/browser/src/Editor/NeovimEditor/NeovimEditor.tsx @@ -764,6 +764,13 @@ export class NeovimEditor extends Editor implements Oni.Editor { return this._neovimInstance.blockInput(inputFunction) } + public async checkMapping( + key: string, + mode: "n" | "v" | "i", + ): Promise<{ key: string; mapping: string }> { + return this._neovimInstance.checkUserMapping({ key, mode }) + } + public dispose(): void { super.dispose() diff --git a/browser/src/Editor/NeovimEditor/WelcomeBufferLayer.tsx b/browser/src/Editor/NeovimEditor/WelcomeBufferLayer.tsx index 506b580ba..25fdef459 100644 --- a/browser/src/Editor/NeovimEditor/WelcomeBufferLayer.tsx +++ b/browser/src/Editor/NeovimEditor/WelcomeBufferLayer.tsx @@ -301,9 +301,9 @@ export interface IWelcomeInputEvent { horizontal?: number } -interface ICommandMetadata { +interface ICommandMetadata { + execute: (args?: T) => void command: string - args?: T } export interface IWelcomeCommandsDictionary { @@ -315,23 +315,67 @@ export interface IWelcomeCommandsDictionary { openWorkspaceFolder: ICommandMetadata commandPalette: ICommandMetadata commandline: ICommandMetadata + restoreSession: (sessionName: string) => Promise } export class WelcomeBufferLayer implements Oni.BufferLayer { public inputEvent = new Event() - public readonly welcomeCommands: IWelcomeCommandsDictionary = { - openFile: { command: "oni.editor.newFile" }, - openWorkspaceFolder: { command: "workspace.openFolder" }, - commandPalette: { command: "quickOpen.show" }, - commandline: { command: "executeVimCommand" }, - openTutor: { command: "oni.tutor.open" }, - openDocs: { command: "oni.docs.open" }, - openConfig: { command: "oni.config.openUserConfig" }, - openThemes: { command: "oni.themes.open" }, + constructor(private _oni: OniWithActiveSection) {} + + public showCommandline = async () => { + const remapping: string = await this._oni.editors.activeEditor.neovim.callFunction( + "mapcheck", + [":", "n"], + ) + const mapping = remapping || ":" + this._oni.automation.sendKeys(mapping) } - constructor(private _oni: OniWithActiveSection) {} + public executeCommand: ExecuteCommand = (cmd, args) => { + this._oni.commands.executeCommand(cmd, args) + } + + public restoreSession = async (name: string) => { + await this._oni.sessions.restoreSession(name) + } + + // tslint:disable-next-line + public readonly welcomeCommands: IWelcomeCommandsDictionary = { + openFile: { + execute: args => this.executeCommand("oni.editor.newFile", args), + command: "oni.editor.newFile", + }, + openWorkspaceFolder: { + execute: args => this.executeCommand("workspace.openFolder", args), + command: "workspace.openFolder", + }, + commandPalette: { + execute: args => this.executeCommand("commands.show", args), + command: "commands.show", + }, + commandline: { + execute: this.showCommandline, + command: "editor.executeVimCommand", + }, + openTutor: { + execute: args => this.executeCommand("oni.tutor.open", args), + command: "oni.tutor.open", + }, + openDocs: { + execute: args => this.executeCommand("oni.docs.open", args), + command: "oni.docs.open", + }, + openConfig: { + execute: args => this.executeCommand("oni.config.openUserConfig", args), + command: "oni.config.openUserConfig", + }, + openThemes: { + execute: args => this.executeCommand("oni.themes.choose", args), + command: "oni.themes.open", + }, + restoreSession: args => this.restoreSession(args), + } public get id() { return "oni.welcome" @@ -369,19 +413,12 @@ export class WelcomeBufferLayer implements Oni.BufferLayer { } } - public executeCommand: ExecuteCommand = (cmd, args) => { - if (cmd) { - this._oni.commands.executeCommand(cmd, args) - } - } - - public restoreSession = async (name: string) => { - await this._oni.sessions.restoreSession(name) - } - public getProps() { const active = this._oni.getActiveSection() === "editor" - const commandIds = Object.values(this.welcomeCommands).map(({ command }) => command) + const commandIds = Object.values(this.welcomeCommands) + .map(({ command }) => command) + .filter(Boolean) + const sessions = this._oni.sessions ? this._oni.sessions.allSessions : ([] as ISession[]) const sessionIds = sessions.map(({ id }) => id) const ids = [...commandIds, ...sessionIds] @@ -442,7 +479,7 @@ export class WelcomeView extends React.PureComponent { const { currentIndex } = this.state - const { sections, ids, executeCommand, active } = this.props + const { sections, ids, active } = this.props const newIndex = this.getNextIndex(currentIndex, vertical, horizontal, sections) const selectedId = ids[newIndex] @@ -452,10 +489,10 @@ export class WelcomeView extends React.PureComponent { selectedId: string } -export class WelcomeCommandsView extends React.PureComponent { - public render() { - const { commands, executeCommand } = this.props - const isSelected = (command: string) => command === this.props.selectedId - return ( - - - Quick Commands - executeCommand(commands.openFile.command)} - description="Control + N" - command={commands.openFile.command} - selected={isSelected(commands.openFile.command)} - /> - executeCommand(commands.openWorkspaceFolder.command)} - description="Control + O" - command={commands.openWorkspaceFolder.command} - selected={isSelected(commands.openWorkspaceFolder.command)} - /> - executeCommand(commands.commandPalette.command)} - description="Control + Shift + P" - command={commands.commandPalette.command} - selected={isSelected(commands.commandPalette.command)} - /> - executeCommand(commands.commandline.command)} - selected={isSelected(commands.commandline.command)} - /> - - - Learn - executeCommand(commands.openTutor.command)} - description="Learn modal editing with an interactive tutorial." - command={commands.openTutor.command} - selected={isSelected(commands.openTutor.command)} - /> - executeCommand(commands.openDocs.command)} - description="Discover what Oni can do for you." - command={commands.openDocs.command} - selected={isSelected(commands.openDocs.command)} - /> - - - Customize - executeCommand(commands.openConfig.command)} - description="Make Oni work the way you want." - command={commands.openConfig.command} - selected={isSelected(commands.openConfig.command)} - /> - executeCommand(commands.openThemes.command)} - description="Choose a theme that works for you." - command={commands.openThemes.command} - selected={isSelected(commands.openThemes.command)} - /> - - - ) - } +export const WelcomeCommandsView: React.SFC = props => { + const isSelected = (command: string) => command === props.selectedId + const { commands } = props + return ( + + + Quick Commands + commands.openFile.execute()} + description="Control + N" + command={commands.openFile.command} + selected={isSelected(commands.openFile.command)} + /> + commands.openWorkspaceFolder.execute()} + command={commands.openWorkspaceFolder.command} + selected={isSelected(commands.openWorkspaceFolder.command)} + /> + commands.commandPalette.execute()} + description="Control + Shift + P" + command={commands.commandPalette.command} + selected={isSelected(commands.commandPalette.command)} + /> + commands.commandline.execute()} + selected={isSelected(commands.commandline.command)} + /> + + + Learn + commands.openTutor.execute()} + description="Learn modal editing with an interactive tutorial." + command={commands.openTutor.command} + selected={isSelected(commands.openTutor.command)} + /> + commands.openDocs.execute()} + description="Discover what Oni can do for you." + command={commands.openDocs.command} + selected={isSelected(commands.openDocs.command)} + /> + + + Customize + commands.openConfig.execute()} + description="Make Oni work the way you want." + command={commands.openConfig.command} + selected={isSelected(commands.openConfig.command)} + /> + commands.openThemes.execute()} + description="Choose a theme that works for you." + command={commands.openThemes.command} + selected={isSelected(commands.openThemes.command)} + /> + + + ) } diff --git a/browser/src/Editor/OniEditor/OniEditor.tsx b/browser/src/Editor/OniEditor/OniEditor.tsx index 403d2c3df..869490052 100644 --- a/browser/src/Editor/OniEditor/OniEditor.tsx +++ b/browser/src/Editor/OniEditor/OniEditor.tsx @@ -210,12 +210,7 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor { ) } - this._neovimEditor.onShowWelcomeScreen.subscribe(async () => { - const oni = this._pluginManager.getApi() - const welcomeBuffer = await this._neovimEditor.createWelcomeBuffer() - const welcomeLayer = new WelcomeBufferLayer(oni as OniWithActiveSection) - welcomeBuffer.addLayer(welcomeLayer) - }) + this._neovimEditor.onShowWelcomeScreen.subscribe(this.openWelcomeScreen) } public dispose(): void { @@ -233,6 +228,14 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor { this._neovimEditor.enter() + commandManager.registerCommand({ + name: "Oni: Show Welcome", + detail: "Open the welcome screen", + command: "oni.welcome.open", + execute: this.openWelcomeScreen, + enabled: () => this._configuration.getValue("experimental.welcome.enabled"), + }) + commandManager.registerCommand({ command: "editor.split.horizontal", execute: () => this._split("horizontal"), @@ -255,6 +258,13 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor { this._neovimEditor.leave() } + public openWelcomeScreen = async () => { + const oni = this._pluginManager.getApi() + const welcomeBuffer = await this._neovimEditor.createWelcomeBuffer() + const welcomeLayer = new WelcomeBufferLayer(oni as OniWithActiveSection) + welcomeBuffer.addLayer(welcomeLayer) + } + public async openFile( file: string, openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions, diff --git a/browser/src/neovim/NeovimInstance.ts b/browser/src/neovim/NeovimInstance.ts index 208ce294f..a88ce4e72 100644 --- a/browser/src/neovim/NeovimInstance.ts +++ b/browser/src/neovim/NeovimInstance.ts @@ -75,6 +75,13 @@ export interface INeovimCompletionInfo { export type CommandLineContent = [any, string] +export type MapcheckModes = "n" | "v" | "i" + +interface IMapping { + key: string + mode: MapcheckModes +} + export interface INeovimCommandLineShowEvent { content: CommandLineContent[] pos: number @@ -411,6 +418,16 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { await this.command(`cd! ${directoryPath}`) } + public async checkUserMapping({ key, mode = "n" }: IMapping) { + const mapping: string = await this.callFunction("mapcheck", [key, mode]) + return { key, mapping } + } + + public async checkUserMappings(keys: IMapping[]) { + const mappings = await Promise.all(keys.map(this.checkUserMapping)) + return mappings + } + // Make a direct request against the msgpack API public async request(request: string, args: any[]): Promise { if (!this._neovim || this._neovim.isDisposed) { diff --git a/ui-tests/WelcomeCommandsView.test.tsx b/ui-tests/WelcomeCommandsView.test.tsx index a6bb01279..8fc82b4f9 100644 --- a/ui-tests/WelcomeCommandsView.test.tsx +++ b/ui-tests/WelcomeCommandsView.test.tsx @@ -20,24 +20,52 @@ describe("Welcome Layer test", () => { "button8", ] + const restoreSession = jest.fn() + const executeCommand = jest.fn() + const commands = { - openFile: { command: "button1" }, - openTutor: { command: "button2" }, - openDocs: { command: "button3" }, - openConfig: { command: "button4" }, - openThemes: { command: "button5" }, - openWorkspaceFolder: { command: "button6" }, - commandPalette: { command: "button7" }, - commandline: { command: "button8" }, + openFile: { + execute: jest.fn(), + command: "button1", + }, + openTutor: { + execute: jest.fn(), + command: "button2", + }, + openDocs: { + execute: jest.fn(), + command: "button3", + }, + openConfig: { + execute: jest.fn(), + command: "button4", + }, + openThemes: { + execute: jest.fn(), + command: "button5", + }, + openWorkspaceFolder: { + execute: jest.fn(), + command: "button6", + }, + commandPalette: { + execute: jest.fn(), + command: "button7", + }, + commandline: { + execute: jest.fn(), + command: "button8", + }, + restoreSession, } - const executeMock = jest.fn() + it("should render without crashing", () => { const wrapper = shallow( , ) expect(wrapper.length).toBe(1) @@ -49,7 +77,7 @@ describe("Welcome Layer test", () => { selectedId="button1" active commands={commands} - executeCommand={executeMock} + executeCommand={executeCommand} />, ) expect(toJson(wrapper)).toMatchSnapshot() @@ -60,9 +88,9 @@ describe("Welcome Layer test", () => { selectedId="button1" active commands={commands} - executeCommand={executeMock} + executeCommand={executeCommand} />, ) - expect(wrapper.dive().find(WelcomeButton).length).toBe(Object.values(commands).length) + expect(wrapper.dive().find(WelcomeButton).length).toBe(8) }) }) diff --git a/ui-tests/WelcomeView.test.tsx b/ui-tests/WelcomeView.test.tsx index 8f8f01634..2de8561f8 100644 --- a/ui-tests/WelcomeView.test.tsx +++ b/ui-tests/WelcomeView.test.tsx @@ -25,15 +25,43 @@ describe("", () => { "button8", ] + const restoreSession = jest.fn() + const executeCommand = jest.fn() + const commands = { - openFile: { command: "button1" }, - openTutor: { command: "button2" }, - openDocs: { command: "button3" }, - openConfig: { command: "button4" }, - openThemes: { command: "button5" }, - openWorkspaceFolder: { command: "button6" }, - commandPalette: { command: "button7" }, - commandline: { command: "button8" }, + openFile: { + execute: jest.fn(), + command: "button1", + }, + openTutor: { + execute: jest.fn(), + command: "button2", + }, + openDocs: { + execute: jest.fn(), + command: "button3", + }, + openConfig: { + execute: jest.fn(), + command: "button4", + }, + openThemes: { + execute: jest.fn(), + command: "button5", + }, + openWorkspaceFolder: { + execute: jest.fn(), + command: "button6", + }, + quickOpenShow: { + execute: jest.fn(), + command: "button7", + }, + commandline: { + execute: jest.fn(), + command: "button8", + }, + restoreSession, } const sessions = [ @@ -46,8 +74,6 @@ describe("", () => { }, ] - const restoreSession = jest.fn() - const executeCommand = jest.fn() const inputEvent = new Event("handleInputTestEvent") const getMetadata = async () => ({ version: "1" }) @@ -107,7 +133,7 @@ describe("", () => { it("should trigger a command on enter event", () => { const instance = wrapper.instance() as WelcomeView instance.handleInput({ vertical: 0, select: true }) - expect(executeCommand.mock.calls[0][0]).toBe("button1") + expect(commands.openFile.execute).toHaveBeenCalled() }) it("should navigate right if horizontal is 1", () => { diff --git a/ui-tests/__snapshots__/WelcomeView.test.tsx.snap b/ui-tests/__snapshots__/WelcomeView.test.tsx.snap index 8ccdf48d2..2fd4d0eef 100644 --- a/ui-tests/__snapshots__/WelcomeView.test.tsx.snap +++ b/ui-tests/__snapshots__/WelcomeView.test.tsx.snap @@ -78,33 +78,42 @@ exports[` should match its last snapshot 1`] = ` ] } > - { + const context = { + isActive: true, + windowId: 1, + fontPixelWidth: 3, + fontPixelHeight: 10, + cursorColumn: 4, + cursorLine: 30, + bufferToScreen: jest.fn(), + screenToPixel: jest.fn(), + bufferToPixel: jest.fn().mockReturnValue({ + pixelX: 20, + pixelY: 20, + }), + dimensions: { + width: 100, + height: 100, + x: 0, + y: 0, + }, + visibleLines: [], + topBufferLine: 20, + bottomBufferLine: 40, + } + + const mockEvent = { + dispatch: jest.fn(), + subscribe: jest.fn(), + } + + const mockRestoreSession = jest.fn() + const getActiveSectionMock = jest.fn() + + let layer: WelcomeBufferLayer + + const setup = () => { + const oni = new MockOni({ + getActiveSection: jest.fn(), + sessions: { restoreSession: mockRestoreSession, allSessions: [] }, + }) + getActiveSectionMock.mockReturnValue("editor") + layer = new WelcomeBufferLayer(oni as OniWithActiveSection) + layer.inputEvent = mockEvent as any + } + + beforeEach(() => { + setup() + }) + + afterEach(() => { + mockEvent.dispatch.mockClear() + mockEvent.subscribe.mockClear() + }) + + it("should correctly return a component", () => { + // this test can return the actual jsx but its difficult to test the exact return value + expect(layer.render(context)).toBeTruthy() + }) + + it("should correctly dispatch a right navigation event", () => { + layer.handleInput("l") + expect(mockEvent.dispatch.mock.calls[0][0]).toEqual({ + horizontal: 1, + vertical: 0, + select: false, + }) + }) + + it("should correctly dispatch a upwards navigation event", () => { + layer.handleInput("k") + expect(mockEvent.dispatch.mock.calls[0][0]).toEqual({ + vertical: -1, + select: false, + }) + }) + + it("should correctly dispatch a upwards navigation event", () => { + layer.handleInput("j") + expect(mockEvent.dispatch.mock.calls[0][0]).toEqual({ + vertical: 1, + select: false, + }) + }) + + it("should correctly return an active status of false if the editor is not active", () => { + getActiveSectionMock.mockReturnValueOnce("commandline") + expect(layer.isActive()).toBe(false) + }) + + it("should have the correct command names [THIS IS TO PREVENT WELCOME COMMAND REGRESSIONS]", () => { + const existingCommands = [ + "oni.editor.newFile", + "workspace.openFolder", + "commands.show", + "editor.executeVimCommand", + "oni.tutor.open", + "oni.docs.open", + "oni.config.openUserConfig", + "oni.themes.open", + ] + const props = layer.getProps() + expect(props.ids).toEqual(existingCommands) + expect(props.sections).toEqual([8]) + }) +})