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 => {
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': () => {
installCLI(true);
},

View file

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

View file

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

View file

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

View file

@ -113,6 +113,13 @@ module.exports = (commandKeys, execCommand) => {
click(item, 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_CLEAR_ACTIVE,
SESSION_USER_DATA,
SESSION_SET_XTERM_TITLE
SESSION_SET_XTERM_TITLE,
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions';
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) {
return (dispatch, getState) => {
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,
url: session.url,
cleared: session.cleared,
search: session.search,
cols: session.cols,
rows: session.rows,
copyOnSelect: this.props.copyOnSelect,
@ -86,6 +87,8 @@ class TermGroup_ extends React.PureComponent {
onResize: this.bind(this.props.onResize, null, uid),
onTitle: this.bind(this.props.onTitle, 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),
borderColor: this.props.borderColor,
selectionColor: this.props.selectionColor,

View file

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

View file

@ -120,6 +120,8 @@ export default class Terms extends React.Component {
onResize: this.props.onResize,
onTitle: this.props.onTitle,
onData: this.props.onData,
toggleSearch: this.props.toggleSearch,
onURLAbort: this.props.onURLAbort,
onContextMenu: this.props.onContextMenu,
quickEdit: this.props.quickEdit,
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_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE';
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 {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 getRootGroups from '../selectors';
@ -61,6 +62,9 @@ const TermsContainer = connect(
onActive(uid) {
dispatch(setActiveSession(uid));
},
toggleSearch(uid) {
dispatch(onSearch(uid));
},
onContextMenu(uid, selection) {
dispatch(setActiveSession(uid));

View file

@ -106,6 +106,14 @@ rpc.on('session break req', () => {
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', () => {
store_.dispatch(termGroupActions.requestTermGroup());
});

View file

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