Skip to content

Commit

Permalink
feat: terminal search optimization (#4384)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marckon authored Feb 18, 2025
1 parent 305bfb1 commit dacfb46
Show file tree
Hide file tree
Showing 10 changed files with 313 additions and 95 deletions.
61 changes: 61 additions & 0 deletions packages/terminal-next/src/browser/component/search.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@option-size: 20px;

.terminalSearch {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 999;
right: 16px;
border-radius: 2px;
padding: 6px 8px;
box-shadow: rgba(0, 0, 0, 0.133) 0px 3.2px 7.2px 0px, rgba(0, 0, 0, 0.11) 0px 0.6px 1.8px 0px;
background: var(--kt-panelTitle-background);

.searchField {
&:focus {
border-color: var(--focusBorder);
}

.optionBtn {
width: @option-size;
height: @option-size;
margin: 0 1px;
box-sizing: border-box;
display: flex;
justify-content: center;
user-select: none;
background-repeat: no-repeat;
background-position: center;
border: 1px solid transparent;
cursor: pointer;
opacity: 0.7;

&:hover {
opacity: 1;
}

&::before {
display: inline-block;
height: @option-size - 2;
line-height: @option-size - 2;
}

&.select {
background-color: var(--inputOption-activeBackground);
border-color: var(--inputOption-activeBorder);
opacity: 1;
}
}
}

.panelBtn {
padding: 0px 6px;
cursor: pointer;
}

.searchResult {
margin: 0 4px;
font-size: 14px;
}
}
142 changes: 142 additions & 0 deletions packages/terminal-next/src/browser/component/search.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import cls from 'classnames';
import React from 'react';

import { ValidateInput } from '@opensumi/ide-components';
import { getIcon, localize, useInjectable } from '@opensumi/ide-core-browser';

import { ISearchResult, ITerminalSearchService } from '../../common';

import styles from './search.module.less';

export const TerminalSearch: React.FC<{}> = React.memo((props) => {
const searchService = useInjectable<ITerminalSearchService>(ITerminalSearchService);
const [UIState, setUIState] = React.useState(searchService.UIState);
const [searchResult, setSearchResult] = React.useState<ISearchResult | null>(null);
const [inputText, setInputText] = React.useState(searchService.text || '');
const inputRef = React.useRef<HTMLInputElement>(null);

React.useEffect(() => {
const dispose = searchService.onVisibleChange((show) => {
if (show && inputRef.current) {
inputRef.current.focus();

if (inputRef.current.value.length > 0) {
inputRef.current.setSelectionRange(0, inputRef.current.value.length);
}
}
});
return () => dispose.dispose();
}, [searchService]);

React.useEffect(() => {
if (!searchService.onResultChange) {
return;
}

const dispose = searchService.onResultChange((event) => {
setSearchResult(event);
});

return () => dispose.dispose();
}, [searchService]);

const searchInput = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
searchService.text = event.target.value;
searchService.search();
setInputText(event.target.value);
},
[searchService],
);

const searchKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
searchService.search();
}

if (event.key === 'Escape') {
searchService.close();
searchService.clear();
}
},
[searchService],
);

const toggleMatchCase = React.useCallback(() => {
searchService.updateUIState({ isMatchCase: !UIState.isMatchCase });
setUIState(searchService.UIState);
}, [searchService, UIState]);

const toggleRegex = React.useCallback(() => {
searchService.updateUIState({ isUseRegexp: !UIState.isUseRegexp });
setUIState(searchService.UIState);
}, [searchService, UIState]);

const toggleWholeWord = React.useCallback(() => {
searchService.updateUIState({ isWholeWord: !UIState.isWholeWord });
setUIState(searchService.UIState);
}, [searchService, UIState]);

const searchNext = React.useCallback(() => {
searchService.searchNext();
}, [searchService]);

const searchPrev = React.useCallback(() => {
searchService.searchPrevious();
}, [searchService]);

const close = React.useCallback(() => {
searchService.close();
}, [searchService]);

return (
<div className={styles.terminalSearch}>
<ValidateInput
className={styles.searchField}
autoFocus
id='search-input-field'
title={localize('search.input.placeholder')}
type='text'
value={inputText}
placeholder={localize('common.find')}
onKeyDown={searchKeyDown}
onChange={searchInput}
ref={inputRef}
validateMessage={undefined}
addonAfter={[
<span
key={localize('search.caseDescription')}
className={cls(getIcon('ab'), styles['match-case'], styles.optionBtn, {
[styles.select]: UIState.isMatchCase,
})}
title={localize('search.caseDescription')}
onClick={toggleMatchCase}
></span>,
<span
key={localize('search.wordsDescription')}
className={cls(getIcon('abl'), styles['whole-word'], styles.optionBtn, {
[styles.select]: UIState.isWholeWord,
})}
title={localize('search.wordsDescription')}
onClick={toggleWholeWord}
></span>,
<span
key={localize('search.regexDescription')}
className={cls(getIcon('regex'), styles['use-regexp'], styles.optionBtn, {
[styles.select]: UIState.isUseRegexp,
})}
title={localize('search.regexDescription')}
onClick={toggleRegex}
></span>,
]}
/>
<div className={styles.searchResult}>
{searchResult ? `${searchResult.resultIndex + 1}/${searchResult.resultCount}` : '0/0'}
</div>
<div className={cls(styles.panelBtn, getIcon('up'))} onClick={searchPrev}></div>
<div className={cls(styles.panelBtn, getIcon('down'))} onClick={searchNext}></div>
<div className={cls(styles.panelBtn, getIcon('close'))} onClick={close}></div>
</div>
);
});
30 changes: 0 additions & 30 deletions packages/terminal-next/src/browser/component/terminal.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -44,36 +44,6 @@
width: 100%;
}

