Feat/text search (#3075)

* Added persistent text box search

* Toggle search box now working

* Restyled search box

* Linter and bug squashing

* Added multi OS hotkey support

* PR changes as requested

* Added ability to use escape button to close search field

* Woops forgot key mapping on non mac platforms

* fixed bug where escape would open up search window

* Removal of unused vars that died in conflict
This commit is contained in:
Brandon Lee Dring 2019-09-23 10:37:22 -07:00 committed by Benjamin Staneck
parent 67a1fc4dbb
commit 5bc8e0b1e8
14 changed files with 187 additions and 3 deletions

View file

@ -101,6 +101,12 @@ const commands = {
'editor:break': focusedWindow => { 'editor:break': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session break req'); focusedWindow && focusedWindow.rpc.emit('session break req');
}, },
'editor:search': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session search');
},
'editor:search-close': focusedWindow => {
focusedWindow && focusedWindow.rpc.emit('session search close');
},
'cli:install': () => { 'cli:install': () => {
installCLI(true); installCLI(true);
}, },

View file

@ -39,6 +39,8 @@
"editor:copy": "command+c", "editor:copy": "command+c",
"editor:paste": "command+v", "editor:paste": "command+v",
"editor:selectAll": "command+a", "editor:selectAll": "command+a",
"editor:search": "command+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "alt+left", "editor:movePreviousWord": "alt+left",
"editor:moveNextWord": "alt+right", "editor:moveNextWord": "alt+right",
"editor:moveBeginningLine": "command+left", "editor:moveBeginningLine": "command+left",

View file

@ -36,6 +36,8 @@
"editor:copy": "ctrl+shift+c", "editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v", "editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a", "editor:selectAll": "ctrl+shift+a",
"editor:search": "ctrl+shift+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "ctrl+left", "editor:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right", "editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "home", "editor:moveBeginningLine": "home",

View file

@ -40,6 +40,8 @@
"editor:copy": "ctrl+shift+c", "editor:copy": "ctrl+shift+c",
"editor:paste": "ctrl+shift+v", "editor:paste": "ctrl+shift+v",
"editor:selectAll": "ctrl+shift+a", "editor:selectAll": "ctrl+shift+a",
"editor:search": "ctrl+shift+f",
"editor:search-close": "esc",
"editor:movePreviousWord": "ctrl+left", "editor:movePreviousWord": "ctrl+left",
"editor:moveNextWord": "ctrl+right", "editor:moveNextWord": "ctrl+right",
"editor:moveBeginningLine": "Home", "editor:moveBeginningLine": "Home",

View file

@ -113,6 +113,13 @@ module.exports = (commandKeys, execCommand) => {
click(item, focusedWindow) { click(item, focusedWindow) {
execCommand('editor:clearBuffer', focusedWindow); execCommand('editor:clearBuffer', focusedWindow);
} }
},
{
label: 'Search',
accelerator: commandKeys['editor:search'],
click(item, focusedWindow) {
execCommand('editor:search', focusedWindow);
}
} }
]; ];

View file

