Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support quick file-search with go to line #9478

Merged
merged 1 commit into from
May 19, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 63 additions & 12 deletions packages/file-search/src/browser/quick-file-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/
import * as fuzzy from '@theia/core/shared/fuzzy';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
import { EditorOpenerOptions, Position, Range } from '@theia/editor/lib/browser';

export const quickFileOpen: Command = {
id: 'file-search.openFile',
category: 'File',
label: 'Open File...'
};

export interface FilterAndRange {
filter: string;
range: Range;
}

// Supports patterns of <path><#|:><line><#|:|,><col?>
const LINE_COLON_PATTERN = /\s?[#:\(](?:line )?(\d*)(?:[#:,](\d*))?\)?\s*$/;

@injectable()
export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {

Expand Down Expand Up @@ -69,10 +78,12 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
*/
protected isOpen: boolean = false;

protected filterAndRangeDefault = { filter: '', range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } };

/**
* The current lookFor string input by the user.
* Tracks the user file search filter and location range e.g. fileFilter:line:column or fileFilter:line,column
*/
protected currentLookFor: string = '';
protected filterAndRange: FilterAndRange = this.filterAndRangeDefault;

/**
* The score constants when comparing file search results.
Expand All @@ -94,7 +105,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
}

getOptions(): QuickOpenOptions {
let placeholder = 'File name to search.';
let placeholder = 'File name to search (append : to go to line).';
const keybinding = this.getKeyCommand();
if (keybinding) {
placeholder += ` (Press ${keybinding} to show/hide ignored files)`;
Expand Down Expand Up @@ -126,11 +137,11 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
this.hideIgnoredFiles = !this.hideIgnoredFiles;
} else {
this.hideIgnoredFiles = true;
this.currentLookFor = '';
this.filterAndRange = this.filterAndRangeDefault;
this.isOpen = true;
}

this.quickOpenService.open(this.currentLookFor);
this.quickOpenService.open(this.filterAndRange.filter);
}

/**
Expand All @@ -157,20 +168,22 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {

const roots = this.workspaceService.tryGetRoots();

this.currentLookFor = lookFor;
this.filterAndRange = this.splitFilterAndRange(lookFor);
const fileFilter = this.filterAndRange.filter;

const alreadyCollected = new Set<string>();
const recentlyUsedItems: QuickOpenItem[] = [];

const locations = [...this.navigationLocationService.locations()].reverse();
for (const location of locations) {
const uriString = location.uri.toString();
if (location.uri.scheme === 'file' && !alreadyCollected.has(uriString) && fuzzy.test(lookFor, uriString)) {
if (location.uri.scheme === 'file' && !alreadyCollected.has(uriString) && fuzzy.test(fileFilter, uriString)) {
const item = this.toItem(location.uri, { groupLabel: recentlyUsedItems.length === 0 ? 'recently opened' : undefined, showBorder: false });
recentlyUsedItems.push(item);
alreadyCollected.add(uriString);
}
}
if (lookFor.length > 0) {
if (fileFilter.length > 0) {
const handler = async (results: string[]) => {
if (token.isCancellationRequested) {
return;
Expand Down Expand Up @@ -205,7 +218,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
acceptor([...recentlyUsedItems, ...sortedResults]);
};

this.fileSearchService.find(lookFor, {
this.fileSearchService.find(fileFilter, {
rootUris: roots.map(r => r.resource.toString()),
fuzzyMatch: true,
limit: 200,
Expand Down Expand Up @@ -254,7 +267,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
}

// Normalize the user query.
const query: string = normalize(this.currentLookFor);
const query: string = normalize(this.filterAndRange.filter);

/**
* Score a given string.
Expand Down Expand Up @@ -347,11 +360,17 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
}

openFile(uri: URI): void {
this.openerService.getOpener(uri)
.then(opener => opener.open(uri))
const options = this.buildOpenerOptions();
const resolvedOpener = this.openerService.getOpener(uri, options);
resolvedOpener
.then(opener => opener.open(uri, options))
.catch(error => this.messageService.error(error));
}

protected buildOpenerOptions(): EditorOpenerOptions {
return { selection: this.filterAndRange.range };
}

private toItem(uriOrString: URI | string, group?: QuickOpenGroupItemOptions): QuickOpenItem<QuickOpenItemOptions> {
const uri = uriOrString instanceof URI ? uriOrString : new URI(uriOrString);
let description = this.labelProvider.getLongName(uri.parent);
Expand Down Expand Up @@ -386,4 +405,36 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
};
return new QuickOpenItem<QuickOpenItemOptions>(options);
}

/**
* Splits the given expression into a structure of search-file-filter and
* location-range.
*
* @param expression patterns of <path><#|:><line><#|:|,><col?>
*/
protected splitFilterAndRange(expression: string): FilterAndRange {
let lineNumber = 0;
let startColumn = 0;

// Find line and column number from the expression using RegExp.
const patternMatch = LINE_COLON_PATTERN.exec(expression);

if (patternMatch) {
const line = parseInt(patternMatch[1] ?? '', 10);
if (Number.isFinite(line)) {
lineNumber = line > 0 ? line - 1 : 0;

const column = parseInt(patternMatch[2] ?? '', 10);
startColumn = Number.isFinite(column) && column > 0 ? column - 1 : 0;
}
}

const position = Position.create(lineNumber, startColumn);
const range = { start: position, end: position };
const fileFilter = patternMatch ? expression.substr(0, patternMatch.index) : expression;
return {
filter: fileFilter,
range
};
}
}