Skip to content
This repository was archived by the owner on Apr 1, 2020. It is now read-only.

Commit 262098f

Browse files
authored
Input: Reverse lookup API (#1365)
* Add 'getBoundKeys' method and unit tests * Show bound keys in command palette * Add keyparser info * Merge master * Get parsing tests green * Fix missed semicolon * Revert suppressing the file open behavior * Clean up typescript package file * Fix lint issue * Add KeyBindingInfo * Improve look and feel of reverse bindings * Restore pinned icon in quick open * Fix lint issues
1 parent c2a2244 commit 262098f

File tree

13 files changed

+286
-23
lines changed

13 files changed

+286
-23
lines changed

browser/src/Editor/NeovimEditor/WelcomeBufferLayer.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as React from "react"
88

99
import styled, { keyframes } from "styled-components"
1010

11+
import { inputManager, InputManager } from "./../../Services/InputManager"
12+
1113
import * as Oni from "oni-api"
1214

1315
import { withProps } from "./../../UI/components/common"
@@ -167,20 +169,24 @@ export class WelcomeBufferLayer implements Oni.EditorLayer {
167169
className="enable-mouse"
168170
style={{ animation: `${entranceFull} 0.25s ease-in 0.1s forwards` }}
169171
>
170-
<WelcomeView />
172+
<WelcomeView inputManager={inputManager} />
171173
</WelcomeWrapper>
172174
)
173175
}
174176
}
175177

178+
export interface WelcomeViewProps {
179+
inputManager: InputManager
180+
}
181+
176182
export interface WelcomeViewState {
177183
version: string
178184
}
179185

180186
import { getMetadata } from "./../../Services/Metadata"
181187

