Skip to content

Commit

Permalink
Add terminal search widget.
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
  • Loading branch information
AndrienkoAleksandr committed Feb 24, 2020
1 parent 8ad04be commit 5209a9e
Show file tree
Hide file tree
Showing 15 changed files with 415 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/core/src/browser/icons/arrow-down-bright.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/core/src/browser/icons/arrow-down-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/core/src/browser/icons/arrow-up-bright.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/core/src/browser/icons/arrow-up-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/core/src/browser/style/variables-bright.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ is not optimized for dense, information rich UIs.

/* Icons */
--theia-icon-close: url(../icons/close-bright.svg);
--theia-icon-arrow-up: url(../icons/arrow-up-bright.svg);
--theia-icon-arrow-down: url(../icons/arrow-down-bright.svg);
--theia-sprite-y-offset: 0px;
--theia-icon-circle: url(../icons/circle-bright.svg);
--theia-preloader: url(../icons/spinner.gif);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/browser/style/variables-dark.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ is not optimized for dense, information rich UIs.

/* Icons */
--theia-icon-close: url(../icons/close-dark.svg);
--theia-icon-arrow-up: url(../icons/arrow-up-dark.svg);
--theia-icon-arrow-down: url(../icons/arrow-down-dark.svg);
--theia-sprite-y-offset: -20px;
--theia-icon-circle: url(../icons/circle-dark.svg);
--theia-preloader: url(../icons/spinner.gif);
Expand Down
5 changes: 5 additions & 0 deletions packages/terminal/src/browser/base/terminal-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { Event } from '@theia/core';
import { BaseWidget } from '@theia/core/lib/browser';
import { TerminalSearchWidget } from '../search/terminal-search-widget';

/**
* Terminal UI widget.
Expand Down Expand Up @@ -48,6 +49,10 @@ export abstract class TerminalWidget extends BaseWidget {
*/
abstract clearOutput(): void;