.terminalSearch {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 999;
right: 16px;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.133) 0px 3.2px 7.2px 0px, rgba(0, 0, 0, 0.11) 0px 0.6px 1.8px 0px;
background: var(--kt-panelTitle-background);

input {
height: 28px;
padding: 0px 8px;
font-size: 12px;
border-style: solid;
border-width: 1px;
border-color: transparent;

&:focus {
border-color: var(--focusBorder);
}
}

.closeBtn {
padding: 0px 6px;
cursor: pointer;
}
}

.terminalFake {
position: absolute;
z-index: -999;
Expand Down
59 changes: 3 additions & 56 deletions packages/terminal-next/src/browser/component/terminal.view.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import cls from 'classnames';
import debounce from 'lodash/debounce';
import React from 'react';

import { FRAME_THREE, getIcon, localize, useAutorun, useEventEffect, useInjectable } from '@opensumi/ide-core-browser';
import { FRAME_THREE, useAutorun, useEventEffect, useInjectable } from '@opensumi/ide-core-browser';

import {
ITerminalController,
Expand All @@ -15,6 +14,7 @@ import {
} from '../../common';

import ResizeView, { ResizeDirection } from './resize.view';
import { TerminalSearch } from './search.view';
import styles from './terminal.module.less';
import TerminalWidget from './terminal.widget';

Expand All @@ -26,27 +26,13 @@ export default () => {
const errorService = useInjectable<ITerminalErrorService>(ITerminalErrorService);
const network = useInjectable<ITerminalNetwork>(ITerminalNetwork);

const inputRef = React.useRef<HTMLInputElement>(null);
const wrapperRef = React.useRef<HTMLDivElement | null>(null);

const view = useInjectable<ITerminalGroupViewService>(ITerminalGroupViewService);
const currentGroupId = useAutorun(view.currentGroupId);
const currentGroupIndex = useAutorun(view.currentGroupIndex);
const groups = useAutorun(view.groups);

React.useEffect(() => {
const dispose = searchService.onVisibleChange((show) => {
if (show && inputRef.current) {
inputRef.current.focus();

if (inputRef.current.value.length > 0) {
inputRef.current.setSelectionRange(0, inputRef.current.value.length);
}
}
});
return () => dispose.dispose();
}, [searchService, inputRef.current]);

const [themeBackground, setThemeBackground] = React.useState(controller.themeBackground);

useEventEffect(controller.onThemeBackgroundChange, (themeBackground) => {
Expand Down Expand Up @@ -78,31 +64,6 @@ export default () => {
setIsVisible(visible);
});

const [inputText, setInputText] = React.useState('');

const searchInput = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
searchService.text = event.target.value;
searchService.search();
setInputText(event.target.value);
},
[searchService],
);

const searchKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
searchService.search();
}

if (event.key === 'Escape') {
searchService.close();
searchService.clear();
}
},
[searchService],
);

React.useEffect(() => {
if (wrapperRef.current) {
controller.initContextKey(wrapperRef.current);
Expand All @@ -117,21 +78,7 @@ export default () => {
style={{ backgroundColor: themeBackground }}
data-group-current={currentGroupId}
>
{isVisible && (
<div className={styles.terminalSearch}>
<div className='kt-input-box'>
<input
autoFocus
ref={inputRef}
placeholder={localize('common.find')}
value={inputText}
onChange={searchInput}
onKeyDown={searchKeyDown}
/>
</div>
<div className={cls(styles.closeBtn, getIcon('close'))} onClick={() => searchService.close()}></div>
</div>
)}
{isVisible && <TerminalSearch />}
{groups.map((group, index) => {
if (!group.activated.get()) {
return;
Expand Down
15 changes: 13 additions & 2 deletions packages/terminal-next/src/browser/terminal.client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ISearchOptions } from '@xterm/addon-search';

import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di';
import { IEventBus, QuickPickService, TerminalClientAttachEvent, localize } from '@opensumi/ide-core-browser';
import { PreferenceService } from '@opensumi/ide-core-browser/lib/preferences/types';
Expand Down Expand Up @@ -729,9 +731,18 @@ export class TerminalClient extends Disposable implements ITerminalClient {
return this.xterm.raw.paste(text);
}

findNext(text: string) {
findNext(text: string, searchOptions: ISearchOptions = {}) {
this._checkReady();
return this.xterm.findNext(text, searchOptions);
}

findPrevious(text: string, searchOptions: ISearchOptions = {}) {
this._checkReady();
return this.xterm.findNext(text);
return this.xterm.findPrevious(text, searchOptions);
}

get onSearchResultsChange() {
return this.xterm.onSearchResultsChange;
}

closeSearch() {
Expand Down
Loading

0 comments on commit dacfb46

Please sign in to comment.