mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-06-13 07:10:26 +08:00
search provider
This commit is contained in:
@ -16,6 +16,9 @@
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"textSearchProviderNew"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onFileSystem:memfs"
|
||||
],
|
||||
|
||||
@ -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', _ => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
275
fsprovider-sample/vscode.proposed.textSearchProviderNew.d.ts
vendored
Normal file
275
fsprovider-sample/vscode.proposed.textSearchProviderNew.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user