search improvements, added highlighting - fixes #5940, fixes #5032, fixes #5152

This commit is contained in:
Eugene Pankov 2022-04-03 15:19:22 +02:00
parent cfa29acb5a
commit 4aa824d725
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
8 changed files with 84 additions and 39 deletions

View File

@ -261,7 +261,9 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
case 'search': case 'search':
this.showSearchPanel = true this.showSearchPanel = true
setImmediate(() => { setImmediate(() => {
this.element.nativeElement.querySelector('.search-input').focus() const input = this.element.nativeElement.querySelector('.search-input')
input?.focus()
input?.select()
}) })
break break
case 'pane-focus-all': case 'pane-focus-all':

View File

@ -2,7 +2,7 @@
flex: auto; flex: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; // overflow: hidden;
position: relative; position: relative;
&> .content { &> .content {

View File

@ -1,27 +1,31 @@
input.search-input.form-control( .input-group
type='text', input.search-input.form-control(
[(ngModel)]='query', type='text',
(ngModelChange)='onQueryChange()', [(ngModel)]='query',
[class.text-danger]='notFound', (ngModelChange)='onQueryChange()',
(click)='$event.stopPropagation()', [class.text-danger]='state.resultCount == 0',
(keyup.enter)='findPrevious()', (click)='$event.stopPropagation()',
(keyup.esc)='close.emit()', (keyup.enter)='findPrevious()',
[placeholder]='"Search"|translate' (keyup.esc)='close.emit()',
) [placeholder]='"Search"|translate'
)
.input-group-append(*ngIf='state.resultCount > 0')
.input-group-text.result-counter {{state.resultIndex + 1}} / {{state.resultCount}}
button.btn.btn-link( ng-container(*ngIf='state.resultCount > 0')
(click)='findPrevious()', button.btn.btn-link(
ngbTooltip='Search up', (click)='findPrevious()',
placement='bottom', ngbTooltip='Search up',
[fastHtmlBind]='icons.arrowUp' placement='bottom',
) [fastHtmlBind]='icons.arrowUp'
)
button.btn.btn-link( button.btn.btn-link(
(click)='findNext()', (click)='findNext()',
ngbTooltip='Search down', ngbTooltip='Search down',
placement='bottom', placement='bottom',
[fastHtmlBind]='icons.arrowDown' [fastHtmlBind]='icons.arrowDown'
) )
.mr-2 .mr-2

View File

@ -1,6 +1,6 @@
:host { :host {
position: fixed; position: fixed;
width: 400px; width: 600px;
right: 40px; right: 40px;
z-index: 5; z-index: 5;
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
@ -15,3 +15,8 @@
flex-shrink: 0; flex-shrink: 0;
} }
} }
.result-counter {
font-size: 0.7rem;
opacity: .5;
}

View File

