diff --git a/assets/search-icons.svg b/assets/search-icons.svg deleted file mode 100644 index 48778e75..00000000 --- a/assets/search-icons.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - left arrow - - - - - - - - right arrow - - - - - - - - cancel - - - - - - - - - diff --git a/lib/components/searchBox.tsx b/lib/components/searchBox.tsx index 1cc2fda7..9fa93b6f 100644 --- a/lib/components/searchBox.tsx +++ b/lib/components/searchBox.tsx @@ -1,76 +1,254 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {SearchBoxProps} from '../hyper'; +import {VscArrowUp} from '@react-icons/all-files/vsc/VscArrowUp'; +import {VscArrowDown} from '@react-icons/all-files/vsc/VscArrowDown'; +import {VscClose} from '@react-icons/all-files/vsc/VscClose'; +import {VscCaseSensitive} from '@react-icons/all-files/vsc/VscCaseSensitive'; +import {VscRegex} from '@react-icons/all-files/vsc/VscRegex'; +import {VscWholeWord} from '@react-icons/all-files/vsc/VscWholeWord'; +import clsx from 'clsx'; -const searchBoxStyling: React.CSSProperties = { - float: 'right', - height: '28px', - backgroundColor: 'white', - position: 'absolute', - right: '10px', - top: '0px', - width: '224px', - zIndex: 9999 +type SearchButtonColors = { + foregroundColor: string; + selectionColor: string; + backgroundColor: string; }; -const enterKey = 13; +type SearchButtonProps = React.PropsWithChildren< + { + onClick: () => void; + active: boolean; + title: string; + } & SearchButtonColors +>; -export default class SearchBox extends React.PureComponent { +const SearchButton = ({ + onClick, + active, + title, + foregroundColor, + backgroundColor, + selectionColor, + children +}: SearchButtonProps) => { + const handleKeyUp = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + onClick(); + } + }, + [onClick] + ); + + return ( +
+ {children} + +
+ ); +}; + +class SearchBox extends React.PureComponent { searchTerm: string; + input: HTMLInputElement | null = null; + searchButtonColors: SearchButtonColors; + constructor(props: SearchBoxProps) { super(props); this.searchTerm = ''; + this.searchButtonColors = { + backgroundColor: this.props.borderColor, + selectionColor: this.props.selectionColor, + foregroundColor: this.props.foregroundColor + }; } handleChange = (event: React.KeyboardEvent) => { this.searchTerm = event.currentTarget.value; - if (event.keyCode === enterKey) { - this.props.search(event.currentTarget.value); + if (event.shiftKey && event.key === 'Enter') { + this.props.prev(this.searchTerm); + } else if (event.key === 'Enter') { + this.props.next(this.searchTerm); } }; + componentDidMount(): void { + this.input?.focus(); + } + render() { + const { + caseSensitive, + wholeWord, + regex, + results, + toggleCaseSensitive, + toggleWholeWord, + toggleRegex, + next, + prev, + close, + backgroundColor, + foregroundColor, + borderColor, + selectionColor, + font + } = this.props; + return ( -
- input?.focus()} /> - this.props.prev(this.searchTerm)}> - - - this.props.next(this.searchTerm)}> - - - this.props.close()}> - - +
+
+ { + this.input = input; + }} + placeholder="Search" + > + + + + + + + + + + + + +
+ + + {results === undefined + ? '' + : results.resultCount === 0 + ? 'No results' + : `${results.resultIndex + 1} of ${results.resultCount}`} + + +
+ prev(this.searchTerm)} + active={false} + title="Previous Match" + {...this.searchButtonColors} + > + + + + next(this.searchTerm)} + active={false} + title="Next Match" + {...this.searchButtonColors} + > + + + + close()} active={false} title="Close" {...this.searchButtonColors}> + + +
+ @@ -78,3 +256,5 @@ export default class SearchBox extends React.PureComponent { ); } } + +export default SearchBox; diff --git a/lib/components/style-sheet.tsx b/lib/components/style-sheet.tsx index a20af4c4..2fb75e28 100644 --- a/lib/components/style-sheet.tsx +++ b/lib/components/style-sheet.tsx @@ -137,6 +137,15 @@ export default class StyleSheet extends React.PureComponent { overflow: hidden; } + .xterm .xterm-decoration-overview-ruler { + position: absolute; + z-index: 10; + right: 0px; + top: unset; + left: unset; + pointer-events: none; + } + ::-webkit-scrollbar { width: 5px; } diff --git a/lib/components/term.tsx b/lib/components/term.tsx index 914a8c9e..524aaabf 100644 --- a/lib/components/term.tsx +++ b/lib/components/term.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Terminal, ITerminalOptions, IDisposable} from 'xterm'; import {FitAddon} from 'xterm-addon-fit'; import {WebLinksAddon} from 'xterm-addon-web-links'; -import {SearchAddon} from 'xterm-addon-search'; +import {SearchAddon, ISearchDecorationOptions} from 'xterm-addon-search'; import {WebglAddon} from 'xterm-addon-webgl'; import {LigaturesAddon} from 'xterm-addon-ligatures'; import {Unicode11Addon} from 'xterm-addon-unicode11'; @@ -10,9 +10,12 @@ import {clipboard, shell} from 'electron'; import Color from 'color'; import terms from '../terms'; import processClipboard from '../utils/paste'; -import SearchBox from './searchBox'; +import _SearchBox from './searchBox'; import {TermProps} from '../hyper'; import {ObjectTypedKeys} from '../utils/object'; +import {decorate} from '../utils/plugins'; + +const SearchBox = decorate(_SearchBox, 'SearchBox'); const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].includes(navigator.platform); @@ -78,11 +81,27 @@ const getTermOptions = (props: TermProps): ITerminalOptions => { brightCyan: props.colors.lightCyan, brightWhite: props.colors.lightWhite }, - screenReaderMode: props.screenReaderMode + screenReaderMode: props.screenReaderMode, + overviewRulerWidth: 20 }; }; -export default class Term extends React.PureComponent { +export default class Term extends React.PureComponent< + TermProps, + { + searchOptions: { + caseSensitive: boolean; + wholeWord: boolean; + regex: boolean; + }; + searchResults: + | { + resultIndex: number; + resultCount: number; + } + | undefined; + } +> { termRef: HTMLElement | null; termWrapperRef: HTMLElement | null; termOptions: ITerminalOptions; @@ -94,6 +113,16 @@ export default class Term extends React.PureComponent { term!: Terminal; resizeObserver!: ResizeObserver; resizeTimeout!: NodeJS.Timeout; + searchDecorations: ISearchDecorationOptions; + state = { + searchOptions: { + caseSensitive: false, + wholeWord: false, + regex: false + }, + searchResults: undefined + }; + constructor(props: TermProps) { super(props); props.ref_(props.uid, this); @@ -104,6 +133,11 @@ export default class Term extends React.PureComponent { this.termDefaultBellSound = null; this.fitAddon = new FitAddon(); this.searchAddon = new SearchAddon(); + this.searchDecorations = { + activeMatchColorOverviewRuler: Color(this.props.cursorColor).hex(), + matchOverviewRuler: Color(this.props.borderColor).hex(), + activeMatchBackground: Color(this.props.cursorColor).hex() + }; } // The main process shows this in the About dialog @@ -242,6 +276,15 @@ export default class Term extends React.PureComponent { ); } + this.disposableListeners.push( + this.searchAddon.onDidChangeResults((results) => { + this.setState((state) => ({ + ...state, + searchResults: results + })); + }) + ); + window.addEventListener('paste', this.onWindowPaste, { capture: true }); @@ -302,20 +345,28 @@ export default class Term extends React.PureComponent { this.term.reset(); } - search = (searchTerm = '') => { - this.searchAddon.findNext(searchTerm); - }; - searchNext = (searchTerm: string) => { - this.searchAddon.findNext(searchTerm); + this.searchAddon.findNext(searchTerm, { + ...this.state.searchOptions, + decorations: this.searchDecorations + }); }; searchPrevious = (searchTerm: string) => { - this.searchAddon.findPrevious(searchTerm); + this.searchAddon.findPrevious(searchTerm, { + ...this.state.searchOptions, + decorations: this.searchDecorations + }); }; closeSearchBox = () => { this.props.onCloseSearch(); + this.searchAddon.clearDecorations(); + this.searchAddon.clearActiveDecoration(); + this.setState((state) => ({ + ...state, + searchResults: undefined + })); this.term.focus(); }; @@ -350,8 +401,8 @@ export default class Term extends React.PureComponent { // otherwise use the default sound found in xterm. nextTermOptions.bellSound = this.props.bellSound || this.termDefaultBellSound!; - if (!prevProps.search && this.props.search) { - this.search(); + if (prevProps.search && !this.props.search) { + this.closeSearchBox(); } // Update only options that have changed. @@ -445,14 +496,38 @@ export default class Term extends React.PureComponent { {this.props.customChildren} {this.props.search ? ( + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, caseSensitive: !this.state.searchOptions.caseSensitive} + }) + } + toggleWholeWord={() => + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, wholeWord: !this.state.searchOptions.wholeWord} + }) + } + toggleRegex={() => + this.setState({ + ...this.state, + searchOptions: {...this.state.searchOptions, regex: !this.state.searchOptions.regex} + }) + } + selectionColor={this.props.selectionColor} + backgroundColor={this.props.backgroundColor} + foregroundColor={this.props.foregroundColor} + borderColor={this.props.borderColor} + font={this.props.uiFontFamily} /> - ) : ( - '' - )} + ) : null}