Skip to content

Commit 3075474

Browse files
committed
History browsing + reverse search
1 parent bb595af commit 3075474

File tree

2 files changed

+158
-5
lines changed

2 files changed

+158
-5
lines changed

example/src/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export default class App extends Component {
102102
})}
103103
/>
104104
<table>
105+
<tbody>
105106
<tr>
106107
<td><code>echo ...args</code></td>
107108
<td>Echo</td>
@@ -110,6 +111,7 @@ export default class App extends Component {
110111
<td><code>sleep `ms`</code></td>
111112
<td>Sleeps for a number of milliseconds</td>
112113
</tr>
114+
</tbody>
113115
</table>
114116
</div>
115117
)

src/index.tsx

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ type State = {
2828
output: Array<string>,
2929
commandInProgress: boolean,
3030
input: string,
31+
history?: Array<string>,
32+
historyPosition: number,
33+
reverseSearchString?: string,
34+
reverseSearchPosition: number,
3135
}
3236

3337
export default class ReactConsole extends React.Component<Props, State> {
3438

3539
inputRef: any = null;
3640
wrapperRef: any = null;
41+
reverseStringRef: any = null;
3742

3843
static defaultProps = {
3944
prompt: '$',
@@ -48,15 +53,29 @@ export default class ReactConsole extends React.Component<Props, State> {
4853
input: '',
4954
output: [],
5055
commandInProgress: false,
56+
history: [
57+
"hello1",
58+
"hello2",
59+
"ahojky",
60+
"ahoj",
61+
"hello3",
62+
"world",
63+
],
64+
reverseSearchString: undefined,
65+
historyPosition: Infinity,
66+
reverseSearchPosition: Infinity,
5167
};
5268

5369
componentDidMount() {
54-
const {welcomeMessage} = this.props
70+
const {welcomeMessage} = this.props;
5571
if (welcomeMessage) {
5672
this.setState({
5773
output: [welcomeMessage],
5874
})
5975
}
76+
this.setState({
77+
historyPosition: this.state.history.length,
78+
})
6079
}
6180

6281
clear = () => {
@@ -69,26 +88,44 @@ export default class ReactConsole extends React.Component<Props, State> {
6988
})
7089
};
7190

72-
onSubmit = async (e: any) => {
91+
/**
92+
* Get filtered history entries based on reverse search string
93+
*/
94+
getReverseHistory = (): Array<boolean> => {
95+
const {reverseSearchString} = this.state;
96+
return this.state.history.map(entry => (reverseSearchString === undefined || reverseSearchString === '') ?
97+
// @ts-ignore
98+
false : entry.includes(reverseSearchString))
99+
};
100+
101+
// TODO rename
102+
getLog = () => {
73103
const {prompt} = this.props;
104+
const inputString: string = this.state.input;
105+
return `${prompt}\xa0${inputString}`;
106+
};
107+
108+
onSubmit = async (e: any) => {
74109
e.preventDefault();
75110

76111
const inputString: string = this.state.input
77112
if (inputString === null) {
78113
return
79114
}
80115

81-
const log = `${prompt}\xa0${inputString}`;
116+
const log = this.getLog();
82117

83118
if (inputString === '') {
84119
this.setState({
85120
output: [...this.state.output, log],
86121
input: '',
87122
});
88-
this.scrollToBottom()
123+
this.scrollToBottom();
89124
return
90125
}
91126

127+
this.addHistoryEntry(inputString);
128+
92129
const [cmd, ...args] = inputString.split(" ");
93130

94131
if (cmd === 'clear') {
@@ -162,11 +199,12 @@ export default class ReactConsole extends React.Component<Props, State> {
162199
className={promptClass}
163200
>{prompt}&nbsp;</span>
164201
<input
165-
disabled={this.state.commandInProgress}
202+
disabled={this.state.commandInProgress || this.isReverseSearchOn()}
166203
ref={ref => this.inputRef = ref}
167204
autoFocus={autoFocus}
168205
value={this.state.input}
169206
onChange={this.onInputChange}
207+
onKeyDown={this.onKeyDown}
170208
autoComplete={'off'}
171209
spellCheck={false}
172210
autoCapitalize={'false'}
@@ -176,21 +214,134 @@ export default class ReactConsole extends React.Component<Props, State> {
176214
/>
177215
</div>
178216
</form>
217+
{this.isReverseSearchOn() && <form onSubmit={this.onReverseSearchSubmit}>bck-i-search: <input
218+
value={this.state.reverseSearchString}
219+
ref={ref => this.reverseStringRef = ref}
220+
onKeyDown={this.onReverseKeyDown}
221+
className={classnames([styles.input, inputClassName])}
222+
onChange={this.onReverseStringInputChange}
223+
/>
224+
</form>}
179225
</div>
180226
)
181227
}
182228

229+
onReverseStringInputChange = (e: any) => {
230+
this.setState({
231+
reverseSearchString: e.target.value,
232+
}, () => {
233+
const history: Array<boolean> = this.getReverseHistory();
234+
const historyIndex: number = history.lastIndexOf(true);
235+
this.executeNextReverseSearch(historyIndex)
236+
})
237+
};
238+
239+
nextReverseSearch = () => {
240+
const history: Array<boolean> = this.getReverseHistory();
241+
const endOffset = Math.max(0, this.state.reverseSearchPosition - 1); // so that we don't go from the end again
242+
const historyIndex: number = history.lastIndexOf(true, endOffset);
243+
this.executeNextReverseSearch(historyIndex)
244+
};
245+
246+
private executeNextReverseSearch = (historyIndex: number) => {
247+
this.setState({
248+
reverseSearchPosition: historyIndex,
249+
});
250+
if (historyIndex !== -1) {
251+
this.setPreviewPosition(historyIndex)
252+
}
253+
if (this.state.reverseSearchString === '') {
254+
this.setPreviewPosition(Infinity);
255+
}
256+
};
257+
258+
onReverseSearch = () => {
259+
// we enabled reverse search
260+
this.setState({
261+
reverseSearchString: '',
262+
}, () => {
263+
this.reverseStringRef.focus()
264+
})
265+
};
266+
267+
onReverseSearchSubmit = (e: any) => {
268+
e.preventDefault();
269+
this.disableReverseSearch();
270+
};
271+
183272
onInputChange = (e: any) => {
184273
this.setState({
185274
input: e.target.value,
186275
})
187276
};
188277

278+
isReverseSearchOn = (): boolean => this.state.reverseSearchString !== undefined;
279+
280+
disableReverseSearch = (reset: boolean = false) => {
281+
this.setState({
282+
reverseSearchString: undefined,
283+
});
284+
if (reset) {
285+
this.setState({
286+
input: '',
287+
})
288+
}
289+
setTimeout(() => {
290+
this.inputRef.focus();
291+
});
292+
};
293+
294+
onReverseKeyDown = (e: any) => {
295+
if (e.which === 38 || e.which === 40) { // up or down
296+
this.disableReverseSearch()
297+
} else if (e.which === 67 && e.ctrlKey) { // ctrl + c
298+
this.disableReverseSearch(true);
299+
} else if (e.which === 82 && e.ctrlKey) { // ctrl + r
300+
this.nextReverseSearch();
301+
}
302+
};
303+
304+
setPreviewPosition = (historyPosition: number) => {
305+
this.setState({
306+
historyPosition,
307+
input: this.state.history[historyPosition] || '',
308+
});
309+
};
310+
311+
onKeyDown = (e: any) => {
312+
if (e.which === 38) { // key up
313+
const historyPosition = Math.max(0, this.state.historyPosition - 1);
314+
this.setPreviewPosition(historyPosition);
315+
e.preventDefault()
316+
} else if (e.which === 40) {
317+
const historyPosition = Math.min(this.state.history.length, this.state.historyPosition + 1);
318+
this.setPreviewPosition(historyPosition);
319+
e.preventDefault()
320+
} else if (e.which === 82 && e.ctrlKey) { // ctrl + r
321+
console.log('reverse search mode');
322+
this.onReverseSearch()
323+
} else if (e.which === 67 && e.ctrlKey) { // ctrl + c
324+
this.setState({
325+
output: [...this.state.output, this.getLog()],
326+
input: '',
327+
});
328+
this.scrollToBottom();
329+
}
330+
};
331+
189332
focusConsole = () => {
190333
if (this.inputRef) {
191334
if (document.getSelection().isCollapsed) {
192335
this.inputRef.focus()
193336
}
194337
}
338+
};
339+
340+
private addHistoryEntry(inputString: string) {
341+
const history: Array<string> = [...this.state.history, inputString];
342+
this.setState({
343+
history,
344+
historyPosition: history.length,
345+
})
195346
}
196347
}

0 commit comments

Comments
 (0)