Feature/fix remaining welcome commands (#2533)

* rename commands in welcome

* fix welcome commands make open welcome a separate method
add a command for this
add helper method to check a users binding

* add lint ignore comment

* update tests to match new commands

* add tests for the welcome layer itself

* add small explainer comment

* add parens to call the open config function on click

* fix command palette button and naming on welcome screen

* update welcomelayer test, convert commands view to sfc

* update snapshot

* fix command rename in mock for commands view
This commit is contained in:
Akin 2018-09-10 11:53:34 +01:00 committed by GitHub
parent 5c5beef05c
commit a4c8515d62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 381 additions and 134 deletions

View File

@ -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()

View File

@ -301,9 +301,9 @@ export interface IWelcomeInputEvent {
horizontal?: number
}
interface ICommandMetadata<T = undefined> {
interface ICommandMetadata {
execute: <T = undefined>(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<void>
}
export class WelcomeBufferLayer implements Oni.BufferLayer {
public inputEvent = new Event<IWelcomeInputEvent>()
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<WelcomeViewProps, WelcomeVi
public handleInput = async ({ vertical, select, horizontal }: IWelcomeInputEvent) => {
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<WelcomeViewProps, WelcomeVi
if (select && active) {
if (selectedSession) {
await this.props.restoreSession(selectedSession.name)
await this.props.commands.restoreSession(selectedSession.name)
} else {
const currentCommand = this.getCurrentCommand(selectedId)
executeCommand(currentCommand.command, currentCommand.args)
currentCommand.execute()
}
}
}
@ -552,78 +589,76 @@ export interface IWelcomeCommandsViewProps extends Partial<WelcomeViewProps> {
selectedId: string
}
export class WelcomeCommandsView extends React.PureComponent<IWelcomeCommandsViewProps, {}> {
public render() {
const { commands, executeCommand } = this.props
const isSelected = (command: string) => command === this.props.selectedId
return (
<LeftColumn>
<AnimatedContainer duration="0.25s">
<SectionHeader>Quick Commands</SectionHeader>
<WelcomeButton
title="New File"
onClick={() => executeCommand(commands.openFile.command)}
description="Control + N"
command={commands.openFile.command}
selected={isSelected(commands.openFile.command)}
/>
<WelcomeButton
title="Open File / Folder"
onClick={() => executeCommand(commands.openWorkspaceFolder.command)}
description="Control + O"
command={commands.openWorkspaceFolder.command}
selected={isSelected(commands.openWorkspaceFolder.command)}
/>
<WelcomeButton
title="Command Palette"
onClick={() => executeCommand(commands.commandPalette.command)}
description="Control + Shift + P"
command={commands.commandPalette.command}
selected={isSelected(commands.commandPalette.command)}
/>
<WelcomeButton
title="Vim Ex Commands"
description=":"
command="editor.openExCommands"
onClick={() => executeCommand(commands.commandline.command)}
selected={isSelected(commands.commandline.command)}
/>
</AnimatedContainer>
<AnimatedContainer duration="0.25s">
<SectionHeader>Learn</SectionHeader>
<WelcomeButton
title="Tutor"
onClick={() => executeCommand(commands.openTutor.command)}
description="Learn modal editing with an interactive tutorial."
command={commands.openTutor.command}
selected={isSelected(commands.openTutor.command)}
/>
<WelcomeButton
title="Documentation"
onClick={() => executeCommand(commands.openDocs.command)}
description="Discover what Oni can do for you."
command={commands.openDocs.command}
selected={isSelected(commands.openDocs.command)}
/>
</AnimatedContainer>
<AnimatedContainer duration="0.25s">
<SectionHeader>Customize</SectionHeader>
<WelcomeButton
title="Configure"
onClick={() => executeCommand(commands.openConfig.command)}
description="Make Oni work the way you want."
command={commands.openConfig.command}
selected={isSelected(commands.openConfig.command)}
/>
<WelcomeButton
title="Themes"
onClick={() => executeCommand(commands.openThemes.command)}
description="Choose a theme that works for you."
command={commands.openThemes.command}
selected={isSelected(commands.openThemes.command)}
/>
</AnimatedContainer>
</LeftColumn>
)
}
export const WelcomeCommandsView: React.SFC<IWelcomeCommandsViewProps> = props => {
const isSelected = (command: string) => command === props.selectedId
const { commands } = props
return (
<LeftColumn>
<AnimatedContainer duration="0.25s">
<SectionHeader>Quick Commands</SectionHeader>
<WelcomeButton
title="New File"
onClick={() => commands.openFile.execute()}
description="Control + N"
command={commands.openFile.command}
selected={isSelected(commands.openFile.command)}
/>
<WelcomeButton
title="Open File / Folder"
description="Control + O"
onClick={() => commands.openWorkspaceFolder.execute()}
command={commands.openWorkspaceFolder.command}
selected={isSelected(commands.openWorkspaceFolder.command)}
/>
<WelcomeButton
title="Command Palette"
onClick={() => commands.commandPalette.execute()}
description="Control + Shift + P"
command={commands.commandPalette.command}
selected={isSelected(commands.commandPalette.command)}
/>
<WelcomeButton
title="Vim Ex Commands"
description=":"
command="editor.openExCommands"
onClick={() => commands.commandline.execute()}
selected={isSelected(commands.commandline.command)}
/>
</AnimatedContainer>
<AnimatedContainer duration="0.25s">
<SectionHeader>Learn</SectionHeader>
<WelcomeButton
title="Tutor"
onClick={() => commands.openTutor.execute()}
description="Learn modal editing with an interactive tutorial."
command={commands.openTutor.command}
selected={isSelected(commands.openTutor.command)}
/>
<WelcomeButton
title="Documentation"
onClick={() => commands.openDocs.execute()}
description="Discover what Oni can do for you."
command={commands.openDocs.command}
selected={isSelected(commands.openDocs.command)}
/>
</AnimatedContainer>
<AnimatedContainer duration="0.25s">
<SectionHeader>Customize</SectionHeader>
<WelcomeButton
title="Configure"
onClick={() => commands.openConfig.execute()}
description="Make Oni work the way you want."
command={commands.openConfig.command}
selected={isSelected(commands.openConfig.command)}
/>
<WelcomeButton
title="Themes"
onClick={() => commands.openThemes.execute()}
description="Choose a theme that works for you."
command={commands.openThemes.command}
selected={isSelected(commands.openThemes.command)}
/>
</AnimatedContainer>
</LeftColumn>
)
}

View File

@ -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,

View File

@ -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<T>(request: string, args: any[]): Promise<T> {
if (!this._neovim || this._neovim.isDisposed) {

View File

@ -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(
<WelcomeCommandsView
selectedId="button1"
active
commands={commands}
executeCommand={executeMock}
executeCommand={executeCommand}
/>,
)
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)
})
})

View File

@ -25,15 +25,43 @@ describe("<WelcomeView />", () => {
"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("<WelcomeView />", () => {
},
]
const restoreSession = jest.fn()
const executeCommand = jest.fn()
const inputEvent = new Event<IWelcomeInputEvent>("handleInputTestEvent")
const getMetadata = async () => ({ version: "1" })
@ -107,7 +133,7 @@ describe("<WelcomeView />", () => {
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", () => {

View File

@ -78,33 +78,42 @@ exports[`<WelcomeView /> should match its last snapshot 1`] = `
]
}
>
<WelcomeCommandsView
<Component
commands={
Object {
"commandPalette": Object {
"command": "button7",
},
"commandline": Object {
"command": "button8",
"execute": [MockFunction],
},
"openConfig": Object {
"command": "button4",
"execute": [MockFunction],
},
"openDocs": Object {
"command": "button3",
"execute": [MockFunction],
},
"openFile": Object {
"command": "button1",
"execute": [MockFunction],
},
"openThemes": Object {
"command": "button5",
"execute": [MockFunction],
},
"openTutor": Object {
"command": "button2",
"execute": [MockFunction],
},
"openWorkspaceFolder": Object {
"command": "button6",
"execute": [MockFunction],
},
"quickOpenShow": Object {
"command": "button7",
"execute": [MockFunction],
},
"restoreSession": [MockFunction],
}
}
executeCommand={[MockFunction]}

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Event } from "oni-types"
import {
IWelcomeInputEvent,
OniWithActiveSection,
WelcomeBufferLayer,
} from "./../browser/src/Editor/NeovimEditor/WelcomeBufferLayer"
import MockOni from "./mocks/Oni"
describe("Welcome Layer tests", () => {
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])
})
})