Skip to content

Commit

Permalink
[siw] performance improvements
Browse files Browse the repository at this point in the history
- one message per file with multiple matches
- long lines are not send entirely to clients
- new option for max file size (defaults to 20M)

Signed-off-by: Sven Efftinge <sven.efftinge@typefox.io>
  • Loading branch information
svenefftinge committed Jan 3, 2020
1 parent 5a012fc commit ec2a866
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 145 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,7 @@ body.theia-editor-highlightModifiedTabs
.p-TabBar-toolbar .item .refresh {
background: var(--theia-icon-refresh) no-repeat;
}

.p-TabBar-toolbar .item .cancel {
background: var(--theia-icon-close) no-repeat;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export namespace SearchInWorkspaceCommands {
label: 'Refresh',
iconClass: 'refresh'
};
export const CANCEL_SEARCH: Command = {
id: 'search-in-workspace.cancel',
category: SEARCH_CATEGORY,
label: 'Cancel Search',
iconClass: 'cancel'
};
export const COLLAPSE_ALL: Command = {
id: 'search-in-workspace.collapse-all',
category: SEARCH_CATEGORY,
Expand Down Expand Up @@ -133,6 +139,11 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut
}
}));

commands.registerCommand(SearchInWorkspaceCommands.CANCEL_SEARCH, {
execute: w => this.withWidget(w, widget => widget.getCancelIndicator() && widget.getCancelIndicator()!.cancel()),
isEnabled: w => this.withWidget(w, widget => widget.getCancelIndicator() !== undefined),
isVisible: w => this.withWidget(w, widget => widget.getCancelIndicator() !== undefined)
});
commands.registerCommand(SearchInWorkspaceCommands.REFRESH_RESULTS, {
execute: w => this.withWidget(w, widget => widget.refresh()),
isEnabled: w => this.withWidget(w, widget => (widget.hasResultList() || widget.hasSearchTerm()) && this.workspaceService.tryGetRoots().length > 0),
Expand Down Expand Up @@ -201,25 +212,32 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
const widget = await this.widget;
const onDidChange = widget.onDidUpdate;
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.CANCEL_SEARCH.id,
command: SearchInWorkspaceCommands.CANCEL_SEARCH.id,
tooltip: SearchInWorkspaceCommands.CANCEL_SEARCH.label,
priority: 0,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.REFRESH_RESULTS.id,
command: SearchInWorkspaceCommands.REFRESH_RESULTS.id,
tooltip: SearchInWorkspaceCommands.REFRESH_RESULTS.label,
priority: 0,
priority: 1,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.CLEAR_ALL.id,
command: SearchInWorkspaceCommands.CLEAR_ALL.id,
tooltip: SearchInWorkspaceCommands.CLEAR_ALL.label,
priority: 1,
priority: 2,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.COLLAPSE_ALL.id,
command: SearchInWorkspaceCommands.COLLAPSE_ALL.id,
tooltip: SearchInWorkspaceCommands.COLLAPSE_ALL.label,
priority: 2,
priority: 3,
onDidChange
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { CancellationTokenSource, Emitter, Event } from '@theia/core';
import { EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane, EditorWidget, ReplaceOperation, EditorOpenerOptions } from '@theia/editor/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileResourceResolver } from '@theia/filesystem/lib/browser';
import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
import { SearchInWorkspaceResult, SearchInWorkspaceOptions, SearchMatch } from '../common/search-in-workspace-interface';
import { SearchInWorkspaceService } from './search-in-workspace-service';
import { MEMORY_TEXT } from './in-memory-text-resource';
import URI from '@theia/core/lib/common/uri';
Expand Down Expand Up @@ -83,7 +83,7 @@ export namespace SearchInWorkspaceFileNode {
}
}

export interface SearchInWorkspaceResultLineNode extends SelectableTreeNode, SearchInWorkspaceResult { // line node
export interface SearchInWorkspaceResultLineNode extends SelectableTreeNode, SearchInWorkspaceResult, SearchMatch { // line node
parent: SearchInWorkspaceFileNode
}
export namespace SearchInWorkspaceResultLineNode {
Expand All @@ -104,7 +104,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {

protected appliedDecorations = new Map<string, string[]>();

private cancelIndicator = new CancellationTokenSource();
cancelIndicator?: CancellationTokenSource;

protected changeEmitter = new Emitter<Map<string, SearchInWorkspaceRootFolderNode>>();
// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -200,53 +200,53 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
this.searchTerm = searchTerm;
const collapseValue: string = this.searchInWorkspacePreferences['search.collapseResults'];
this.resultTree.clear();
this.cancelIndicator.cancel();
this.cancelIndicator = new CancellationTokenSource();
const token = this.cancelIndicator.token;
if (this.cancelIndicator) {
this.cancelIndicator.cancel();
}
if (searchTerm === '') {
this.refreshModelChildren();
return;
}
this.cancelIndicator = new CancellationTokenSource();
const cancelIndicator = this.cancelIndicator;
const token = this.cancelIndicator.token;
token.onCancellationRequested(() => {
this.changeEmitter.fire(this.resultTree);
});
const progress = await this.progressService.showProgress({ text: `search: ${searchTerm}`, options: { location: 'search' } });
// tslint:disable-next-line:no-any
let pendingRefreshTimeout: any;
const searchId = await this.searchService.search(searchTerm, {
onResult: (aSearchId: number, result: SearchInWorkspaceResult) => {
if (token.isCancellationRequested || aSearchId !== searchId) {
return;
}
const { path } = this.filenameAndPath(result.root, result.fileUri);
const tree = this.resultTree;
const rootFolderNode = tree.get(result.root);

if (rootFolderNode) {
const fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (fileNode) {
const line = this.createResultLineNode(result, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
}
this.collapseFileNode(fileNode, collapseValue);
} else {
const newFileNode = this.createFileNode(result.root, path, result.fileUri, rootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
const line = this.createResultLineNode(result, newFileNode);
newFileNode.children.push(line);
rootFolderNode.children.push(newFileNode);
let rootFolderNode = tree.get(result.root);
if (!rootFolderNode) {
rootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, rootFolderNode);
}
let fileNode = rootFolderNode.children.find(f => f.fileUri === result.fileUri);
if (!fileNode) {
fileNode = this.createFileNode(result.root, path, result.fileUri, rootFolderNode);
rootFolderNode.children.push(fileNode);
}
for (const match of result.matches) {
const line = this.createResultLineNode(result, match, fileNode);
if (fileNode.children.findIndex(lineNode => lineNode.id === line.id) < 0) {
fileNode.children.push(line);
}

} else {
const newRootFolderNode = this.createRootFolderNode(result.root);
tree.set(result.root, newRootFolderNode);
const newFileNode = this.createFileNode(result.root, path, result.fileUri, newRootFolderNode);
this.collapseFileNode(newFileNode, collapseValue);
newFileNode.children.push(this.createResultLineNode(result, newFileNode));
newRootFolderNode.children.push(newFileNode);
}
this.collapseFileNode(fileNode, collapseValue);
if (pendingRefreshTimeout) {
clearTimeout(pendingRefreshTimeout);
}
pendingRefreshTimeout = setTimeout(() => this.refreshModelChildren(), 100);
},
onDone: () => {
progress.cancel();
if (token.isCancellationRequested) {
return;
}
cancelIndicator.cancel();
// Sort the result map by folder URI.
this.resultTree = new Map([...this.resultTree]
.sort((a: [string, SearchInWorkspaceRootFolderNode], b: [string, SearchInWorkspaceRootFolderNode]) => this.compare(a[1].folderUri, b[1].folderUri)));
Expand All @@ -263,6 +263,7 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
if (searchId) {
this.searchService.cancel(searchId);
}
this.cancelIndicator = undefined;
});
}

Expand Down Expand Up @@ -357,12 +358,13 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
};
}

protected createResultLineNode(result: SearchInWorkspaceResult, fileNode: SearchInWorkspaceFileNode): SearchInWorkspaceResultLineNode {
protected createResultLineNode(result: SearchInWorkspaceResult, match: SearchMatch, fileNode: SearchInWorkspaceFileNode): SearchInWorkspaceResultLineNode {
return {
...result,
...match,
selected: false,
id: result.fileUri + '-' + result.line + '-' + result.character + '-' + result.length,
name: result.lineText,
id: result.fileUri + '-' + match.line + '-' + match.character + '-' + match.length,
name: typeof match.lineText === 'string' ? match.lineText : match.lineText.text,
parent: fileNode
};
}
Expand Down Expand Up @@ -628,24 +630,39 @@ export class SearchInWorkspaceResultTreeWidget extends TreeWidget {
}

protected renderResultLineNode(node: SearchInWorkspaceResultLineNode): React.ReactNode {
const prefix = node.character > 26 ? '... ' : '';
return <div className={`resultLine noWrapInfo ${node.selected ? 'selected' : ''}`} title={node.lineText.trim()}>
let before;
let after;
let title;
if (typeof node.lineText === 'string') {
const prefix = node.character > 26 ? '... ' : '';
before = prefix + node.lineText.substr(0, node.character - 1).substr(-25);
after = node.lineText.substr(node.character - 1 + node.length, 75);
title = node.lineText.trim();
} else {
before = node.lineText.text.substr(0, node.lineText.character);
after = node.lineText.text.substr(node.lineText.character + node.length);
title = node.lineText.text.trim();
}
return <div className={`resultLine noWrapInfo ${node.selected ? 'selected' : ''}`} title={title}>
{this.searchInWorkspacePreferences['search.lineNumbers'] && <span className='theia-siw-lineNumber'>{node.line}</span>}
<span>
{prefix + node.lineText.substr(0, node.character - 1).substr(-25)}
{before}
</span>
{this.renderMatchLinePart(node)}
<span>
{node.lineText.substr(node.character - 1 + node.length, 75)}
{after}
</span>
</div>;
}

protected renderMatchLinePart(node: SearchInWorkspaceResultLineNode): React.ReactNode {
const replaceTerm = this._replaceTerm !== '' && this._showReplaceButtons ? <span className='replace-term'>{this._replaceTerm}</span> : '';
const className = `match${this._showReplaceButtons ? ' strike-through' : ''}`;
const match = typeof node.lineText === 'string' ?
node.lineText.substr(node.character - 1, node.length)
: node.lineText.text.substr(node.lineText.character - 1, node.length);
return <React.Fragment>
<span className={className}>{node.lineText.substr(node.character - 1, node.length)}</span>
<span className={className}>{match}</span>
{replaceTerm}
</React.Fragment>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser';
import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
import { ProgressLocationService } from '@theia/core/lib/browser/progress-location-service';
import { ProgressBar } from '@theia/core/lib/browser/progress-bar';
import { CancellationTokenSource } from '@theia/core';

export interface SearchFieldState {
className: string;
Expand Down Expand Up @@ -217,6 +218,10 @@ export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidge
this.update();
}

getCancelIndicator(): CancellationTokenSource | undefined {
return this.resultTreeWidget.cancelIndicator;
}

collapseAll(): void {
this.resultTreeWidget.collapseAll();
this.update();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export interface SearchInWorkspaceOptions {
* Maximum number of results to return. Defaults to unlimited.
*/
maxResults?: number;
/**
* accepts suffixes of K, M or G which correspond to kilobytes,
* megabytes and gigabytes, respectively. If no suffix is provided the input is
* treated as bytes.
*
* defaults to '20M'
*/
maxFileSize?: string;
/**
* Search case sensitively if true.
*/
Expand All @@ -44,7 +52,7 @@ export interface SearchInWorkspaceOptions {
/**
* Glob pattern for matching files and directories to exclude the search.
*/
exclude?: string[]
exclude?: string[];
}

export interface SearchInWorkspaceResult {
Expand All @@ -58,6 +66,13 @@ export interface SearchInWorkspaceResult {
*/
fileUri: string;

/**
* matches found in the file
*/
matches: SearchMatch[];
}

export interface SearchMatch {
/**
* The (1-based) line number of the result.
*/
Expand All @@ -78,7 +93,13 @@ export interface SearchInWorkspaceResult {
/**
* The text of the line containing the result.
*/
lineText: string;
lineText: string | LinePreview;

}

export interface LinePreview {
text: string;
character: number;
}

export namespace SearchInWorkspaceResult {
Expand All @@ -90,16 +111,7 @@ export namespace SearchInWorkspaceResult {
if (a.fileUri !== b.fileUri) {
return a.fileUri < b.fileUri ? -1 : 1;
}

if (a.line !== b.line) {
return a.line - b.line;
}

if (a.character !== b.character) {
return a.character - b.character;
}

return a.length - b.length;
return 0;
}
}

Expand Down
Loading

0 comments on commit ec2a866

Please sign in to comment.