@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter } from '@angular/core' import { Component, Input, Output, EventEmitter } from '@angular/core'
import { Frontend, SearchOptions } from '../frontends/frontend' import { Frontend, SearchOptions, SearchState } from '../frontends/frontend'
import { ConfigService, NotificationsService, TranslateService } from 'tabby-core' import { ConfigService, NotificationsService, TranslateService } from 'tabby-core'
@Component({ @Component({
@ -10,7 +10,7 @@ import { ConfigService, NotificationsService, TranslateService } from 'tabby-cor
export class SearchPanelComponent { export class SearchPanelComponent {
@Input() query: string @Input() query: string
@Input() frontend: Frontend @Input() frontend: Frontend
notFound = false state: SearchState = { resultCount: 0 }
options: SearchOptions = { options: SearchOptions = {
incremental: true, incremental: true,
...this.config.store.terminal.searchOptions, ...this.config.store.terminal.searchOptions,
@ -34,7 +34,7 @@ export class SearchPanelComponent {
) { } ) { }
onQueryChange (): void { onQueryChange (): void {
this.notFound = false this.state = { resultCount: 0 }
this.findPrevious(true) this.findPrevious(true)
} }
@ -42,8 +42,8 @@ export class SearchPanelComponent {
if (!this.query) { if (!this.query) {
return return
} }
if (!this.frontend.findNext(this.query, { ...this.options, incremental: incremental || undefined })) { this.state = this.frontend.findNext(this.query, { ...this.options, incremental: incremental || undefined })
this.notFound = true if (!this.state.resultCount) {
this.notifications.notice(this.translate.instant('Not found')) this.notifications.notice(this.translate.instant('Not found'))
} }
} }
@ -52,8 +52,8 @@ export class SearchPanelComponent {
if (!this.query) { if (!this.query) {
return return
} }
if (!this.frontend.findPrevious(this.query, { ...this.options, incremental: incremental || undefined })) { this.state = this.frontend.findPrevious(this.query, { ...this.options, incremental: incremental || undefined })
this.notFound = true if (!this.state.resultCount) {
this.notifications.notice(this.translate.instant('Not found')) this.notifications.notice(this.translate.instant('Not found'))
} }
} }

View File

@ -9,6 +9,11 @@ export interface SearchOptions {
incremental?: true incremental?: true
} }
export interface SearchState {
resultIndex?: number
resultCount: number
}
/** /**
* Extend to add support for a different VT frontend implementation * Extend to add support for a different VT frontend implementation
*/ */
@ -75,8 +80,8 @@ export abstract class Frontend {
abstract configure (): void abstract configure (): void
abstract setZoom (zoom: number): void abstract setZoom (zoom: number): void
abstract findNext (term: string, searchOptions?: SearchOptions): boolean abstract findNext (term: string, searchOptions?: SearchOptions): SearchState
abstract findPrevious (term: string, searchOptions?: SearchOptions): boolean abstract findPrevious (term: string, searchOptions?: SearchOptions): SearchState
abstract cancelSearch (): void abstract cancelSearch (): void
abstract saveState (): any abstract saveState (): any

View File

@ -16,3 +16,13 @@
.xterm-decoration-overview-ruler { .xterm-decoration-overview-ruler {
right: 6px; right: 6px;
} }
.xterm-find-result-decoration {
box-sizing: content-box;
border-radius: 3px;
padding: 2px;
margin: -2px;
outline: 2px solid yellow;
backdrop-filter: contrast(2);
}

View File

@ -1,6 +1,6 @@
import { Injector } from '@angular/core' import { Injector } from '@angular/core'
import { ConfigService, getCSSFontFamily, HostAppService, HotkeysService, Platform, PlatformService } from 'tabby-core' import { ConfigService, getCSSFontFamily, HostAppService, HotkeysService, Platform, PlatformService } from 'tabby-core'
import { Frontend, SearchOptions } from './frontend' import { Frontend, SearchOptions, SearchState } from './frontend'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { Terminal, ITheme } from 'xterm' import { Terminal, ITheme } from 'xterm'
import { FitAddon } from 'xterm-addon-fit' import { FitAddon } from 'xterm-addon-fit'
@ -34,6 +34,7 @@ export class XTermFrontend extends Frontend {
private configuredTheme: ITheme = {} private configuredTheme: ITheme = {}
private copyOnSelect = false private copyOnSelect = false
private search = new SearchAddon() private search = new SearchAddon()
private searchState: SearchState = { resultCount: 0 }
private fitAddon = new FitAddon() private fitAddon = new FitAddon()
private serializeAddon = new SerializeAddon() private serializeAddon = new SerializeAddon()
private ligaturesAddon?: LigaturesAddon private ligaturesAddon?: LigaturesAddon
@ -178,6 +179,10 @@ export class XTermFrontend extends Frontend {
this.xterm.loadAddon(this.search) this.xterm.loadAddon(this.search)
this.search.onDidChangeResults(state => {
this.searchState = state ?? { resultCount: 0 }
})
window.addEventListener('resize', this.resizeHandler) window.addEventListener('resize', this.resizeHandler)
this.resizeHandler() this.resizeHandler()
@ -327,16 +332,30 @@ export class XTermFrontend extends Frontend {
decorations: { decorations: {
matchOverviewRuler: '#cccc00', matchOverviewRuler: '#cccc00',
activeMatchColorOverviewRuler: '#ffff00', activeMatchColorOverviewRuler: '#ffff00',
matchBorder: '#cc0',
activeMatchBorder: '#ff0',
activeMatchBackground: 'rgba(255, 255, 0, 0.3)',
}, },
} }
} }
findNext (term: string, searchOptions?: SearchOptions): boolean { private wrapSearchResult (result: boolean): SearchState {
return this.search.findNext(term, this.getSearchOptions(searchOptions)) if (!result) {
return { resultCount: 0 }
}
return this.searchState
} }
findPrevious (term: string, searchOptions?: SearchOptions): boolean { findNext (term: string, searchOptions?: SearchOptions): SearchState {
return this.search.findPrevious(term, this.getSearchOptions(searchOptions)) return this.wrapSearchResult(
this.search.findNext(term, this.getSearchOptions(searchOptions))
)
}
findPrevious (term: string, searchOptions?: SearchOptions): SearchState {
return this.wrapSearchResult(
this.search.findPrevious(term, this.getSearchOptions(searchOptions))
)
} }
cancelSearch (): void { cancelSearch (): void {