diff --git a/fsprovider-sample/package.json b/fsprovider-sample/package.json index 294c3511..b88c766d 100644 --- a/fsprovider-sample/package.json +++ b/fsprovider-sample/package.json @@ -16,6 +16,9 @@ "categories": [ "Other" ], + "enabledApiProposals": [ + "textSearchProviderNew" + ], "activationEvents": [ "onFileSystem:memfs" ], diff --git a/fsprovider-sample/src/extension.ts b/fsprovider-sample/src/extension.ts index 3a44e3b4..fa91e5bf 100644 --- a/fsprovider-sample/src/extension.ts +++ b/fsprovider-sample/src/extension.ts @@ -1,7 +1,7 @@ 'use strict'; import * as vscode from 'vscode'; -import { MemFS } from './fileSystemProvider'; +import { MemFS, MemFSSearch } from './fileSystemProvider'; export function activate(context: vscode.ExtensionContext) { @@ -9,6 +9,7 @@ export function activate(context: vscode.ExtensionContext) { const memFs = new MemFS(); context.subscriptions.push(vscode.workspace.registerFileSystemProvider('memfs', memFs, { isCaseSensitive: true })); + context.subscriptions.push(vscode.workspace.registerTextSearchProviderNew('memfs', new MemFSSearch(memFs))); let initialized = false; context.subscriptions.push(vscode.commands.registerCommand('memfs.reset', _ => { diff --git a/fsprovider-sample/src/fileSystemProvider.ts b/fsprovider-sample/src/fileSystemProvider.ts index fda95edd..142102e4 100644 --- a/fsprovider-sample/src/fileSystemProvider.ts +++ b/fsprovider-sample/src/fileSystemProvider.ts @@ -46,6 +46,65 @@ export class Directory implements vscode.FileStat { } } +export class MemFSSearch implements vscode.TextSearchProviderNew { + constructor(private readonly memFS: MemFS) { } + + async provideTextSearchResults(query: vscode.TextSearchQueryNew, options: vscode.TextSearchProviderOptions, progress: vscode.Progress, token: vscode.CancellationToken): Promise { + options.folderOptions.forEach(folderQuery => this.searchFolder(folderQuery, query, progress)); + + return { limitHit: false }; + } + + private searchFolder(folderQuery: vscode.TextSearchProviderOptions['folderOptions'][0], query: vscode.TextSearchQueryNew, progress: vscode.Progress): void { + this.memFS.readDirectory(folderQuery.folder).forEach(([name, type]) => { + if (type === vscode.FileType.File) { + const uri = vscode.Uri.joinPath(folderQuery.folder, name); + const data = this.memFS.readFile(uri); + const text = new TextDecoder().decode(data); + this.textSearch(text, uri, query, progress); + } + }); + } + + private textSearch(file: string, fileUri: vscode.Uri, query: vscode.TextSearchQueryNew, progress: vscode.Progress) { + let regPattern = query.isRegExp ? query.pattern : escapeRegExpCharacters(query.pattern); + if (query.isWordMatch) { + regPattern = `\\b${regPattern}\\b`; + } + + const flags: string[] = ['g']; + if (!query.isCaseSensitive) { + flags.push('i'); + } + if (query.isMultiline) { + flags.push('m'); + } + const regExp = new RegExp(regPattern, flags.join('')); + + for (const match of file.matchAll(regExp)) { + const startIdx = match.index; + const endIdx = match[0].length + startIdx; + + // This is really inefficient! + const matchStartLine = file.substring(0, startIdx).split('\n').length - 1; + const matchEndLine = match[0].split('\n').length - 1 + matchStartLine; + const matchStartChar = startIdx - file.substring(0, startIdx).lastIndexOf('\n') - 1; + const matchEndChar = endIdx - file.substring(0, endIdx).lastIndexOf('\n') - 1; + + const searchMatch = new vscode.TextSearchMatchNew(fileUri, [{ + sourceRange: new vscode.Range(new vscode.Position(matchStartLine, matchStartChar), new vscode.Position(matchEndLine, matchEndChar)), + previewRange: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)) // todo + }], match[0]); + console.log(searchMatch); + progress.report(searchMatch); + } + } +} + +function escapeRegExpCharacters(value: string): string { + return value.replace(/[\\{}*+?|^$.[\]()]/g, '\\$&'); +} + export type Entry = File | Directory; export class MemFS implements vscode.FileSystemProvider { diff --git a/fsprovider-sample/vscode.proposed.textSearchProviderNew.d.ts b/fsprovider-sample/vscode.proposed.textSearchProviderNew.d.ts new file mode 100644 index 00000000..a82ca8f4 --- /dev/null +++ b/fsprovider-sample/vscode.proposed.textSearchProviderNew.d.ts @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/59921 + + /** + * The parameters of a query for text search. All optional booleans default to `false`. + */ + export interface TextSearchQueryNew { + /** + * The text pattern to search for. + * + * If explicitly contains a newline character (`\n`), the default search behavior + * will automatically enable {@link isMultiline}. + */ + pattern: string; + + /** + * Whether or not `pattern` should match multiple lines of text. + * + * If using the default search provider, this will be interpreted as `true` if + * `pattern` contains a newline character (`\n`). + */ + isMultiline?: boolean; + + /** + * Whether or not `pattern` should be interpreted as a regular expression. + * + * If using the default search provider, this will be interpreted case-insensitively + * if {@link isCaseSensitive} is `false` or not set. + */ + isRegExp?: boolean; + + /** + * Whether or not the search should be case-sensitive. + * + * If using the default search provider, this can be affected by the `search.smartCase` setting. + * See the setting description for more information. + */ + isCaseSensitive?: boolean; + + /** + * Whether or not to search for whole word matches only. + * + * If enabled, the default search provider will check for boundary characters + * (regex pattern `\b`) surrounding the {@link pattern} to see whether something + * is a word match. + */ + isWordMatch?: boolean; + } + + /** + * Options that apply to text search. + */ + export interface TextSearchProviderOptions { + + folderOptions: { + /** + * The root folder to search within. + */ + folder: Uri; + + /** + * Files that match an `includes` glob pattern should be included in the search. + */ + includes: string[]; + + /** + * Files that match an `excludes` glob pattern should be excluded from the search. + */ + excludes: GlobPattern[]; + + /** + * Whether symlinks should be followed while searching. + * For more info, see the setting description for `search.followSymlinks`. + */ + followSymlinks: boolean; + + /** + * Which file locations we should look for ignore (.gitignore or .ignore) files to respect. + */ + useIgnoreFiles: { + /** + * Use ignore files at the current workspace root. + */ + local: boolean; + /** + * Use ignore files at the parent directory. If set, {@link TextSearchProviderOptions.useIgnoreFiles.local} should also be `true`. + */ + parent: boolean; + /** + * Use global ignore files. If set, {@link TextSearchProviderOptions.useIgnoreFiles.local} should also be `true`. + */ + global: boolean; + }; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding: string; + }[]; + + /** + * The maximum number of results to be returned. + */ + maxResults: number; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions: { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + * Defaults to 100. + */ + matchLines: number; + + /** + * The maximum number of characters included per line. + * Defaults to 10000. + */ + charsPerLine: number; + }; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ + maxFileSize: number | undefined; + + /** + * Number of lines of context to include before and after each match. + */ + surroundingContext: number; + } + + /** + * Information collected when text search is complete. + */ + export interface TextSearchCompleteNew { + /** + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on {@linkcode TextSearchProviderOptions} specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. + */ + limitHit?: boolean; + } + + /** + * A query match instance in a file. + * + * For example, consider this excerpt: + * + * ```ts + * const bar = 1; + * console.log(bar); + * const foo = bar; + * ``` + * + * If the query is `log`, then the line `console.log(bar);` should be represented using a {@link TextSearchMatchNew}. + */ + export class TextSearchMatchNew { + /** + * @param uri The uri for the matching document. + * @param ranges The ranges associated with this match. + * @param previewText The text that is used to preview the match. The highlighted range in `previewText` is specified in `ranges`. + */ + constructor(uri: Uri, ranges: { sourceRange: Range; previewRange: Range }[], previewText: string); + + /** + * The uri for the matching document. + */ + uri: Uri; + + /** + * The ranges associated with this match. + */ + ranges: { + /** + * The range of the match within the document, or multiple ranges for multiple matches. + */ + sourceRange: Range; + /** + * The Range within `previewText` corresponding to the text of the match. + */ + previewRange: Range; + }[]; + + previewText: string; + } + + /** + * The context lines of text that are not a part of a match, + * but that surround a match line of type {@link TextSearchMatchNew}. + * + * For example, consider this excerpt: + * + * ```ts + * const bar = 1; + * console.log(bar); + * const foo = bar; + * ``` + * + * If the query is `log`, then the lines `const bar = 1;` and `const foo = bar;` + * should be represented using two separate {@link TextSearchContextNew} for the search instance. + * This example assumes that the finder requests one line of surrounding context. + */ + export class TextSearchContextNew { + /** + * @param uri The uri for the matching document. + * @param text The line of context text. + * @param lineNumber The line number of this line of context. + */ + constructor(uri: Uri, text: string, lineNumber: number); + + /** + * The uri for the matching document. + */ + uri: Uri; + + /** + * One line of text. + * previewOptions.charsPerLine applies to this + */ + text: string; + + /** + * The line number of this line of context. + */ + lineNumber: number; + } + + /** + * A result payload for a text search, pertaining to {@link TextSearchMatchNew matches} + * and its associated {@link TextSearchContextNew context} within a single file. + */ + export type TextSearchResultNew = TextSearchMatchNew | TextSearchContextNew; + + /** + * A TextSearchProvider provides search results for text results inside files in the workspace. + */ + export interface TextSearchProviderNew { + /** + * WARNING: VERY EXPERIMENTAL. + * + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all {@link TextSearchResultNew results}. + * These results can be direct matches, or context that surrounds matches. + * @param token A cancellation token. + */ + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult; + } + + export namespace workspace { + /** + * Register a text search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerTextSearchProviderNew(scheme: string, provider: TextSearchProviderNew): Disposable; + } +}