@ -12,7 +12,9 @@ import {
SESSION_SET_ACTIVE, SESSION_SET_ACTIVE,
SESSION_CLEAR_ACTIVE, SESSION_CLEAR_ACTIVE,
SESSION_USER_DATA, SESSION_USER_DATA,
SESSION_SET_XTERM_TITLE SESSION_SET_XTERM_TITLE,
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions'; } from '../constants/sessions';
export function addSession({uid, shell, pid, cols, rows, splitDirection}) { export function addSession({uid, shell, pid, cols, rows, splitDirection}) {
@ -131,6 +133,26 @@ export function resizeSession(uid, cols, rows) {
}; };
} }
export function onSearch(uid) {
return (dispatch, getState) => {
const targetUid = uid || getState().sessions.activeUid;
dispatch({
type: SESSION_SEARCH,
uid: targetUid
});
};
}
export function closeSearch(uid) {
return (dispatch, getState) => {
const targetUid = uid || getState().sessions.activeUid;
dispatch({
type: SESSION_SEARCH_CLOSE,
uid: targetUid
});
};
}
export function sendSessionData(uid, data, escaped) { export function sendSessionData(uid, data, escaped) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({

View file

@ -0,0 +1,78 @@
import React from 'react';
const searchBoxStyling = {
float: 'right',
height: '28px',
backgroundColor: 'white',
position: 'absolute',
right: '10px',
top: '25px',
width: '224px',
zIndex: '9999'
};
const enterKey = 13;
export default class SearchBox extends React.PureComponent {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.searchTerm = '';
}
handleChange(event) {
this.searchTerm = event.target.value;
if (event.keyCode === enterKey) {
this.props.search(event.target.value);
}
}
render() {
return (
<div style={searchBoxStyling}>
<input type="text" className="search-box" onKeyUp={this.handleChange} ref={input => input && input.focus()} />
<span className="search-button" onClick={() => this.props.prev(this.searchTerm)}>
{' '}
&#x2190;{' '}
</span>
<span className="search-button" onClick={() => this.props.next(this.searchTerm)}>
{' '}
&#x2192;{' '}
</span>
<span className="search-button" onClick={() => this.props.close()}>
{' '}
x{' '}
</span>
<style jsx>
{`
.search-box {
font-size: 18px;
padding: 6px;
width: 145px;
border: none;
}
.search-box:focus {
outline: none;
}
.search-button {
background-color: #ffffff;
color: black;
padding: 7px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
cursor: pointer;
}
.search-button:hover {
background-color: #e7e7e7;
}
`}
</style>
</div>
);
}
}

View file

@ -77,6 +77,7 @@ class TermGroup_ extends React.PureComponent {
padding: this.props.padding, padding: this.props.padding,
url: session.url, url: session.url,
cleared: session.cleared, cleared: session.cleared,
search: session.search,
cols: session.cols, cols: session.cols,
rows: session.rows, rows: session.rows,
copyOnSelect: this.props.copyOnSelect, copyOnSelect: this.props.copyOnSelect,
@ -86,6 +87,8 @@ class TermGroup_ extends React.PureComponent {
onResize: this.bind(this.props.onResize, null, uid), onResize: this.bind(this.props.onResize, null, uid),
onTitle: this.bind(this.props.onTitle, null, uid), onTitle: this.bind(this.props.onTitle, null, uid),
onData: this.bind(this.props.onData, null, uid), onData: this.bind(this.props.onData, null, uid),
onURLAbort: this.bind(this.props.onURLAbort, null, uid),
toggleSearch: this.bind(this.props.toggleSearch, null, uid),
onContextMenu: this.bind(this.props.onContextMenu, null, uid), onContextMenu: this.bind(this.props.onContextMenu, null, uid),
borderColor: this.props.borderColor, borderColor: this.props.borderColor,
selectionColor: this.props.selectionColor, selectionColor: this.props.selectionColor,

View file

@ -3,15 +3,18 @@ import React from 'react';
import {Terminal} from 'xterm'; import {Terminal} from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit'; import * as fit from 'xterm/lib/addons/fit/fit';
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks'; import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
import * as search from 'xterm/lib/addons/search';
import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat'; import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat';
import {clipboard} from 'electron'; import {clipboard} from 'electron';
import * as Color from 'color'; import * as Color from 'color';
import terms from '../terms'; import terms from '../terms';
import processClipboard from '../utils/paste'; import processClipboard from '../utils/paste';
import SearchBox from './searchBox';
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
Terminal.applyAddon(webLinks); Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat); Terminal.applyAddon(winptyCompat);
Terminal.applyAddon(search);
// map old hterm constants to xterm.js // map old hterm constants to xterm.js
const CURSOR_STYLES = { const CURSOR_STYLES = {
@ -108,6 +111,10 @@ export default class Term extends React.PureComponent {
this.onWindowPaste = this.onWindowPaste.bind(this); this.onWindowPaste = this.onWindowPaste.bind(this);
this.onTermWrapperRef = this.onTermWrapperRef.bind(this); this.onTermWrapperRef = this.onTermWrapperRef.bind(this);
this.onMouseUp = this.onMouseUp.bind(this); this.onMouseUp = this.onMouseUp.bind(this);
this.search = this.search.bind(this);
this.searchNext = this.searchNext.bind(this);
this.searchPrevious = this.searchPrevious.bind(this);
this.closeSearchBox = this.closeSearchBox.bind(this);
this.termOptions = {}; this.termOptions = {};
this.disposableListeners = []; this.disposableListeners = [];
} }
@ -243,6 +250,22 @@ export default class Term extends React.PureComponent {
this.term.reset(); this.term.reset();
} }
search(searchTerm) {
this.term.findNext(searchTerm);
}
searchNext(searchTerm) {
this.term.findNext(searchTerm);
}
searchPrevious(searchTerm) {
this.term.findPrevious(searchTerm);
}
closeSearchBox() {
this.props.toggleSearch();
}
resize(cols, rows) { resize(cols, rows) {
this.term.resize(cols, rows); this.term.resize(cols, rows);
} }
@ -269,6 +292,10 @@ export default class Term extends React.PureComponent {
} }
const nextTermOptions = getTermOptions(nextProps); const nextTermOptions = getTermOptions(nextProps);
if (!this.props.search && nextProps.search) {
this.search();
}
// Update only options that have changed. // Update only options that have changed.
Object.keys(nextTermOptions) Object.keys(nextTermOptions)
.filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option]) .filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
@ -358,6 +385,16 @@ export default class Term extends React.PureComponent {
{this.props.customChildrenBefore} {this.props.customChildrenBefore}
<div ref={this.onTermWrapperRef} className="term_fit term_wrapper" /> <div ref={this.onTermWrapperRef} className="term_fit term_wrapper" />
{this.props.customChildren} {this.props.customChildren}
{this.props.search ? (
<SearchBox
search={this.search}
next={this.searchNext}
prev={this.searchPrevious}
close={this.closeSearchBox}
/>
) : (
''
)}
<style jsx global>{` <style jsx global>{`
.term_fit { .term_fit {

View file

@ -120,6 +120,8 @@ export default class Terms extends React.Component {
onResize: this.props.onResize, onResize: this.props.onResize,
onTitle: this.props.onTitle, onTitle: this.props.onTitle,
onData: this.props.onData, onData: this.props.onData,
toggleSearch: this.props.toggleSearch,
onURLAbort: this.props.onURLAbort,
onContextMenu: this.props.onContextMenu, onContextMenu: this.props.onContextMenu,
quickEdit: this.props.quickEdit, quickEdit: this.props.quickEdit,
webGLRenderer: this.props.webGLRenderer, webGLRenderer: this.props.webGLRenderer,

View file

@ -12,3 +12,5 @@ export const SESSION_URL_SET = 'SESSION_URL_SET';
export const SESSION_URL_UNSET = 'SESSION_URL_UNSET'; export const SESSION_URL_UNSET = 'SESSION_URL_UNSET';
export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE'; export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE';
export const SESSION_SET_CWD = 'SESSION_SET_CWD'; export const SESSION_SET_CWD = 'SESSION_SET_CWD';
export const SESSION_SEARCH = 'SESSION_SEARCH';
export const SESSION_SEARCH_CLOSE = 'SESSION_SEARCH_CLOSE';

View file

@ -1,6 +1,7 @@
import Terms from '../components/terms'; import Terms from '../components/terms';
import {connect} from '../utils/plugins'; import {connect} from '../utils/plugins';
import {resizeSession, sendSessionData, setSessionXtermTitle, setActiveSession} from '../actions/sessions'; import {resizeSession, sendSessionData, setSessionXtermTitle, setActiveSession, onSearch} from '../actions/sessions';
import {openContextMenu} from '../actions/ui'; import {openContextMenu} from '../actions/ui';
import getRootGroups from '../selectors'; import getRootGroups from '../selectors';
@ -61,6 +62,9 @@ const TermsContainer = connect(
onActive(uid) { onActive(uid) {
dispatch(setActiveSession(uid)); dispatch(setActiveSession(uid));
}, },
toggleSearch(uid) {
dispatch(onSearch(uid));
},
onContextMenu(uid, selection) { onContextMenu(uid, selection) {
dispatch(setActiveSession(uid)); dispatch(setActiveSession(uid));

View file

@ -106,6 +106,14 @@ rpc.on('session break req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x03')); store_.dispatch(sessionActions.sendSessionData(null, '\x03'));
}); });
rpc.on('session search', () => {
store_.dispatch(sessionActions.onSearch());
});
rpc.on('session search close', () => {
store_.dispatch(sessionActions.closeSearch());
});
rpc.on('termgroup add req', () => { rpc.on('termgroup add req', () => {
store_.dispatch(termGroupActions.requestTermGroup()); store_.dispatch(termGroupActions.requestTermGroup());
}); });

View file

@ -9,7 +9,9 @@ import {
SESSION_CLEAR_ACTIVE, SESSION_CLEAR_ACTIVE,
SESSION_RESIZE, SESSION_RESIZE,
SESSION_SET_XTERM_TITLE, SESSION_SET_XTERM_TITLE,
SESSION_SET_CWD SESSION_SET_CWD,
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions'; } from '../constants/sessions';
const initialState = Immutable({ const initialState = Immutable({
@ -25,6 +27,7 @@ function Session(obj) {
rows: null, rows: null,
url: null, url: null,
cleared: false, cleared: false,
search: false,
shell: '', shell: '',
pid: null pid: null
}).merge(obj); }).merge(obj);
@ -47,6 +50,12 @@ const reducer = (state = initialState, action) => {
case SESSION_SET_ACTIVE: case SESSION_SET_ACTIVE:
return state.set('activeUid', action.uid); return state.set('activeUid', action.uid);
case SESSION_SEARCH:
return state.setIn(['sessions', action.uid, 'search'], !state.sessions[action.uid].search);
case SESSION_SEARCH_CLOSE:
return state.setIn(['sessions', action.uid, 'search'], false);
case SESSION_CLEAR_ACTIVE: case SESSION_CLEAR_ACTIVE:
return state.merge( return state.merge(
{ {