/**
* Return Terminal search box widget.
*/
abstract getSearchBox(): TerminalSearchWidget;
/**
* Whether the terminal process has child processes.
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/terminal/src/browser/search/terminal-search-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/********************************************************************************
* Copyright (C) 2019 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { interfaces } from 'inversify';
import { TerminalSearchWidget, TerminalSearchWidgetFactory } from './terminal-search-widget';
import { Terminal } from 'xterm';

export function createTerminalSearchFactory(container: interfaces.Container): TerminalSearchWidgetFactory {
container.bind(TerminalSearchWidget).toSelf().inSingletonScope();

return (terminal: Terminal) => {
container.bind(Terminal).toConstantValue(terminal);
return container.get(TerminalSearchWidget);
};
}
157 changes: 157 additions & 0 deletions packages/terminal/src/browser/search/terminal-search-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/********************************************************************************
* Copyright (C) 2019 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject, postConstruct } from 'inversify';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import * as React from 'react';
import '../../../src/browser/style/terminal-search.css';
import { Terminal } from 'xterm';
import { findNext, findPrevious } from 'xterm/lib/addons/search/search';
import { ISearchOptions } from 'xterm/lib/addons/search/Interfaces';
import { Key } from '@theia/core/lib/browser';

export const TERMINAL_SEARCH_WIDGET_FACTORY_ID = 'terminal-search';
export const TerminalSearchWidgetFactory = Symbol('TerminalSearchWidgetFactory');
export type TerminalSearchWidgetFactory = (terminal: Terminal) => TerminalSearchWidget;

@injectable()
export class TerminalSearchWidget extends ReactWidget {

private searchInput: HTMLInputElement | null;
private searchBox: HTMLDivElement | null;
private searchOptions: ISearchOptions = {};

@inject(Terminal)
protected terminal: Terminal;

@postConstruct()
protected init(): void {
this.node.classList.add('theia-search-terminal-widget-parent');

this.hide();
this.update();
}

protected render(): React.ReactNode {
return <div className='theia-search-terminal-widget'>
<div className='theia-search-elem-box' ref={searchBox => this.searchBox = searchBox} >
<input
title='Find'
type='text'
placeholder='Find'
ref={ip => this.searchInput = ip}
onKeyUp={this.onInputChanged}
onFocus={this.onSearchInputFocus}
onBlur={this.onSearchInputBlur}
/>
<div title='Match case' tabIndex={0} className='search-elem match-case' onClick={this.handleCaseSensitiveOptionClicked}></div>
<div title='Match whole word' tabIndex={0} className='search-elem whole-word' onClick={this.handleWroleWordOptionClicked}></div>
<div title='Use regular expression' tabIndex={0} className='search-elem use-regexp' onClick={this.handleRegexOptionClicked}></div>
</div>
<button title='Previous match' className='search-elem arrow-up' onClick={this.handlePreviousButtonClicked}></button>
<button title='Next match' className='search-elem arrow-down' onClick={this.handleNextButtonClicked}></button>
<button title='Close' className='search-elem close' onClick={this.handleHide}></button>
</div>;
}

onSearchInputFocus = (): void => {
if (this.searchBox) {
this.searchBox.classList.add('focused');
}
};

onSearchInputBlur = (): void => {
if (this.searchBox) {
this.searchBox.classList.remove('focused');
}
};

private handleHide = (): void => {
this.hide();
};

private handleCaseSensitiveOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.caseSensitive = !this.searchOptions.caseSensitive;
this.updateSearchInputBox(this.searchOptions.caseSensitive, event.currentTarget);
};

private handleWroleWordOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.wholeWord = !this.searchOptions.wholeWord;
this.updateSearchInputBox(this.searchOptions.wholeWord, event.currentTarget);
};

private handleRegexOptionClicked = (event: React.MouseEvent<HTMLSpanElement>): void => {
this.searchOptions.regex = !this.searchOptions.regex;
this.updateSearchInputBox(this.searchOptions.regex, event.currentTarget);
};

private updateSearchInputBox(enable: boolean, optionElement: HTMLSpanElement): void {
if (enable) {
optionElement.classList.add('option-enabled');
} else {
optionElement.classList.remove('option-enabled');
}
this.searchInput!.focus();
}

private onInputChanged = (event: React.KeyboardEvent): void => {
// move to previous search result on `Shift + Enter`
if (event && event.shiftKey && event.keyCode === Key.ENTER.keyCode) {
this.search(false, 'previous');
return;
}
// move to next search result on `Enter`
if (event && event.keyCode === Key.ENTER.keyCode) {
this.search(false, 'next');
return;
}

this.search(true, 'next');
};

search(incremental: boolean, searchDirection: 'next' | 'previous'): void {
if (this.searchInput) {
this.searchOptions.incremental = incremental;
const searchText = this.searchInput.value;

if (searchDirection === 'next') {
findNext(this.terminal, searchText, this.searchOptions);
}

if (searchDirection === 'previous') {
findPrevious(this.terminal, searchText, this.searchOptions);
}
}
}

private handleNextButtonClicked = (): void => {
this.search(false, 'next');
};

private handlePreviousButtonClicked = (): void => {
this.search(false, 'previous');
};

onAfterHide(): void {
this.terminal.focus();
}

onAfterShow(): void {
if (this.searchInput) {
this.searchInput.select();
}
}
}
112 changes: 112 additions & 0 deletions packages/terminal/src/browser/style/terminal-search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/********************************************************************************
* Copyright (C) 2019 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

.theia-search-terminal-widget-parent {
background: var(--theia-sideBar-background);
position: absolute;
margin: 0px;
border: var(--theia-border-width) solid transparent;
padding: 0px;
top: 1px;
right: 19px;

z-index: 10;
}

.theia-search-terminal-widget-parent .theia-search-elem-box {
display: flex;
margin: 0px;
border: var(--theia-border-width) solid transparent;
padding: 0px;
align-items: center;
color: var(--theia-input-foreground);
background: var(--theia-input-background);
}

.theia-search-terminal-widget-parent .theia-search-elem-box input {
margin-left: 5px;
padding: 0px;
width: 100px;
height: 18px;
color: inherit;
background-color: inherit;
border: var(--theia-border-width) solid transparent;
outline: none;
}

.theia-search-terminal-widget-parent .search-elem {
border: var(--theia-border-width) solid transparent;
background-position: center;
background-repeat: no-repeat;
height: 18px;
width: 18px;
margin: 1px;
opacity: 0.7;
outline: none;
}

.theia-search-terminal-widget-parent .search-elem:hover {
opacity: 1;
}

.theia-search-terminal-widget-parent .theia-search-elem-box.focused {
border: var(--theia-border-width) solid var(--theia-focusBorder);
}

.theia-search-terminal-widget-parent .theia-search-elem-box .search-elem.option-enabled {
border: var(--theia-border-width) solid var(--theia-inputOption-activeBorder);
background-color: var(--theia-inputOption-activeBackground);
}

.theia-search-terminal-widget-parent .theia-search-elem-box .whole-word {
background-image: var(--theia-icon-whole-word);
}

.theia-search-terminal-widget-parent .theia-search-elem-box .match-case {
background-image: var(--theia-icon-case-sensitive);
}

.theia-search-terminal-widget-parent .theia-search-elem-box .use-regexp {
background-image: var(--theia-icon-regex);
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget {
margin: 2px;
display: flex;
align-items: center;
font: var(--theia-content-font-size);
color: var(--theia-input-foreground);
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget button {
background-color: transparent;
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget button:focus {
border: var(--theia-border-width) var(--theia-focusBorder) solid;
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget .search-elem.close {
background-image: var(--theia-icon-close);
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget .search-elem.arrow-up {
background-image: var(--theia-icon-arrow-up);
}

.theia-search-terminal-widget-parent .theia-search-terminal-widget .search-elem.arrow-down {
background-image: var(--theia-icon-arrow-down);
}
File renamed without changes.
Loading

0 comments on commit 5209a9e

Please sign in to comment.