Skip to content

Commit

Permalink
Add search provider for scheme vscode-test-web (#114)
Browse files Browse the repository at this point in the history
* Add search provider for scheme vscode-test-web

* test: add to sample and add test for search

* fix: use extension context as base URI

When using the URIs for requests, for instance for reading contents
of the file, /static/mount had to be removed

* polish

* fix indentation

---------

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
  • Loading branch information
wkillerud and aeschli committed Jan 26, 2024
1 parent 897bca4 commit cca88dc
Show file tree
Hide file tree
Showing 8 changed files with 1,953 additions and 1,740 deletions.
3,499 changes: 1,765 additions & 1,734 deletions fs-provider/package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion fs-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
"Other"
],
"activationEvents": [
"onFileSystem:vscode-test-web"
"onFileSystem:vscode-test-web",
"onSearch:vscode-test-web"
],
"enabledApiProposals": [
"fileSearchProvider"
],
"contributes": {
"resourceLabelFormatters": [
Expand Down
6 changes: 5 additions & 1 deletion fs-provider/src/fsExtensionMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ export function activate(context: ExtensionContext) {
const serverUri = context.extensionUri.with({ path: '/static/mount', query: undefined });
const serverBackedRootDirectory = new ServerBackedDirectory(serverUri, [], '');

const disposable = workspace.registerFileSystemProvider(SCHEME, new MemFileSystemProvider(SCHEME, serverBackedRootDirectory));
const memFsProvider = new MemFileSystemProvider(SCHEME, serverBackedRootDirectory, context.extensionUri);
const disposable = workspace.registerFileSystemProvider(SCHEME, memFsProvider);
context.subscriptions.push(disposable);

const searchDisposable = workspace.registerFileSearchProvider(SCHEME, memFsProvider);
context.subscriptions.push(searchDisposable);

console.log(`vscode-test-web-support fs provider registers for ${SCHEME}, initial content from ${serverUri.toString(/*skipEncoding*/ true)}`);
}

Expand Down
39 changes: 35 additions & 4 deletions fs-provider/src/fsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { EventEmitter, Event, Uri, FileSystemProvider, Disposable, FileType, FileStat, FileSystemError, FileChangeType, FileChangeEvent } from 'vscode';
import { EventEmitter, Event, Uri, FileSystemProvider, Disposable, FileType, FileStat, FileSystemError, FileChangeType, FileChangeEvent, FileSearchQuery, FileSearchOptions, CancellationToken, ProviderResult, FileSearchProvider } from 'vscode';
import { Utils } from 'vscode-uri';
import { Minimatch } from 'minimatch';

export interface File {
readonly type: FileType.File;
Expand All @@ -31,9 +32,8 @@ function modifiedFileStat(stats: FileStat, size?: number): Promise<FileStat> {
return Promise.resolve({ type: stats.type, ctime: stats.ctime, mtime: Date.now(), size: size ?? stats.size });
}

export class MemFileSystemProvider implements FileSystemProvider {

constructor(private readonly scheme: string, private readonly root: Directory) {
export class MemFileSystemProvider implements FileSystemProvider, FileSearchProvider {
constructor(private readonly scheme: string, private readonly root: Directory, private readonly extensionUri: Uri) {
}

// --- manage file metadata
Expand Down Expand Up @@ -137,6 +137,37 @@ export class MemFileSystemProvider implements FileSystemProvider {
this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { type: FileChangeType.Created, uri });
}

// --- search

async provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): Promise<Uri[]> {
const pattern = query.pattern;
// Pattern is always blank: https://github.com/microsoft/vscode/issues/200892
const glob = pattern ? new Minimatch(pattern) : undefined;

const result: Uri[] = [];
const dive = async (currentDirectory: Directory, pathSegments: string[] = []) => {
for (const [name, entry] of await currentDirectory.entries) {
if (typeof options.maxResults !== 'undefined' && result.length >= options.maxResults) {
break;
}

const uri = Uri.joinPath(this.extensionUri, ...pathSegments, entry.name);
if (entry.type === FileType.File) {
const toMatch = uri.toString();
// Pattern is always blank: https://github.com/microsoft/vscode/issues/200892
if (!glob || glob.match(toMatch)) {
result.push(uri);
}
} else if (entry.type === FileType.Directory) {
await dive(entry, [...pathSegments, name]);
}
}
};

await dive(this.root);
return result;
}

// --- lookup

private async _lookup(uri: Uri, silent: false): Promise<Entry>;
Expand Down
70 changes: 70 additions & 0 deletions fs-provider/types/vscode.proposed.fileSearchProvider.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* 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/73524

/**
* The parameters of a query for file search.
*/
export interface FileSearchQuery {
/**
* The search pattern to match against file paths.
*/
pattern: string;
}

/**
* Options that apply to file search.
*/
export interface FileSearchOptions extends SearchOptions {
/**
* The maximum number of results to be returned.
*/
maxResults?: number;

/**
* A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache,
* and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared.
*/
session?: CancellationToken;
}

/**
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions.
*
* A FileSearchProvider is the more powerful of two ways to implement file search in the editor. Use a FileSearchProvider if you wish to search within a folder for
* all files that match the user's query.
*
* The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string,
* and in that case, every file in the folder should be returned.
*/
export interface FileSearchProvider {
/**
* Provide the set of files that match a certain file path pattern.
* @param query The parameters for this query.
* @param options A set of options to consider while searching files.
* @param token A cancellation token.
*/
provideFileSearchResults(
query: FileSearchQuery,
options: FileSearchOptions,
token: CancellationToken
): ProviderResult<Uri[]>;
}

export namespace workspace {
/**
* Register a 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 registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable;
}
}
4 changes: 4 additions & 0 deletions sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
{
"command": "vscode-test-web-sample.helloWorld",
"title": "Hello World"
},
{
"command": "vscode-test-web-sample.findFiles",
"title": "Find files"
}
]
},
Expand Down
17 changes: 17 additions & 0 deletions sample/src/web/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ export function activate(context: vscode.ExtensionContext) {
});

context.subscriptions.push(disposable);

let findFilesDisposable = vscode.commands.registerCommand('vscode-test-web-sample.findFiles', () => {
vscode.window.showInputBox({ title: 'Enter a pattern', placeHolder: '**/*.md' })
.then((pattern) => {
return pattern ? vscode.workspace.findFiles(pattern) : undefined;
})
.then((results) => {
if (!results) {
return vscode.window.showErrorMessage('Find files returned undefined');
}
let summary = `Found:\n${results.map(uri => ` - ${uri.path}`).join('\n')}`;
return vscode.window.showInformationMessage(summary);
});
});

context.subscriptions.push(findFilesDisposable);

}

// this method is called when your extension is deactivated
Expand Down
52 changes: 52 additions & 0 deletions sample/src/web/test/suite/search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Workspace search', () => {
// tests findFiles operation against the current workspace folder
// when running with `@vscode/test-web`, this will be a virtual file system, powered
// by the vscoe-web-test file system provider

const workspaceFolder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
assert.ok(workspaceFolder, 'Expecting an open folder');

const workspaceFolderUri = workspaceFolder.uri;

function getUri(path: string): vscode.Uri {
return vscode.Uri.joinPath(workspaceFolderUri, path);
}

async function assertEntries(path: string, expectedFiles: string[], expectedFolders: string[]) {
const entrySorter = (e1: [string, vscode.FileType], e2: [string, vscode.FileType]) => {
const d = e1[1] - e2[1];
if (d === 0) {
return e1[0].localeCompare(e2[0]);
}
return d;
};

let entries = await vscode.workspace.fs.readDirectory(getUri(path));
entries = entries.sort(entrySorter);

let expected = expectedFolders
.map<[string, vscode.FileType]>((name) => [name, vscode.FileType.Directory])
.concat(expectedFiles.map((name) => [name, vscode.FileType.File]))
.sort(entrySorter);

assert.deepStrictEqual(entries, expected);
}

async function assertFindsFiles(pattern: string, expectedFiles: string[]) {
let entries = await vscode.workspace.findFiles(pattern);
let foundFiles = entries.map((uri) => uri.path.substring(uri.path.lastIndexOf('/') + 1));

assert.deepStrictEqual(foundFiles, expectedFiles);
}

test('Find files', async () => {
await assertEntries('/folder', ['x.txt'], ['.bar']);
await assertEntries('/folder/', ['x.txt'], ['.bar']);
await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']);

await assertFindsFiles('**/*.txt', ['x.txt', 'hello.txt', 'world.txt']);
});
});

0 comments on commit cca88dc

Please sign in to comment.