search provider

This commit is contained in:
Rob Lourens
2024-08-27 12:23:06 -07:00
parent d5cb4fc36b
commit 44e0ccbadf
4 changed files with 339 additions and 1 deletions

View File

@ -16,6 +16,9 @@
"categories": [
"Other"
],
"enabledApiProposals": [
"textSearchProviderNew"
],
"activationEvents": [
"onFileSystem:memfs"
],

View File

@ -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', _ => {

View File

@ -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<vscode.TextSearchResultNew>, token: vscode.CancellationToken): Promise<vscode.TextSearchCompleteNew> {
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<vscode.TextSearchResultNew>): 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<vscode.TextSearchResultNew>) {
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 {

View File

@ -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<TextSearchResultNew>, token: CancellationToken): ProviderResult<TextSearchCompleteNew>;
}
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;
}
}