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) 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 { public dispose(): void {
super.dispose() super.dispose()

View File

@ -301,9 +301,9 @@ export interface IWelcomeInputEvent {
horizontal?: number horizontal?: number
} }
interface ICommandMetadata<T = undefined> { interface ICommandMetadata {
execute: <T = undefined>(args?: T) => void
command: string command: string
args?: T
} }
export interface IWelcomeCommandsDictionary { export interface IWelcomeCommandsDictionary {
@ -315,23 +315,67 @@ export interface IWelcomeCommandsDictionary {
openWorkspaceFolder: ICommandMetadata openWorkspaceFolder: ICommandMetadata
commandPalette: ICommandMetadata commandPalette: ICommandMetadata
commandline: ICommandMetadata commandline: ICommandMetadata
restoreSession: (sessionName: string) => Promise<void>
} }
export class WelcomeBufferLayer implements Oni.BufferLayer { export class WelcomeBufferLayer implements Oni.BufferLayer {
public inputEvent = new Event<IWelcomeInputEvent>() public inputEvent = new Event<IWelcomeInputEvent>()
public readonly welcomeCommands: IWelcomeCommandsDictionary = { constructor(private _oni: OniWithActiveSection) {}
openFile: { command: "oni.editor.newFile" },
openWorkspaceFolder: { command: "workspace.openFolder" }, public showCommandline = async () => {
commandPalette: { command: "quickOpen.show" }, const remapping: string = await this._oni.editors.activeEditor.neovim.callFunction(
commandline: { command: "executeVimCommand" }, "mapcheck",
openTutor: { command: "oni.tutor.open" }, [":", "n"],
openDocs: { command: "oni.docs.open" }, )
openConfig: { command: "oni.config.openUserConfig" }, const mapping = remapping || ":"
openThemes: { command: "oni.themes.open" }, 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() { public get id() {
return "oni.welcome" 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() { public getProps() {
const active = this._oni.getActiveSection() === "editor" 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 sessions = this._oni.sessions ? this._oni.sessions.allSessions : ([] as ISession[])
const sessionIds = sessions.map(({ id }) => id) const sessionIds = sessions.map(({ id }) => id)
const ids = [...commandIds, ...sessionIds] const ids = [...commandIds, ...sessionIds]
@ -442,7 +479,7 @@ export class WelcomeView extends React.PureComponent<WelcomeViewProps, WelcomeVi
public handleInput = async ({ vertical, select, horizontal }: IWelcomeInputEvent) => { public handleInput = async ({ vertical, select, horizontal }: IWelcomeInputEvent) => {
const { currentIndex } = this.state 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 newIndex = this.getNextIndex(currentIndex, vertical, horizontal, sections)
const selectedId = ids[newIndex] const selectedId = ids[newIndex]
@ -452,10 +489,10 @@ export class WelcomeView extends React.PureComponent<WelcomeViewProps, WelcomeVi
if (select && active) { if (select && active) {
if (selectedSession) { if (selectedSession) {
await this.props.restoreSession(selectedSession.name) await this.props.commands.restoreSession(selectedSession.name)
} else { } else {
const currentCommand = this.getCurrentCommand(selectedId) const currentCommand = this.getCurrentCommand(selectedId)
executeCommand(currentCommand.command, currentCommand.args) currentCommand.execute()
} }
} }
} }
@ -552,78 +589,76 @@ export interface IWelcomeCommandsViewProps extends Partial<WelcomeViewProps> {
selectedId: string selectedId: string
} }
export class WelcomeCommandsView extends React.PureComponent<IWelcomeCommandsViewProps, {}> { export const WelcomeCommandsView: React.SFC<IWelcomeCommandsViewProps> = props => {
public render() { const isSelected = (command: string) => command === props.selectedId
const { commands, executeCommand } = this.props const { commands } = props
const isSelected = (command: string) => command === this.props.selectedId return (
return ( <LeftColumn>
<LeftColumn> <AnimatedContainer duration="0.25s">
<AnimatedContainer duration="0.25s"> <SectionHeader>Quick Commands</SectionHeader>
<SectionHeader>Quick Commands</SectionHeader> <WelcomeButton
<WelcomeButton title="New File"
title="New File" onClick={() => commands.openFile.execute()}
onClick={() => executeCommand(commands.openFile.command)} description="Control + N"
description="Control + N" command={commands.openFile.command}
command={commands.openFile.command} selected={isSelected(commands.openFile.command)}
selected={isSelected(commands.openFile.command)} />
/> <WelcomeButton
<WelcomeButton title="Open File / Folder"
title="Open File / Folder" description="Control + O"
onClick={() => executeCommand(commands.openWorkspaceFolder.command)} onClick={() => commands.openWorkspaceFolder.execute()}
description="Control + O" command={commands.openWorkspaceFolder.command}
command={commands.openWorkspaceFolder.command} selected={isSelected(commands.openWorkspaceFolder.command)}
selected={isSelected(commands.openWorkspaceFolder.command)} />
/> <WelcomeButton
<WelcomeButton title="Command Palette"
title="Command Palette" onClick={() => commands.commandPalette.execute()}
onClick={() => executeCommand(commands.commandPalette.command)} description="Control + Shift + P"
description="Control + Shift + P" command={commands.commandPalette.command}
command={commands.commandPalette.command} selected={isSelected(commands.commandPalette.command)}
selected={isSelected(commands.commandPalette.command)} />
/> <WelcomeButton
<WelcomeButton title="Vim Ex Commands"
title="Vim Ex Commands" description=":"
description=":" command="editor.openExCommands"
command="editor.openExCommands" onClick={() => commands.commandline.execute()}
onClick={() => executeCommand(commands.commandline.command)} selected={isSelected(commands.commandline.command)}
selected={isSelected(commands.commandline.command)} />
/> </AnimatedContainer>
</AnimatedContainer> <AnimatedContainer duration="0.25s">
<AnimatedContainer duration="0.25s"> <SectionHeader>Learn</SectionHeader>
<SectionHeader>Learn</SectionHeader> <WelcomeButton
<WelcomeButton title="Tutor"
title="Tutor" onClick={() => commands.openTutor.execute()}
onClick={() => executeCommand(commands.openTutor.command)} description="Learn modal editing with an interactive tutorial."
description="Learn modal editing with an interactive tutorial." command={commands.openTutor.command}
command={commands.openTutor.command} selected={isSelected(commands.openTutor.command)}
selected={isSelected(commands.openTutor.command)} />
/> <WelcomeButton
<WelcomeButton title="Documentation"
title="Documentation" onClick={() => commands.openDocs.execute()}
onClick={() => executeCommand(commands.openDocs.command)} description="Discover what Oni can do for you."
description="Discover what Oni can do for you." command={commands.openDocs.command}
command={commands.openDocs.command} selected={isSelected(commands.openDocs.command)}
selected={isSelected(commands.openDocs.command)} />
/> </AnimatedContainer>
</AnimatedContainer> <AnimatedContainer duration="0.25s">
<AnimatedContainer duration="0.25s"> <SectionHeader>Customize</SectionHeader>
<SectionHeader>Customize</SectionHeader> <WelcomeButton
<WelcomeButton title="Configure"
title="Configure" onClick={() => commands.openConfig.execute()}
onClick={() => executeCommand(commands.openConfig.command)} description="Make Oni work the way you want."
description="Make Oni work the way you want." command={commands.openConfig.command}
command={commands.openConfig.command} selected={isSelected(commands.openConfig.command)}
selected={isSelected(commands.openConfig.command)} />
/> <WelcomeButton
<WelcomeButton title="Themes"
title="Themes" onClick={() => commands.openThemes.execute()}
onClick={() => executeCommand(commands.openThemes.command)} description="Choose a theme that works for you."
description="Choose a theme that works for you." command={commands.openThemes.command}
command={commands.openThemes.command} selected={isSelected(commands.openThemes.command)}
selected={isSelected(commands.openThemes.command)} />
/> </AnimatedContainer>
</AnimatedContainer> </LeftColumn>
</LeftColumn> )
)
}
} }

View File

@ -210,12 +210,7 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor {
) )
} }
this._neovimEditor.onShowWelcomeScreen.subscribe(async () => { this._neovimEditor.onShowWelcomeScreen.subscribe(this.openWelcomeScreen)
const oni = this._pluginManager.getApi()
const welcomeBuffer = await this._neovimEditor.createWelcomeBuffer()
const welcomeLayer = new WelcomeBufferLayer(oni as OniWithActiveSection)
welcomeBuffer.addLayer(welcomeLayer)
})
} }
public dispose(): void { public dispose(): void {
@ -233,6 +228,14 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor {
this._neovimEditor.enter() 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({ commandManager.registerCommand({
command: "editor.split.horizontal", command: "editor.split.horizontal",
execute: () => this._split("horizontal"), execute: () => this._split("horizontal"),
@ -255,6 +258,13 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor {
this._neovimEditor.leave() 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( public async openFile(
file: string, file: string,
openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions, openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions,

View File

@ -75,6 +75,13 @@ export interface INeovimCompletionInfo {
export type CommandLineContent = [any, string] export type CommandLineContent = [any, string]
export type MapcheckModes = "n" | "v" | "i"
interface IMapping {
key: string
mode: MapcheckModes
}
export interface INeovimCommandLineShowEvent { export interface INeovimCommandLineShowEvent {
content: CommandLineContent[] content: CommandLineContent[]
pos: number pos: number
@ -411,6 +418,16 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance {
await this.command(`cd! ${directoryPath}`) 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 // Make a direct request against the msgpack API
public async request<T>(request: string, args: any[]): Promise<T> { public async request<T>(request: string, args: any[]): Promise<T> {
if (!this._neovim || this._neovim.isDisposed) { if (!this._neovim || this._neovim.isDisposed) {

View File

@ -20,24 +20,52 @@ describe("Welcome Layer test", () => {
"button8", "button8",
] ]
const restoreSession = jest.fn()
const executeCommand = jest.fn()
const commands = { const commands = {
openFile: { command: "button1" }, openFile: {
openTutor: { command: "button2" }, execute: jest.fn(),
openDocs: { command: "button3" }, command: "button1",
openConfig: { command: "button4" }, },
openThemes: { command: "button5" }, openTutor: {
openWorkspaceFolder: { command: "button6" }, execute: jest.fn(),
commandPalette: { command: "button7" }, command: "button2",
commandline: { command: "button8" }, },
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", () => { it("should render without crashing", () => {
const wrapper = shallow( const wrapper = shallow(
<WelcomeCommandsView <WelcomeCommandsView
selectedId="button1" selectedId="button1"
active active
commands={commands} commands={commands}
executeCommand={executeMock} executeCommand={executeCommand}
/>, />,
) )
expect(wrapper.length).toBe(1) expect(wrapper.length).toBe(1)
@ -49,7 +77,7 @@ describe("Welcome Layer test", () => {
selectedId="button1" selectedId="button1"
active active
commands={commands} commands={commands}
executeCommand={executeMock} executeCommand={executeCommand}
/>, />,
) )
expect(toJson(wrapper)).toMatchSnapshot() expect(toJson(wrapper)).toMatchSnapshot()
@ -60,9 +88,9 @@ describe("Welcome Layer test", () => {
selectedId="button1" selectedId="button1"
active active
commands={commands} 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", "button8",
] ]
const restoreSession = jest.fn()
const executeCommand = jest.fn()
const commands = { const commands = {
openFile: { command: "button1" }, openFile: {
openTutor: { command: "button2" }, execute: jest.fn(),
openDocs: { command: "button3" }, command: "button1",
openConfig: { command: "button4" }, },
openThemes: { command: "button5" }, openTutor: {
openWorkspaceFolder: { command: "button6" }, execute: jest.fn(),
commandPalette: { command: "button7" }, command: "button2",
commandline: { command: "button8" }, },
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 = [ const sessions = [
@ -46,8 +74,6 @@ describe("<WelcomeView />", () => {
}, },
] ]
const restoreSession = jest.fn()
const executeCommand = jest.fn()
const inputEvent = new Event<IWelcomeInputEvent>("handleInputTestEvent") const inputEvent = new Event<IWelcomeInputEvent>("handleInputTestEvent")
const getMetadata = async () => ({ version: "1" }) const getMetadata = async () => ({ version: "1" })
@ -107,7 +133,7 @@ describe("<WelcomeView />", () => {
it("should trigger a command on enter event", () => { it("should trigger a command on enter event", () => {
const instance = wrapper.instance() as WelcomeView const instance = wrapper.instance() as WelcomeView
instance.handleInput({ vertical: 0, select: true }) 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", () => { 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={ commands={
Object { Object {
"commandPalette": Object {
"command": "button7",
},
"commandline": Object { "commandline": Object {
"command": "button8", "command": "button8",
"execute": [MockFunction],
}, },
"openConfig": Object { "openConfig": Object {
"command": "button4", "command": "button4",
"execute": [MockFunction],
}, },
"openDocs": Object { "openDocs": Object {
"command": "button3", "command": "button3",
"execute": [MockFunction],
}, },
"openFile": Object { "openFile": Object {
"command": "button1", "command": "button1",
"execute": [MockFunction],
}, },
"openThemes": Object { "openThemes": Object {
"command": "button5", "command": "button5",
"execute": [MockFunction],
}, },
"openTutor": Object { "openTutor": Object {
"command": "button2", "command": "button2",
"execute": [MockFunction],
}, },
"openWorkspaceFolder": Object { "openWorkspaceFolder": Object {
"command": "button6", "command": "button6",
"execute": [MockFunction],
}, },
"quickOpenShow": Object {
"command": "button7",
"execute": [MockFunction],
},
"restoreSession": [MockFunction],
} }
} }
executeCommand={[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])
})
})