chrome-devtools-frontend
Version:
Chrome DevTools UI
263 lines (224 loc) • 9.62 kB
text/typescript
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import searchResultsPaneStyles from './searchResultsPane.css.js';
import {type SearchConfig, type SearchResult} from './SearchConfig.js';
const UIStrings = {
/**
*@description Accessibility label for number of matches in each file in search results pane
*@example {2} PH1
*/
matchesCountS: 'Matches Count {PH1}',
/**
*@description Search result label for results in the Search tool
*@example {2} PH1
*/
lineS: 'Line {PH1}',
/**
*@description Text in Search Results Pane of the Search tab
*@example {2} PH1
*/
showDMore: 'Show {PH1} more',
};
const str_ = i18n.i18n.registerUIStrings('panels/search/SearchResultsPane.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class SearchResultsPane extends UI.Widget.VBox {
private readonly searchConfig: SearchConfig;
private readonly searchResults: SearchResult[];
private readonly treeElements: SearchResultsTreeElement[];
private treeOutline: UI.TreeOutline.TreeOutlineInShadow;
private matchesExpandedCount: number;
constructor(searchConfig: SearchConfig) {
super(true);
this.searchConfig = searchConfig;
this.searchResults = [];
this.treeElements = [];
this.treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
this.treeOutline.hideOverflow();
this.contentElement.appendChild(this.treeOutline.element);
this.matchesExpandedCount = 0;
}
addSearchResult(searchResult: SearchResult): void {
this.searchResults.push(searchResult);
this.addTreeElement(searchResult);
}
showAllMatches(): void {
this.treeElements.forEach(treeElement => {
treeElement.expand();
treeElement.showAllMatches();
});
}
collapseAllResults(): void {
this.treeElements.forEach(treeElement => {
treeElement.collapse();
});
}
private addTreeElement(searchResult: SearchResult): void {
const treeElement = new SearchResultsTreeElement(this.searchConfig, searchResult);
this.treeOutline.appendChild(treeElement);
if (!this.treeOutline.selectedTreeElement) {
treeElement.select(/* omitFocus */ true, /* selectedByUser */ true);
}
// Expand until at least a certain number of matches is expanded.
if (this.matchesExpandedCount < matchesExpandedByDefault) {
treeElement.expand();
}
this.matchesExpandedCount += searchResult.matchesCount();
this.treeElements.push(treeElement);
}
override wasShown(): void {
super.wasShown();
this.treeOutline.registerCSSFiles([searchResultsPaneStyles]);
}
}
export const matchesExpandedByDefault = 200;
export const matchesShownAtOnce = 20;
export class SearchResultsTreeElement extends UI.TreeOutline.TreeElement {
private searchConfig: SearchConfig;
private searchResult: SearchResult;
private initialized: boolean;
override toggleOnClick: boolean;
constructor(searchConfig: SearchConfig, searchResult: SearchResult) {
super('', true);
this.searchConfig = searchConfig;
this.searchResult = searchResult;
this.initialized = false;
this.toggleOnClick = true;
}
override onexpand(): void {
if (this.initialized) {
return;
}
this.updateMatchesUI();
this.initialized = true;
}
showAllMatches(): void {
this.removeChildren();
this.appendSearchMatches(0, this.searchResult.matchesCount());
}
private updateMatchesUI(): void {
this.removeChildren();
const toIndex = Math.min(this.searchResult.matchesCount(), matchesShownAtOnce);
if (toIndex < this.searchResult.matchesCount()) {
this.appendSearchMatches(0, toIndex - 1);
this.appendShowMoreMatchesElement(toIndex - 1);
} else {
this.appendSearchMatches(0, toIndex);
}
}
override onattach(): void {
this.updateSearchMatches();
}
private updateSearchMatches(): void {
this.listItemElement.classList.add('search-result');
const fileNameSpan = span(this.searchResult.label(), 'search-result-file-name');
fileNameSpan.appendChild(span('\u2014', 'search-result-dash'));
fileNameSpan.appendChild(span(this.searchResult.description(), 'search-result-qualifier'));
this.tooltip = this.searchResult.description();
this.listItemElement.appendChild(fileNameSpan);
const matchesCountSpan = document.createElement('span');
matchesCountSpan.className = 'search-result-matches-count';
matchesCountSpan.textContent = `${this.searchResult.matchesCount()}`;
UI.ARIAUtils.setAccessibleName(
matchesCountSpan, i18nString(UIStrings.matchesCountS, {PH1: this.searchResult.matchesCount()}));
this.listItemElement.appendChild(matchesCountSpan);
if (this.expanded) {
this.updateMatchesUI();
}
function span(text: string, className: string): Element {
const span = document.createElement('span');
span.className = className;
span.textContent = text;
return span;
}
}
private appendSearchMatches(fromIndex: number, toIndex: number): void {
const searchResult = this.searchResult;
const queries = this.searchConfig.queries();
const regexes = [];
for (let i = 0; i < queries.length; ++i) {
regexes.push(Platform.StringUtilities.createSearchRegex(
queries[i], !this.searchConfig.ignoreCase(), this.searchConfig.isRegex()));
}
for (let i = fromIndex; i < toIndex; ++i) {
const lineContent = searchResult.matchLineContent(i).trim();
let matchRanges: TextUtils.TextRange.SourceRange[] = [];
for (let j = 0; j < regexes.length; ++j) {
matchRanges = matchRanges.concat(this.regexMatchRanges(lineContent, regexes[j]));
}
const anchor = Components.Linkifier.Linkifier.linkifyRevealable(searchResult.matchRevealable(i), '');
anchor.classList.add('search-match-link');
const labelSpan = document.createElement('span');
labelSpan.classList.add('search-match-line-number');
const resultLabel = searchResult.matchLabel(i);
labelSpan.textContent = resultLabel;
if (typeof resultLabel === 'number' && !isNaN(resultLabel)) {
UI.ARIAUtils.setAccessibleName(labelSpan, i18nString(UIStrings.lineS, {PH1: resultLabel}));
} else {
UI.ARIAUtils.setAccessibleName(labelSpan, resultLabel);
}
anchor.appendChild(labelSpan);
const contentSpan = this.createContentSpan(lineContent, matchRanges);
anchor.appendChild(contentSpan);
const searchMatchElement = new UI.TreeOutline.TreeElement();
this.appendChild(searchMatchElement);
searchMatchElement.listItemElement.className = 'search-match';
searchMatchElement.listItemElement.appendChild(anchor);
searchMatchElement.listItemElement.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.consume(true);
void Common.Revealer.reveal(searchResult.matchRevealable(i));
}
});
searchMatchElement.tooltip = lineContent;
}
}
private appendShowMoreMatchesElement(startMatchIndex: number): void {
const matchesLeftCount = this.searchResult.matchesCount() - startMatchIndex;
const showMoreMatchesText = i18nString(UIStrings.showDMore, {PH1: matchesLeftCount});
const showMoreMatchesTreeElement = new UI.TreeOutline.TreeElement(showMoreMatchesText);
this.appendChild(showMoreMatchesTreeElement);
showMoreMatchesTreeElement.listItemElement.classList.add('show-more-matches');
showMoreMatchesTreeElement.onselect =
this.showMoreMatchesElementSelected.bind(this, showMoreMatchesTreeElement, startMatchIndex);
}
private createContentSpan(lineContent: string, matchRanges: TextUtils.TextRange.SourceRange[]): Element {
let trimBy = 0;
if (matchRanges.length > 0 && matchRanges[0].offset > 20) {
trimBy = 15;
}
lineContent = lineContent.substring(trimBy, 1000 + trimBy);
if (trimBy) {
matchRanges =
matchRanges.map(range => new TextUtils.TextRange.SourceRange(range.offset - trimBy + 1, range.length));
lineContent = '…' + lineContent;
}
const contentSpan = document.createElement('span');
contentSpan.className = 'search-match-content';
contentSpan.textContent = lineContent;
UI.ARIAUtils.setAccessibleName(contentSpan, `${lineContent} line`);
UI.UIUtils.highlightRangesWithStyleClass(contentSpan, matchRanges, 'highlighted-search-result');
return contentSpan;
}
private regexMatchRanges(lineContent: string, regex: RegExp): TextUtils.TextRange.SourceRange[] {
regex.lastIndex = 0;
let match;
const matchRanges = [];
while ((regex.lastIndex < lineContent.length) && (match = regex.exec(lineContent))) {
matchRanges.push(new TextUtils.TextRange.SourceRange(match.index, match[0].length));
}
return matchRanges;
}
private showMoreMatchesElementSelected(
showMoreMatchesTreeElement: UI.TreeOutline.TreeElement, startMatchIndex: number): boolean {
this.removeChild(showMoreMatchesTreeElement);
this.appendSearchMatches(startMatchIndex, this.searchResult.matchesCount());
return false;
}
}