182-
export class WelcomeView extends React.PureComponent<{}, WelcomeViewState> {
183-
constructor(props: any) {
188+
export class WelcomeView extends React.PureComponent<WelcomeViewProps, WelcomeViewState> {
189+
constructor(props: WelcomeViewProps) {
184190
super(props)
185191

186192
this.state = {
@@ -282,7 +288,9 @@ export class WelcomeView extends React.PureComponent<{}, WelcomeViewState> {
282288
/>
283289
<WelcomeButton
284290
title="Command Palette"
285-
description="Control + Shift + P"
291+
description={
292+
this.props.inputManager.getBoundKeys("commands.show")[0]
293+
}
286294
command="oni.configuration.open"
287295
/>
288296
<WelcomeButton

browser/src/Input/KeyParser.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* KeyParser.ts
3+
*
4+
* Simple parsing logic to take vim key bindings / chords,
5+
* and return a normalized object.
6+
*/
7+
8+
export interface IKey {
9+
character: string
10+
shift: boolean
11+
alt: boolean
12+
control: boolean
13+
meta: boolean
14+
}
15+
16+
export interface IKeyChord {
17+
chord: IKey[]
18+
}
19+
20+
export const parseKeysFromVimString = (keys: string): IKeyChord => {
21+
const chord: IKey[] = []
22+
23+
let idx = 0
24+
25+
while (idx < keys.length) {
26+
if (keys[idx] !== "<") {
27+
chord.push(parseKey(keys[idx]))
28+
} else {
29+
const endIndex = getNextCharacter(keys, idx + 1)
30+
// Malformed if there isn't a corresponding '>'
31+
if (endIndex === -1) {
32+
return { chord }
33+
}
34+
35+
const keyContents = keys.substring(idx + 1, endIndex)
36+
chord.push(parseKey(keyContents))
37+
idx = endIndex + 1
38+
}
39+
40+
idx++
41+
}
42+
43+
return {
44+
chord,
45+
}
46+
}
47+
48+
const getNextCharacter = (str: string, startIndex: number): number => {
49+
let i = startIndex
50+
while (i < str.length) {
51+
if (str[i] === ">") {
52+
return i
53+
}
54+
i++
55+
}
56+
57+
return -1
58+
}
59+
60+
export const parseKey = (key: string): IKey => {
61+
if (key.indexOf("-") === -1) {
62+
return {
63+
character: key,
64+
shift: false,
65+
alt: false,
66+
control: false,
67+
meta: false,
68+
}
69+
}
70+
71+
const hasControl = key.indexOf("c-") >= 0 || key.indexOf("C-") >= 0
72+
const hasShift = key.indexOf("s-") >= 0 || key.indexOf("S-") >= 0
73+
const hasAlt = key.indexOf("a-") >= 0 || key.indexOf("A-") >= 0
74+
const hasMeta = key.indexOf("m-") >= 0 || key.indexOf("M-") >= 0
75+
76+
const lastIndexoFHyphen = key.lastIndexOf("-")
77+
const finalKey = key.substring(lastIndexoFHyphen + 1, key.length)
78+
79+
return {
80+
character: finalKey,
81+
shift: hasShift,
82+
alt: hasAlt,
83+
control: hasControl,
84+
meta: hasMeta,
85+
}
86+
}

browser/src/Services/InputManager.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type ActionOrCommand = string | ActionFunction
88

99
export type FilterFunction = () => boolean
1010

11+
import { IKeyChord, parseKeysFromVimString } from "./../Input/KeyParser"
12+
1113
export interface KeyBinding {
1214
action: ActionOrCommand
1315
filter?: FilterFunction
@@ -69,6 +71,25 @@ export class InputManager implements Oni.InputManager {
6971
return !!this._boundKeys[keyChord]
7072
}
7173

74+
// Returns an array of keys bound to a command
75+
public getBoundKeys(command: string): string[] {
76+
return Object.keys(this._boundKeys).reduce(
77+
(prev: string[], currentValue: string) => {
78+
const bindings = this._boundKeys[currentValue]
79+
if (bindings.find(b => b.action === command)) {
80+
return [...prev, currentValue]
81+
} else {
82+
return prev
83+
}
84+
},
85+
[] as string[],
86+
)
87+
}
88+
89+
public parseKeys(keys: string): IKeyChord {
90+
return parseKeysFromVimString(keys)
91+
}
92+
7293
/**
7394
* Internal Methods
7495
*/

browser/src/Services/Menu/MenuComponent.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as take from "lodash/take"
66
import * as Oni from "oni-api"
77

88
import { HighlightTextByIndex } from "./../../UI/components/HighlightText"
9-
import { Visible } from "./../../UI/components/Visible"
9+
// import { Visible } from "./../../UI/components/Visible"
1010
import { Icon, IconSize } from "./../../UI/Icon"
1111

1212
import { focusManager } from "./../FocusManager"
@@ -156,6 +156,7 @@ export interface IMenuItemProps {
156156
detail: string
157157
detailHighlights: number[]
158158
pinned: boolean
159+
additionalComponent?: JSX.Element
159160
onClick: () => void
160161
}
161162

@@ -173,7 +174,6 @@ export class MenuItem extends React.PureComponent<IMenuItemProps, {}> {
173174
) : (
174175
this.props.icon
175176
)
176-
177177
return (
178178
<div className={className} onClick={() => this.props.onClick()}>
179179
{icon}
@@ -189,9 +189,7 @@ export class MenuItem extends React.PureComponent<IMenuItemProps, {}> {
189189
highlightIndices={this.props.detailHighlights}
190190
highlightClassName={"highlight"}
191191
/>
192-
<Visible visible={this.props.pinned}>
193-
<Icon name="clock-o" />
194-
</Visible>
192+
{this.props.additionalComponent}
195193
</div>
196194
)
197195
}

browser/src/Services/Menu/MenuFilter.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as Fuse from "fuse.js"
88
import * as sortBy from "lodash/sortBy"
99

10-
import * as Oni from "oni-api"
10+
// import * as Oni from "oni-api"
1111

1212
import { configuration } from "./../../Services/Configuration"
1313

@@ -38,10 +38,7 @@ export const shouldFilterbeCaseSensitive = (searchString: string): boolean => {
3838
}
3939
}
4040

41-
export const fuseFilter = (
42-
options: Oni.Menu.MenuOption[],
43-
searchString: string,
44-
): IMenuOptionWithHighlights[] => {
41+
export const fuseFilter = (options: any[], searchString: string): IMenuOptionWithHighlights[] => {
4542
if (!searchString) {
4643
const opt = options.map(o => {
4744
return {
@@ -53,6 +50,7 @@ export const fuseFilter = (
5350
metadata: o.metadata,
5451
detailHighlights: [],
5552
labelHighlights: [],
53+
additionalComponent: o.additionalComponent,
5654
}
5755
})
5856

@@ -123,6 +121,7 @@ export const fuseFilter = (
123121
metadata: f.item.metadata,
124122
labelHighlights: convertArrayOfPairsToIndices(labelHighlights),
125123
detailHighlights: convertArrayOfPairsToIndices(detailHighlights),
124+
additionalComponent: f.item.additionalComponent,
126125
}
127126
})
128127

browser/src/Services/Notifications/Notifications.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@ import { createStore, INotificationsState } from "./NotificationStore"
1414
import { getView } from "./NotificationsView"
1515

1616
export class Notifications {
17-
1817
private _id: number = 0
1918
private _overlay: Overlay
2019
private _store: Store<INotificationsState>
2120

22-
constructor(
23-
private _overlayManager: OverlayManager,
24-
) {
21+
constructor(private _overlayManager: OverlayManager) {
2522
this._store = createStore()
2623

2724
this._overlay = this._overlayManager.createItem()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* PinnedIconView.tsx
3+
*
4+
* Shows the pinned icon for recently navigated items in quick open
5+
*/
6+
7+
import * as React from "react"
8+
9+
import { Visible } from "./../../UI/components/Visible"
10+
import { Icon } from "./../../UI/Icon"
11+
12+
export const render = (props: { pinned: boolean }) => {
13+
return (
14+
<Visible visible={props.pinned}>
15+
<Icon name="clock-o" />
16+
</Visible>
17+
)
18+
}

browser/src/Services/QuickOpen/QuickOpen.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { editorManager } from "./../EditorManager"
1818
import { fuseFilter, Menu, MenuManager } from "./../Menu"
1919

2020
import { FinderProcess } from "./FinderProcess"
21+
import { render as renderPinnedIcon } from "./PinnedIconView"
2122
import { QuickOpenItem, QuickOpenType } from "./QuickOpenItem"
2223
import { regexFilter } from "./RegExFilter"
2324
import * as RipGrep from "./RipGrep"
@@ -248,19 +249,20 @@ export class QuickOpen {
248249
process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"] === process.cwd()
249250
)
250251
}
251-
252252
// Show menu based on items given
253253
private _setItemsFromQuickOpenItems(items: QuickOpenItem[]): void {
254254
const options = items.map(qitem => {
255255
const f = qitem.item.trim()
256256
const file = path.basename(f)
257257
const folder = path.dirname(f)
258+
const pinned = this._seenItems.indexOf(f) >= 0
258259

259260
return {
260261
icon: getFileIcon(file) as any,
261262
label: file,
262263
detail: folder,
263-
pinned: this._seenItems.indexOf(f) >= 0,
264+
pinned,
265+
additionalComponent: renderPinnedIcon({ pinned }),
264266
}
265267
})
266268

browser/src/Services/Tasks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import * as Oni from "oni-api"
1717

1818
import { Menu, MenuManager } from "./../Services/Menu"
1919

20+
import { render as renderKeyBindingInfo } from "./../UI/components/KeyBindingInfo"
21+
2022
export interface ITask {
2123
name: string
2224
detail: string
@@ -50,9 +52,9 @@ export class Tasks {
5052
.filter(t => t.name || t.detail)
5153
.map(f => {
5254
return {
53-
icon: "tasks",
5455
label: f.name,
5556
detail: f.detail,
57+
additionalComponent: renderKeyBindingInfo({ command: f.command }),
5658
}
5759
})
5860

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* KeyBindingInfo.tsx
3+
*
4+
* Helper component to show a key binding, based on a command
5+
*/
6+
7+
import styled from "styled-components"
8+
9+
import * as React from "react"
10+
11+
import { inputManager } from "./../../Services/InputManager"
12+
13+
export interface IKeyBindingInfoProps {
14+
command: string
15+
}
16+
17+
const KeyWrapper = styled.span`
18+
color: ${props => props.theme["highlight.mode.normal.background"]};
19+
font-size: 0.9em;
20+
`
21+
22+
export class KeyBindingInfo extends React.PureComponent<IKeyBindingInfoProps, {}> {
23+
public render(): JSX.Element {
24+
if (!inputManager) {
25+
return null
26+
}
27+
28+
const boundKeys = inputManager.getBoundKeys(this.props.command)
29+
30+
if (!boundKeys || !boundKeys.length) {
31+
return null
32+
}
33+
34+
const parsedKeys = inputManager.parseKeys(boundKeys[0])
35+
36+
if (!parsedKeys || !parsedKeys.chord || !parsedKeys.chord.length) {
37+
return null
38+
}
39+
40+
const firstChord = parsedKeys.chord[0]
41+
42+
const elems: JSX.Element[] = []
43+
44+
if (firstChord.meta) {
45+
elems.push(<KeyWrapper>{"meta"}</KeyWrapper>)
46+
elems.push(<KeyWrapper>{"+"}</KeyWrapper>)
47+
}
48+
49+
if (firstChord.control) {
50+
elems.push(<KeyWrapper>{"control"}</KeyWrapper>)
51+
elems.push(<KeyWrapper>{"+"}</KeyWrapper>)
52+
}
53+
54+
if (firstChord.alt) {
55+
elems.push(<KeyWrapper>{"alt"}</KeyWrapper>)
56+
elems.push(<KeyWrapper>{"+"}</KeyWrapper>)
57+
}
58+
59+
if (firstChord.shift) {
60+
elems.push(<KeyWrapper>{"shift"}</KeyWrapper>)
61+
elems.push(<KeyWrapper>{"+"}</KeyWrapper>)
62+
}
63+
64+
elems.push(<KeyWrapper>{firstChord.character}</KeyWrapper>)
65+
66+
return <span>{elems}</span>
67+
}
68+
}
69+
70+
export const render = (props: IKeyBindingInfoProps) => <KeyBindingInfo {...props} />

0 commit comments

Comments
 (0)