deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
// *****************************************************************************
// Copyright (C) 2025 Maksim Kachurin and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { KeybindingContribution } from '@theia/core/lib/browser';
import { FileSearchService } from '../common/file-search-service';
import { FileSearchServiceImpl } from './file-search-service-impl';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { QuickFileOpenFrontendContribution } from '../browser/quick-file-open-contribution';
import { QuickFileOpenService } from '../browser/quick-file-open';
import { QuickFileSelectService } from '../browser/quick-file-select-service';
export default new ContainerModule((bind: interfaces.Bind) => {
bind(FileSearchService).to(FileSearchServiceImpl).inSingletonScope();
bind(QuickFileOpenFrontendContribution).toSelf().inSingletonScope();
[CommandContribution, KeybindingContribution, MenuContribution, QuickAccessContribution].forEach(serviceIdentifier =>
bind(serviceIdentifier).toService(QuickFileOpenFrontendContribution)
);
bind(QuickFileSelectService).toSelf().inSingletonScope();
bind(QuickFileOpenService).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,233 @@
// *****************************************************************************
// Copyright (C) 2025 Maksim Kachurin and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, named } from '@theia/core/shared/inversify';
import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import * as fuzzy from '@theia/core/shared/fuzzy';
import { CancellationTokenSource, CancellationToken, ILogger, URI } from '@theia/core';
import { matchesPattern, createIgnoreMatcher, getIgnorePatterns } from '@theia/filesystem/lib/browser-only/file-search';
@injectable()
export class FileSearchServiceImpl implements FileSearchService {
@inject(ILogger)
@named('file-search')
protected logger: ILogger;
@inject(FileService)
protected readonly fs: FileService;
/**
* Searches for files matching the given pattern.
* @param searchPattern - The pattern to search for
* @param options - Search options including root URIs and filters
* @param clientToken - Optional cancellation token
* @returns Promise resolving to array of matching file URIs
*/
async find(searchPattern: string, options: FileSearchService.Options, clientToken?: CancellationToken): Promise<string[]> {
const cancellationSource = new CancellationTokenSource();
if (clientToken) {
clientToken.onCancellationRequested(() => cancellationSource.cancel());
}
const token = cancellationSource.token;
const opts = {
fuzzyMatch: true,
limit: Number.MAX_SAFE_INTEGER,
useGitIgnore: true,
...options
};
// Merge root-specific options with global options
const roots: FileSearchService.RootOptions = options.rootOptions || {};
if (options.rootUris) {
for (const rootUri of options.rootUris) {
if (!roots[rootUri]) {
roots[rootUri] = {};
}
}
}
// eslint-disable-next-line guard-for-in
for (const rootUri in roots) {
const rootOptions = roots[rootUri];
if (opts.includePatterns) {
const includePatterns = rootOptions.includePatterns || [];
rootOptions.includePatterns = [...includePatterns, ...opts.includePatterns];
}
if (opts.excludePatterns) {
const excludePatterns = rootOptions.excludePatterns || [];
rootOptions.excludePatterns = [...excludePatterns, ...opts.excludePatterns];
}
if (rootOptions.useGitIgnore === undefined) {
rootOptions.useGitIgnore = opts.useGitIgnore;
}
}
const exactMatches = new Set<string>();
const fuzzyMatches = new Set<string>();
// Split search pattern into individual terms for matching
const patterns = searchPattern.toLowerCase().split(WHITESPACE_QUERY_SEPARATOR).map(pattern => pattern.trim()).filter(Boolean);
await Promise.all(Object.keys(roots).map(async root => {
try {
const rootUri = new URI(root);
const rootOptions = roots[root];
await this.doFind(rootUri, rootOptions, (fileUri: string) => {
// Skip already matched files
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
return;
}
// Check for exact pattern matches
const candidatePattern = fileUri.toLowerCase();
const patternExists = patterns.every(pattern => candidatePattern.includes(pattern));
if (patternExists) {
exactMatches.add(fileUri);
} else if (!searchPattern || searchPattern === '*') {
exactMatches.add(fileUri);
} else {
// Check for fuzzy matches if enabled
const fuzzyPatternExists = patterns.every(pattern => fuzzy.test(pattern, candidatePattern));
if (opts.fuzzyMatch && fuzzyPatternExists) {
fuzzyMatches.add(fileUri);
}
}
// Cancel search if limit reached
if ((exactMatches.size + fuzzyMatches.size) >= opts.limit) {
cancellationSource.cancel();
}
}, token);
} catch (e) {
this.logger.error('Failed to search:', root, e);
}
}));
if (clientToken?.isCancellationRequested) {
return [];
}
// Return results up to the specified limit
return [...exactMatches, ...fuzzyMatches].slice(0, opts.limit);
}
/**
* Performs the actual file search within a root directory.
* @param rootUri - The root URI to search in
* @param options - Search options for this root
* @param accept - Callback function for each matching file
* @param token - Cancellation token
*/
protected async doFind(
rootUri: URI,
options: FileSearchService.BaseOptions,
accept: (fileUri: string) => void,
token: CancellationToken
): Promise<void> {
const matcher = createIgnoreMatcher();
const queue: URI[] = [rootUri];
let queueIndex = 0;
while (queueIndex < queue.length) {
if (token.isCancellationRequested) {
return;
}
const currentUri = queue[queueIndex++];
try {
// Skip excluded paths
if (this.shouldExcludePath(currentUri, options.excludePatterns)) {
continue;
}
const stat = await this.fs.resolve(currentUri);
const relPath = currentUri.path.toString().replace(/^\/|^\.\//, '');
// Skip paths ignored by gitignore patterns
if (options.useGitIgnore && relPath && matcher.ignores(relPath)) {
continue;
}
// Accept file if it matches include patterns
if (stat.isFile && this.shouldIncludePath(currentUri, options.includePatterns)) {
accept(currentUri.toString());
} else if (stat.isDirectory && Array.isArray(stat.children)) {
// Process ignore files in directory
if (options.useGitIgnore) {
const patterns = await getIgnorePatterns(
currentUri,
uri => this.fs.read(uri).then(content => content.value)
);
matcher.add(patterns);
}
// Add children to search queue
for (const child of stat.children) {
queue.push(child.resource);
}
}
} catch (e) {
this.logger.error(`Error reading directory: ${currentUri.toString()}`, e);
}
}
}
/**
* Checks if a path should be excluded based on exclude patterns.
* @param uri - The URI to check
* @param excludePatterns - Array of exclude patterns
* @returns True if the path should be excluded
*/
private shouldExcludePath(uri: URI, excludePatterns: string[] | undefined): boolean {
if (!excludePatterns?.length) {
return false;
}
const path = uri.path.toString();
return matchesPattern(path, excludePatterns, {
dot: true,
matchBase: true,
nocase: true
});
}
/**
* Checks if a path should be included based on include patterns.
* @param uri - The URI to check
* @param includePatterns - Array of include patterns
* @returns True if the path should be included
*/
private shouldIncludePath(uri: URI, includePatterns: string[] | undefined): boolean {
if (!includePatterns?.length) {
return true;
}
const path = uri.path.toString();
return matchesPattern(path, includePatterns, {
dot: true,
matchBase: true,
nocase: true
});
}
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { WebSocketConnectionProvider, KeybindingContribution } from '@theia/core/lib/browser';
import { QuickFileOpenFrontendContribution } from './quick-file-open-contribution';
import { QuickFileOpenService } from './quick-file-open';
import { fileSearchServicePath, FileSearchService } from '../common/file-search-service';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { QuickFileSelectService } from './quick-file-select-service';
export default new ContainerModule((bind: interfaces.Bind) => {
bind(FileSearchService).toDynamicValue(ctx => {
const provider = ctx.container.get(WebSocketConnectionProvider);
return provider.createProxy<FileSearchService>(fileSearchServicePath);
}).inSingletonScope();
bind(QuickFileOpenFrontendContribution).toSelf().inSingletonScope();
[CommandContribution, KeybindingContribution, MenuContribution, QuickAccessContribution].forEach(serviceIdentifier =>
bind(serviceIdentifier).toService(QuickFileOpenFrontendContribution)
);
bind(QuickFileSelectService).toSelf().inSingletonScope();
bind(QuickFileOpenService).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { QuickFileOpenService, quickFileOpen } from './quick-file-open';
import { CommandRegistry, CommandContribution, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common';
import { KeybindingRegistry, KeybindingContribution, QuickAccessContribution } from '@theia/core/lib/browser';
import { EditorMainMenu } from '@theia/editor/lib/browser';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class QuickFileOpenFrontendContribution implements QuickAccessContribution, CommandContribution, KeybindingContribution, MenuContribution {
@inject(QuickFileOpenService)
protected readonly quickFileOpenService: QuickFileOpenService;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(quickFileOpen, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute: (...args: any[]) => {
let fileURI: string | undefined;
if (args) {
[fileURI] = args;
}
if (fileURI) {
this.quickFileOpenService.openFile(new URI(fileURI));
} else {
this.quickFileOpenService.open();
}
}
});
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: quickFileOpen.id,
keybinding: 'ctrlcmd+p'
});
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(EditorMainMenu.WORKSPACE_GROUP, {
commandId: quickFileOpen.id,
label: nls.localizeByDefault('Go to File...'),
order: '1',
});
}
registerQuickAccessProvider(): void {
this.quickFileOpenService.registerQuickAccessProvider();
}
}

View File

@@ -0,0 +1,208 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CommonCommands, KeybindingRegistry, OpenerService, QuickAccessProvider, QuickAccessRegistry } from '@theia/core/lib/browser';
import { QuickInputService, QuickPickItem, QuickPicks } from '@theia/core/lib/browser/quick-input/quick-input-service';
import { CancellationToken, Command, nls } from '@theia/core/lib/common';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
import { EditorOpenerOptions, EditorWidget, Position, Range } from '@theia/editor/lib/browser';
import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { QuickFileSelectService } from './quick-file-select-service';
export const quickFileOpen = Command.toDefaultLocalizedCommand({
id: 'file-search.openFile',
category: CommonCommands.FILE_CATEGORY,
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*$/;
export type FileQuickPickItem = QuickPickItem & { uri: URI };
@injectable()
export class QuickFileOpenService implements QuickAccessProvider {
static readonly PREFIX = '';
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(QuickAccessRegistry)
protected readonly quickAccessRegistry: QuickAccessRegistry;
@inject(NavigationLocationService)
protected readonly navigationLocationService: NavigationLocationService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(QuickFileSelectService)
protected readonly quickFileSelectService: QuickFileSelectService;
registerQuickAccessProvider(): void {
this.quickAccessRegistry.registerQuickAccessProvider({
getInstance: () => this,
prefix: QuickFileOpenService.PREFIX,
placeholder: this.getPlaceHolder(),
helpEntries: [{ description: 'Open File', needsEditor: false }]
});
}
/**
* Whether to hide .gitignored (and other ignored) files.
*/
protected hideIgnoredFiles = true;
/**
* Whether the dialog is currently open.
*/
protected isOpen = false;
private updateIsOpen = true;
protected filterAndRangeDefault = { filter: '', range: undefined };
/**
* Tracks the user file search filter and location range e.g. fileFilter:line:column or fileFilter:line,column
*/
protected filterAndRange: FilterAndRange = this.filterAndRangeDefault;
@postConstruct()
protected init(): void {
this.quickInputService?.onHide(() => {
if (this.updateIsOpen) {
this.isOpen = false;
} else {
this.updateIsOpen = true;
}
});
}
isEnabled(): boolean {
return this.workspaceService.opened;
}
open(): void {
// Triggering the keyboard shortcut while the dialog is open toggles
// showing the ignored files.
if (this.isOpen) {
this.hideIgnoredFiles = !this.hideIgnoredFiles;
this.hideQuickPick();
} else {
this.hideIgnoredFiles = true;
this.filterAndRange = this.filterAndRangeDefault;
this.isOpen = true;
}
this.quickInputService?.open(this.filterAndRange.filter);
}
protected hideQuickPick(): void {
this.updateIsOpen = false;
this.quickInputService?.hide();
}
/**
* Get a string (suitable to show to the user) representing the keyboard
* shortcut used to open the quick file open menu.
*/
protected getKeyCommand(): string | undefined {
const keyCommand = this.keybindingRegistry.getKeybindingsForCommand(quickFileOpen.id);
if (keyCommand) {
// We only consider the first keybinding.
const accel = this.keybindingRegistry.acceleratorFor(keyCommand[0], '+');
return accel.join(' ');
}
return undefined;
}
async getPicks(filter: string, token: CancellationToken): Promise<QuickPicks> {
this.filterAndRange = this.splitFilterAndRange(filter);
const fileFilter = this.filterAndRange.filter;
return this.quickFileSelectService.getPicks(fileFilter, token, {
hideIgnoredFiles: this.hideIgnoredFiles,
onSelect: item => this.openFile(item.uri)
},
);
}
openFile(uri: URI): void {
const options = this.buildOpenerOptions();
const closedEditor = this.navigationLocationService.closedEditorsStack.find(editor => editor.uri.path.toString() === uri.path.toString());
this.openerService.getOpener(uri, options)
.then(opener => opener.open(uri, options))
.then(widget => {
// Attempt to restore the editor state if it exists, and no selection is explicitly requested.
if (widget instanceof EditorWidget && closedEditor && !options.selection) {
widget.editor.restoreViewState(closedEditor.viewState);
}
})
.catch(error => {
console.warn(error);
this.messageService.error(nls.localizeByDefault("Unable to open '{0}'", uri.path.toString()));
});
}
protected buildOpenerOptions(): EditorOpenerOptions {
return { selection: this.filterAndRange.range };
}
private getPlaceHolder(): string {
let placeholder = nls.localizeByDefault('Search files by name (append {0} to go to line or {1} to go to symbol)', ':', '@');
const keybinding = this.getKeyCommand();
if (keybinding) {
placeholder += nls.localize('theia/file-search/toggleIgnoredFiles', ' (Press {0} to show/hide ignored files)', keybinding);
}
return placeholder;
}
/**
* 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 filter = expression;
let range = undefined;
// 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)) {
const lineNumber = line > 0 ? line - 1 : 0;
const column = parseInt(patternMatch[2] ?? '', 10);
const startColumn = Number.isFinite(column) && column > 0 ? column - 1 : 0;
const position = Position.create(lineNumber, startColumn);
filter = expression.substring(0, patternMatch.index);
range = Range.create(position, position);
}
}
return { filter, range };
}
}

View File

@@ -0,0 +1,298 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { KeybindingRegistry, OpenerService, QuickAccessRegistry } from '@theia/core/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { findMatches, QuickInputService, QuickPickItem, QuickPicks } from '@theia/core/lib/browser/quick-input/quick-input-service';
import { CancellationToken, nls, PreferenceService, QuickPickSeparator } from '@theia/core/lib/common';
import { MessageService } from '@theia/core/lib/common/message-service';
import URI from '@theia/core/lib/common/uri';
import * as fuzzy from '@theia/core/shared/fuzzy';
import { inject, injectable, optional } from '@theia/core/shared/inversify';
import { Position, Range } from '@theia/editor/lib/browser';
import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/navigation-location-service';
import { FileSystemPreferences } from '@theia/filesystem/lib/common';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service';
export interface FilterAndRange {
filter: string;
range?: Range;
}
export interface QuickFileSelectOptions {
/** Whether to hide .gitignored (and other ignored) files. */
hideIgnoredFiles?: boolean;
/** Executed when the item is selected. */
onSelect?: (item: FileQuickPickItem) => void;
}
// Supports patterns of <path><#|:><line><#|:|,><col?>
const LINE_COLON_PATTERN = /\s?[#:\(](?:line )?(\d*)(?:[#:,](\d*))?\)?\s*$/;
export type FileQuickPickItem = QuickPickItem & { uri: URI };
export namespace FileQuickPickItem {
export function is(obj: QuickPickItem | QuickPickSeparator): obj is FileQuickPickItem {
return obj && 'uri' in obj;
}
}
@injectable()
export class QuickFileSelectService {
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
@inject(QuickAccessRegistry)
protected readonly quickAccessRegistry: QuickAccessRegistry;
@inject(FileSearchService)
protected readonly fileSearchService: FileSearchService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(NavigationLocationService)
protected readonly navigationLocationService: NavigationLocationService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(FileSystemPreferences)
protected readonly fsPreferences: FileSystemPreferences;
@inject(PreferenceService)
protected readonly preferences: PreferenceService;
/**
* The score constants when comparing file search results.
*/
private static readonly Scores = {
max: 1000, // represents the maximum score from fuzzy matching (Infinity).
exact: 500, // represents the score assigned to exact matching.
partial: 250 // represents the score assigned to partial matching.
};
async getPicks(
fileFilter: string = '',
token: CancellationToken = CancellationToken.None,
options: QuickFileSelectOptions = {
hideIgnoredFiles: true
}
): Promise<QuickPicks> {
const roots = this.workspaceService.tryGetRoots();
const alreadyCollected = new Set<string>();
const recentlyUsedItems: QuickPicks = [];
if (this.preferences.get('search.quickOpen.includeHistory')) {
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(fileFilter, uriString)) {
if (recentlyUsedItems.length === 0) {
recentlyUsedItems.push({
type: 'separator',
label: nls.localizeByDefault('recently opened')
});
}
const item = this.toItem(fileFilter, location.uri, options.onSelect);
recentlyUsedItems.push(item);
alreadyCollected.add(uriString);
}
}
}
if (fileFilter.length > 0) {
const handler = async (results: string[]) => {
if (token.isCancellationRequested || results.length <= 0) {
return [];
}
const result = [...recentlyUsedItems];
const fileSearchResultItems: FileQuickPickItem[] = [];
for (const fileUri of results) {
if (!alreadyCollected.has(fileUri)) {
const item = this.toItem(fileFilter, fileUri, options.onSelect);
fileSearchResultItems.push(item);
alreadyCollected.add(fileUri);
}
}
// Create a copy of the file search results and sort.
const sortedResults = fileSearchResultItems.slice();
sortedResults.sort((a, b) => this.compareItems(a, b, fileFilter));
if (sortedResults.length > 0) {
result.push({
type: 'separator',
label: nls.localizeByDefault('file results')
});
result.push(...sortedResults);
}
// Return the recently used items, followed by the search results.
return result;
};
return this.fileSearchService.find(fileFilter, {
rootUris: roots.map(r => r.resource.toString()),
fuzzyMatch: true,
limit: 200,
useGitIgnore: options.hideIgnoredFiles,
excludePatterns: options.hideIgnoredFiles
? Object.keys(this.fsPreferences['files.exclude'])
: undefined,
}, token).then(handler);
} else {
return roots.length !== 0 ? recentlyUsedItems : [];
}
}
protected compareItems(
left: FileQuickPickItem,
right: FileQuickPickItem,
fileFilter: string
): number {
/**
* Score a given string.
*
* @param str the string to score on.
* @returns the score.
*/
function score(str: string | undefined): number {
if (!str) {
return 0;
}
let exactMatch = true;
const partialMatches = querySplit.reduce((matched, part) => {
const partMatches = str.includes(part);
exactMatch = exactMatch && partMatches;
return partMatches ? matched + QuickFileSelectService.Scores.partial : matched;
}, 0);
// Check fuzzy matches.
const fuzzyMatch = fuzzy.match(queryJoin, str) ?? { score: 0 };
if (fuzzyMatch.score === Infinity && exactMatch) {
return Number.MAX_SAFE_INTEGER;
}
return fuzzyMatch.score + partialMatches + (exactMatch ? QuickFileSelectService.Scores.exact : 0);
}
const query: string = normalize(fileFilter);
// Adjust for whitespaces in the query.
const querySplit = query.split(WHITESPACE_QUERY_SEPARATOR);
const queryJoin = querySplit.join('');
const compareByLabelScore = (l: FileQuickPickItem, r: FileQuickPickItem) => score(r.label) - score(l.label);
const compareByLabelIndex = (l: FileQuickPickItem, r: FileQuickPickItem) => r.label.indexOf(query) - l.label.indexOf(query);
const compareByLabel = (l: FileQuickPickItem, r: FileQuickPickItem) => l.label.localeCompare(r.label);
const compareByPathScore = (l: FileQuickPickItem, r: FileQuickPickItem) => score(r.uri.path.toString()) - score(l.uri.path.toString());
const compareByPathIndex = (l: FileQuickPickItem, r: FileQuickPickItem) => r.uri.path.toString().indexOf(query) - l.uri.path.toString().indexOf(query);
const compareByPathLabel = (l: FileQuickPickItem, r: FileQuickPickItem) => l.uri.path.toString().localeCompare(r.uri.path.toString());
return compareWithDiscriminators(left, right, compareByLabelScore, compareByLabelIndex, compareByLabel, compareByPathScore, compareByPathIndex, compareByPathLabel);
}
private toItem(lookFor: string, uriOrString: URI | string, onSelect?: ((item: FileQuickPickItem) => void) | undefined): FileQuickPickItem {
const uri = uriOrString instanceof URI ? uriOrString : new URI(uriOrString);
const label = this.labelProvider.getName(uri);
const description = this.getItemDescription(uri);
const iconClasses = this.getItemIconClasses(uri);
const item = <FileQuickPickItem>{
label,
description,
highlights: {
label: findMatches(label, lookFor),
description: findMatches(description, lookFor)
},
iconClasses,
uri
};
return {
...item,
execute: () => onSelect ? onSelect(item) : undefined
};
}
private getItemIconClasses(uri: URI): string[] | undefined {
const icon = this.labelProvider.getIcon(uri).split(' ').filter(v => v.length > 0);
if (icon.length > 0) {
icon.push('file-icon');
}
return icon;
}
private getItemDescription(uri: URI): string {
return this.labelProvider.getDetails(uri);
}
/**
* 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 filter = expression;
let range = undefined;
// 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)) {
const lineNumber = line > 0 ? line - 1 : 0;
const column = parseInt(patternMatch[2] ?? '', 10);
const startColumn = Number.isFinite(column) && column > 0 ? column - 1 : 0;
const position = Position.create(lineNumber, startColumn);
filter = expression.substring(0, patternMatch.index);
range = Range.create(position, position);
}
}
return { filter, range };
}
}
/**
* Normalize a given string.
*
* @param str the raw string value.
* @returns the normalized string value.
*/
function normalize(str: string): string {
return str.trim().toLowerCase();
}
function compareWithDiscriminators<T>(left: T, right: T, ...discriminators: ((left: T, right: T) => number)[]): number {
let comparisonValue = 0;
let i = 0;
while (comparisonValue === 0 && i < discriminators.length) {
comparisonValue = discriminators[i](left, right);
i++;
}
return comparisonValue;
}

View File

@@ -0,0 +1,52 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CancellationToken } from '@theia/core';
export const fileSearchServicePath = '/services/search';
/**
* The JSON-RPC file search service interface.
*/
export interface FileSearchService {
/**
* finds files by a given search pattern.
* @return the matching file uris
*/
find(searchPattern: string, options: FileSearchService.Options, cancellationToken?: CancellationToken): Promise<string[]>;
}
export const FileSearchService = Symbol('FileSearchService');
export namespace FileSearchService {
export interface BaseOptions {
useGitIgnore?: boolean
includePatterns?: string[]
excludePatterns?: string[]
}
export interface RootOptions {
[rootUri: string]: BaseOptions
}
export interface Options extends BaseOptions {
rootUris?: string[]
rootOptions?: RootOptions
fuzzyMatch?: boolean
limit?: number
}
}
export const WHITESPACE_QUERY_SEPARATOR = /\s+/;

View File

@@ -0,0 +1,30 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common';
import { FileSearchServiceImpl } from './file-search-service-impl';
import { fileSearchServicePath, FileSearchService } from '../common/file-search-service';
export default new ContainerModule(bind => {
bind(FileSearchService).to(FileSearchServiceImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(fileSearchServicePath, () =>
ctx.container.get(FileSearchService)
)
).inSingletonScope();
});

View File

@@ -0,0 +1,230 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { expect } from 'chai';
import * as assert from 'assert';
import * as path from 'path';
import { FileSearchServiceImpl } from './file-search-service-impl';
import { FileUri } from '@theia/core/lib/node';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { CancellationTokenSource } from '@theia/core';
import { bindLogger } from '@theia/core/lib/node/logger-backend-module';
import URI from '@theia/core/lib/common/uri';
import { FileSearchService } from '../common/file-search-service';
import { RawProcessFactory } from '@theia/process/lib/node';
const testContainer = new Container();
bindLogger(testContainer.bind.bind(testContainer));
testContainer.bind(RawProcessFactory).toConstantValue(() => {
throw new Error('should not be used anymore');
});
testContainer.load(new ContainerModule(bind => {
bind(FileSearchServiceImpl).toSelf().inSingletonScope();
}));
describe('search-service', function (): void {
this.timeout(10000);
let service: FileSearchServiceImpl;
beforeEach(() => {
service = testContainer.get(FileSearchServiceImpl);
});
it('should fuzzy search this spec file', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '..')).toString();
const matches = await service.find('spc', { rootUris: [rootUri] });
const expectedFile = FileUri.create(__filename).path.base;
const testFile = matches.find(e => e.endsWith(expectedFile));
expect(testFile).to.not.be.undefined;
});
it.skip('should respect nested .gitignore', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString();
const matches = await service.find('foo', { rootUris: [rootUri], fuzzyMatch: false });
expect(matches.find(match => match.endsWith('subdir1/sub-bar/foo.txt'))).to.be.undefined;
expect(matches.find(match => match.endsWith('subdir1/sub2/foo.txt'))).to.not.be.undefined;
expect(matches.find(match => match.endsWith('subdir1/foo.txt'))).to.not.be.undefined;
});
it('should cancel searches', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../../../..')).toString();
const cancelTokenSource = new CancellationTokenSource();
cancelTokenSource.cancel();
const matches = await service.find('foo', { rootUris: [rootUri], fuzzyMatch: false }, cancelTokenSource.token);
expect(matches).to.be.empty;
});
it('should perform file search across all folders in the workspace', async () => {
const dirA = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub-bar')).toString();
const dirB = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub2')).toString();
const matches = await service.find('foo', { rootUris: [dirA, dirB] });
expect(matches).to.not.be.undefined;
expect(matches.length).to.eq(2);
});
describe('search with glob', () => {
it('should support file searches with globs', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub2')).toString();
const matches = await service.find('', { rootUris: [rootUri], includePatterns: ['**/*oo.*'] });
expect(matches).to.not.be.undefined;
expect(matches.length).to.eq(1);
});
it('should NOT support file searches with globs without the prefixed or trailing star (*)', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub2')).toString();
const trailingMatches = await service.find('', { rootUris: [rootUri], includePatterns: ['*oo'] });
expect(trailingMatches).to.not.be.undefined;
expect(trailingMatches.length).to.eq(0);
const prefixedMatches = await service.find('', { rootUris: [rootUri], includePatterns: ['oo*'] });
expect(prefixedMatches).to.not.be.undefined;
expect(prefixedMatches.length).to.eq(0);
});
});
describe('search with ignored patterns', () => {
it('should NOT ignore strings passed through the search options', async () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub2')).toString();
const matches = await service.find('', { rootUris: [rootUri], includePatterns: ['**/*oo.*'], excludePatterns: ['foo'] });
expect(matches).to.not.be.undefined;
expect(matches.length).to.eq(1);
});
const ignoreGlobsUri = FileUri.create(path.resolve(__dirname, '../../test-resources/subdir1/sub2')).toString();
it('should ignore globs passed through the search options #1', () => assertIgnoreGlobs({
rootUris: [ignoreGlobsUri],
includePatterns: ['**/*oo.*'],
excludePatterns: ['*fo*']
}));
it('should ignore globs passed through the search options #2', () => assertIgnoreGlobs({
rootOptions: {
[ignoreGlobsUri]: {
includePatterns: ['**/*oo.*'],
excludePatterns: ['*fo*']
}
}
}));
it('should ignore globs passed through the search options #3', () => assertIgnoreGlobs({
rootOptions: {
[ignoreGlobsUri]: {
includePatterns: ['**/*oo.*']
}
},
excludePatterns: ['*fo*']
}));
it('should ignore globs passed through the search options #4', () => assertIgnoreGlobs({
rootOptions: {
[ignoreGlobsUri]: {
excludePatterns: ['*fo*']
}
},
includePatterns: ['**/*oo.*']
}));
it('should ignore globs passed through the search options #5', () => assertIgnoreGlobs({
rootOptions: {
[ignoreGlobsUri]: {}
},
excludePatterns: ['*fo*'],
includePatterns: ['**/*oo.*']
}));
async function assertIgnoreGlobs(options: FileSearchService.Options): Promise<void> {
const matches = await service.find('', options);
expect(matches).to.not.be.undefined;
expect(matches.length).to.eq(0);
}
});
describe('irrelevant absolute results', () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../../..'));
it('not fuzzy', async () => {
const searchPattern = 'package'; // package.json should produce a result.
const matches = await service.find(searchPattern, { rootUris: [rootUri.toString()], fuzzyMatch: false, useGitIgnore: true, limit: 200 });
expect(matches).not.empty;
for (const match of matches) {
const relativeUri = rootUri.relative(new URI(match));
assert.notStrictEqual(relativeUri, undefined);
const relativeMatch = relativeUri!.toString();
assert.notStrictEqual(relativeMatch.indexOf(searchPattern), -1, relativeMatch);
}
});
it('fuzzy', async () => {
const matches = await service.find('shell', { rootUris: [rootUri.toString()], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
expect(matches).not.empty;
for (const match of matches) {
const relativeUri = rootUri.relative(new URI(match));
assert.notStrictEqual(relativeUri, undefined);
const relativeMatch = relativeUri!.toString();
let position = 0;
for (const ch of 'shell') {
position = relativeMatch.toLowerCase().indexOf(ch, position);
assert.notStrictEqual(position, -1, `character "${ch}" not found in "${relativeMatch}"`);
}
}
});
it('should not look into .git', async () => {
const matches = await service.find('master', { rootUris: [rootUri.toString()], fuzzyMatch: false, useGitIgnore: true, limit: 200 });
// `**/.git/refs/remotes/*/master` files should not be picked up
assert.deepStrictEqual([], matches);
});
});
describe('search with whitespaces', () => {
const rootUri = FileUri.create(path.resolve(__dirname, '../../test-resources')).toString();
it('should support file searches with whitespaces', async () => {
const matches = await service.find('foo sub', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
expect(matches).to.be.length(2);
expect(matches[0].endsWith('subdir1/sub-bar/foo.txt'));
expect(matches[1].endsWith('subdir1/sub2/foo.txt'));
});
it('should support fuzzy file searches with whitespaces', async () => {
const matchesExact = await service.find('foo sbd2', { rootUris: [rootUri], fuzzyMatch: false, useGitIgnore: true, limit: 200 });
const matchesFuzzy = await service.find('foo sbd2', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
expect(matchesExact).to.be.length(0);
expect(matchesFuzzy).to.be.length(1);
expect(matchesFuzzy[0].endsWith('subdir1/sub2/foo.txt'));
});
it('should support file searches with whitespaces regardless of order', async () => {
const matchesA = await service.find('foo sub', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
const matchesB = await service.find('sub foo', { rootUris: [rootUri], fuzzyMatch: true, useGitIgnore: true, limit: 200 });
expect(matchesA).to.not.be.empty;
expect(matchesB).to.not.be.empty;
expect(matchesA.length).to.equal(matchesB.length);
// Due to ripgrep parallelism we cannot deepEqual the matches since order is not guaranteed.
expect(matchesA).to.have.members(matchesB);
});
});
});

View File

@@ -0,0 +1,187 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as cp from 'child_process';
import * as fuzzy from '@theia/core/shared/fuzzy';
import * as readline from 'readline';
import { rgPath } from '@vscode/ripgrep';
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { CancellationTokenSource, CancellationToken, ILogger, isWindows } from '@theia/core';
import { RawProcessFactory } from '@theia/process/lib/node';
import { FileSearchService, WHITESPACE_QUERY_SEPARATOR } from '../common/file-search-service';
import * as path from 'path';
@injectable()
export class FileSearchServiceImpl implements FileSearchService {
constructor(
@inject(ILogger) protected readonly logger: ILogger,
/** @deprecated since 1.7.0 */
@inject(RawProcessFactory) protected readonly rawProcessFactory: RawProcessFactory,
) { }
async find(searchPattern: string, options: FileSearchService.Options, clientToken?: CancellationToken): Promise<string[]> {
const cancellationSource = new CancellationTokenSource();
if (clientToken) {
clientToken.onCancellationRequested(() => cancellationSource.cancel());
}
const token = cancellationSource.token;
const opts = {
fuzzyMatch: true,
limit: Number.MAX_SAFE_INTEGER,
useGitIgnore: true,
...options
};
const roots: FileSearchService.RootOptions = options.rootOptions || {};
if (options.rootUris) {
for (const rootUri of options.rootUris) {
if (!roots[rootUri]) {
roots[rootUri] = {};
}
}
}
// eslint-disable-next-line guard-for-in
for (const rootUri in roots) {
const rootOptions = roots[rootUri];
if (opts.includePatterns) {
const includePatterns = rootOptions.includePatterns || [];
rootOptions.includePatterns = [...includePatterns, ...opts.includePatterns];
}
if (opts.excludePatterns) {
const excludePatterns = rootOptions.excludePatterns || [];
rootOptions.excludePatterns = [...excludePatterns, ...opts.excludePatterns];
}
if (rootOptions.useGitIgnore === undefined) {
rootOptions.useGitIgnore = opts.useGitIgnore;
}
}
const exactMatches = new Set<string>();
const fuzzyMatches = new Set<string>();
if (isWindows) {
// Allow users on Windows to search for paths using either forwards or backwards slash
searchPattern = searchPattern.replace(/\//g, '\\');
}
const patterns = searchPattern.toLocaleLowerCase().trim().split(WHITESPACE_QUERY_SEPARATOR);
await Promise.all(Object.keys(roots).map(async root => {
try {
const rootUri = new URI(root);
const rootPath = FileUri.fsPath(rootUri);
const rootOptions = roots[root];
await this.doFind(rootUri, rootOptions, candidate => {
// Convert OS-native candidate path to a file URI string
const fileUri = FileUri.create(path.resolve(rootPath, candidate)).toString();
// Skip results that have already been matched.
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
return;
}
// Determine if the candidate matches any of the patterns exactly or fuzzy
const candidatePattern = candidate.toLocaleLowerCase();
const patternExists = patterns.every(pattern => candidatePattern.indexOf(pattern) !== -1);
if (patternExists) {
exactMatches.add(fileUri);
} else if (!searchPattern || searchPattern === '*') {
exactMatches.add(fileUri);
} else {
const fuzzyPatternExists = patterns.every(pattern => fuzzy.test(pattern, candidate));
if (opts.fuzzyMatch && fuzzyPatternExists) {
fuzzyMatches.add(fileUri);
}
}
// Preemptively terminate the search when the list of exact matches reaches the limit.
if (exactMatches.size === opts.limit) {
cancellationSource.cancel();
}
}, token);
} catch (e) {
console.error('Failed to search:', root, e);
}
}));
if (clientToken && clientToken.isCancellationRequested) {
return [];
}
// Return the list of results limited by the search limit.
return [...exactMatches, ...fuzzyMatches].slice(0, opts.limit);
}
protected doFind(rootUri: URI, options: FileSearchService.BaseOptions, accept: (fileUri: string) => void, token: CancellationToken): Promise<void> {
return new Promise((resolve, reject) => {
const cwd = FileUri.fsPath(rootUri);
const args = this.getSearchArgs(options);
const ripgrep = cp.spawn(rgPath, args, { cwd });
ripgrep.on('error', reject);
ripgrep.on('exit', (code, signal) => {
if (typeof code === 'number' && code !== 0) {
reject(new Error(`"${rgPath}" exited with code: ${code}`));
} else if (typeof signal === 'string') {
reject(new Error(`"${rgPath}" was terminated by signal: ${signal}`));
}
});
token.onCancellationRequested(() => {
ripgrep.kill(); // most likely sends a signal.
resolve(); // avoid rejecting for no good reason.
});
const lineReader = readline.createInterface({
input: ripgrep.stdout,
crlfDelay: Infinity,
});
lineReader.on('line', line => {
if (!token.isCancellationRequested) {
accept(line);
}
});
lineReader.on('close', () => resolve());
});
}
protected getSearchArgs(options: FileSearchService.BaseOptions): string[] {
const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git', '--no-config'];
if (options.includePatterns) {
for (const includePattern of options.includePatterns) {
if (includePattern) {
args.push('--glob', includePattern);
}
}
}
if (options.excludePatterns) {
for (const excludePattern of options.excludePatterns) {
if (excludePattern) {
args.push('--glob', `!${excludePattern}`);
}
}
}
if (options.useGitIgnore) {
// ripgrep follows `.gitignore` by default, but it doesn't exclude `.git`:
args.push('--glob', '!.git');
} else {
args.push('--no-ignore');
}
return args;
}
}