mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
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:
parent
67a1fc4dbb
commit
5bc8e0b1e8
14 changed files with 187 additions and 3 deletions
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
78
lib/components/searchBox.js
Normal file
78
lib/components/searchBox.js
Normal 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)}>
|
||||||
|
{' '}
|
||||||
|
←{' '}
|
||||||
|
</span>
|
||||||
|
<span className="search-button" onClick={() => this.props.next(this.searchTerm)}>
|
||||||
|
{' '}
|
||||||
|
→{' '}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue