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

File search go to symbol #3

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions packages/editor/src/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export interface TextEditorDocument extends lsp.TextDocument, Saveable, Disposab
* @since 1.8.0
*/
findMatches?(options: FindMatchesOptions): FindMatch[];

/**
* @since 1.14.0
*/
waitForLanguageSymbolRegistry(): Promise<boolean>;
}

// Refactoring
Expand Down
142 changes: 127 additions & 15 deletions packages/file-search/src/browser/quick-file-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,31 @@ import {
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import URI from '@theia/core/lib/common/uri';
import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service';
import { CancellationTokenSource } from '@theia/core/lib/common';
import { CancellationTokenSource, CommandService } from '@theia/core/lib/common';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { Command } from '@theia/core/lib/common';
import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service';
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, EditorWidget, Position, Range } from '@theia/editor/lib/browser';
import { timeout } from '@theia/core/lib/common/promise-util';

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

export interface FilterQuery {
filter: string;
range: Range;
isBySymbol: boolean;
}

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

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

Expand All @@ -58,6 +69,8 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
protected readonly messageService: MessageService;
@inject(FileSystemPreferences)
protected readonly fsPreferences: FileSystemPreferences;
@inject(CommandService)
protected readonly commandService: CommandService;

/**
* Whether to hide .gitignored (and other ignored) files.
Expand All @@ -69,10 +82,19 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
*/
protected isOpen: boolean = false;

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

/**
* 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: FilterQuery = this.filterAndRangeDefault;

/**
* The score constants when comparing file search results.
Expand All @@ -94,7 +116,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 +148,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 +179,23 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {

const roots = this.workspaceService.tryGetRoots();

this.currentLookFor = lookFor;
this.filterAndRange = this.extractFilterQuery(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) {
let topItem: QuickOpenItem | undefined = undefined;
const handler = async (results: string[]) => {
if (token.isCancellationRequested) {
return;
Expand Down Expand Up @@ -202,18 +227,46 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
sortedResults.unshift(item);
}
// Return the recently used items, followed by the search results.
acceptor([...recentlyUsedItems, ...sortedResults]);
const allItemsSorted = ([...recentlyUsedItems, ...sortedResults]);
if (allItemsSorted.length > 0) {
topItem = allItemsSorted[0];
}
acceptor(allItemsSorted);
};

this.fileSearchService.find(lookFor, {
this.fileSearchService.find(fileFilter, {
rootUris: roots.map(r => r.resource.toString()),
fuzzyMatch: true,
limit: 200,
useGitIgnore: this.hideIgnoredFiles,
excludePatterns: this.hideIgnoredFiles
? Object.keys(this.fsPreferences['files.exclude'])
: undefined,
}, token).then(handler);
}, token).then(handler).then(async () => {
if (topItem && this.filterAndRange.isBySymbol) {
const uri = topItem.getUri();
if (uri) {
acceptor([]);
const editor = await this.openFileAsync(uri);
if (editor) {
editor.activate();
const timeoutValue = 8000;
const result = await Promise.race([
this.waitForLanguageSymbolRegistry(editor),
timeout(timeoutValue)
]);
if (result) {
console.log('Opening quick outline');
this.commandService.executeCommand('editor.action.quickOutline');
} else {
console.log(`Language Symbol Registry not ready after ${timeoutValue} ms`);
}
} else {
console.log('no editor returned');
}
}
}
});
} else {
if (roots.length !== 0) {
acceptor(recentlyUsedItems);
Expand Down Expand Up @@ -254,7 +307,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,13 +400,36 @@ 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));
}

async openFileAsync(uri: URI): Promise<EditorWidget | undefined> {
const options = this.buildOpenerOptions();
const opener = await this.openerService.getOpener(uri, options);
try {
console.log(`opening editor: ${uri.toString()}`);
return await opener.open(uri, options) as EditorWidget;
} catch (e) {
this.messageService.error(e);
}
}

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

protected async waitForLanguageSymbolRegistry(editor: EditorWidget): Promise<boolean> {
const document = editor?.editor?.document;
return await document.waitForLanguageSymbolRegistry();
}

private toItem(uriOrString: URI | string, group?: QuickOpenGroupItemOptions): QuickOpenItem<QuickOpenItemOptions> {
const uri = uriOrString instanceof URI ? uriOrString : new URI(uriOrString);
console.log('=====> toItem uri: ' + uri.toString());
let description = this.labelProvider.getLongName(uri.parent);
if (this.workspaceService.isMultiRootWorkspaceOpened) {
const rootUri = this.workspaceService.getWorkspaceRootUri(uri);
Expand Down Expand Up @@ -386,4 +462,40 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler {
};
return new QuickOpenItem<QuickOpenItemOptions>(options);
}

/**
* Extracts the given expression into a structure of search-file-filter,
* location-range and symbol-filter.
*
* @param fileExpression patterns of <path><#|:><line><#|:|,><col?>
*/
protected extractFilterQuery(filterExpression: string): FilterQuery {
let lineNumber = 0;
let startColumn = 0;
const fileAndSymbolExpressions = filterExpression.split('@', 2);
const isBySymbol = (fileAndSymbolExpressions.length > 1);

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

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 ? fileExpression.substr(0, patternMatch.index) : fileExpression;
return {
filter: fileFilter,
range,
isBySymbol
};
}
}
27 changes: 27 additions & 0 deletions packages/monaco/src/browser/monaco-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,33 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument {
return extractedMatches;
}

async waitForLanguageSymbolRegistry(): Promise<boolean> {

let symbolProviderRegistryPromiseResolve: (res: boolean) => void;
const symbolProviderRegistryPromise = new Promise<boolean>(resolve => symbolProviderRegistryPromiseResolve = resolve);

if (this.textEditorModel) {
const model = this.textEditorModel;
if (monaco.modes.DocumentSymbolProviderRegistry.has(model)) {
return true;
}

// Resolve promise when registry knows model
const symbolProviderListener = monaco.modes.DocumentSymbolProviderRegistry.onDidChange(() => {
if (monaco.modes.DocumentSymbolProviderRegistry.has(model)) {
symbolProviderListener.dispose();

symbolProviderRegistryPromiseResolve(true);
}
});
}

// // Resolve promise when we get disposed too
// disposables.add(toDisposable(() => symbolProviderRegistryPromiseResolve(false)));

return symbolProviderRegistryPromise;
}

async load(): Promise<MonacoEditorModel> {
await this.resolveModel;
return this;
Expand Down