Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions src/vs/base/common/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
...options,
equals: ignoreCase ? equalsIgnoreCase : (a: string, b: string) => a === b,
endsWith: ignoreCase ? endsWithIgnoreCase : (str: string, candidate: string) => str.endsWith(candidate),
// TODO: the '!isLinux' part below is to keep current behavior unchanged, but it should probably be removed
// in favor of passing correct options from the caller.
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, !isLinux || ignoreCase)
isEqualOrParent: (base: string, candidate: string) => isEqualOrParent(base, candidate, options.ignoreCase ?? !isLinux /* preserve old behaviour for when option is not adopted */)
};

// Check cache
Expand All @@ -371,13 +369,13 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
let match: RegExpExecArray | null;
if (T1.test(pattern)) {
parsedPattern = trivia1(pattern.substring(4), pattern, internalOptions); // common pattern: **/*.txt just need endsWith check
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
} else if (match = T2.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/some.txt just need basename check
parsedPattern = trivia2(match[1], pattern, internalOptions);
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
parsedPattern = trivia3(pattern, internalOptions);
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
} else if (match = T4.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: **/something/else just need endsWith check
parsedPattern = trivia4and5(match[1].substring(1), pattern, true, internalOptions);
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
} else if (match = T5.exec(trimForExclusions(pattern, internalOptions))) { // common pattern: something/else just need equals check
parsedPattern = trivia4and5(match[1], pattern, false, internalOptions);
}

Expand Down
58 changes: 38 additions & 20 deletions src/vs/workbench/api/common/extHostFileSystemEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Emitter, Event, AsyncEmitter, IWaitUntil, IWaitUntilData } from '../../../base/common/event.js';
import { GLOBSTAR, GLOB_SPLIT, IRelativePattern, parse } from '../../../base/common/glob.js';
import { URI } from '../../../base/common/uri.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
import type * as vscode from 'vscode';
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation, MainContext, IRelativePatternDto } from './extHost.protocol.js';
Expand Down Expand Up @@ -52,7 +52,7 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
return Boolean(this._config & 0b100);
}

constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, fileSystemInfo: ExtHostFileSystemInfo, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<FileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {
constructor(mainContext: IMainContext, configuration: ExtHostConfigProvider, fileSystemInfo: ExtHostFileSystemInfo, workspace: IExtHostWorkspace, extension: IExtensionDescription, dispatcher: Event<LazyRevivedFileSystemEvents>, globPattern: string | IRelativePatternDto, options: FileSystemWatcherCreateOptions) {
this._config = 0;
if (options.ignoreCreateEvents) {
this._config += 0b001;
Expand All @@ -68,7 +68,18 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
!((fileSystemInfo.getCapabilities(Schemas.file) ?? 0) & FileSystemProviderCapabilities.PathCaseSensitive) :
fileSystemInfo.extUri.ignorePathCasing(URI.revive(globPattern.baseUri));

const parsedPattern = parse(globPattern, { ignoreCase });
// Performance: pre-lowercase pattern and paths to use fast case-sensitive
// matching instead of repeated case-insensitive comparisons in the hot loop.
// By normalizing to lowercase upfront, we enforce `ignoreCase: false` so the
// glob parser uses strict `===` / `endsWith` instead of character-by-character
// case-folding on every comparison.
let matchGlob: string | IRelativePattern = globPattern;
if (ignoreCase) {
matchGlob = typeof globPattern === 'string'
? globPattern.toLowerCase()
: { base: globPattern.base.toLowerCase(), pattern: globPattern.pattern.toLowerCase() };
}
const parsedPattern = parse(matchGlob, { ignoreCase: false /* speeds up matching, but requires us to lowercase paths and patterns */ });

// 1.64.x behavior change: given the new support to watch any folder
// we start to ignore events outside the workspace when only a string
Expand All @@ -95,25 +106,22 @@ class FileSystemWatcher implements vscode.FileSystemWatcher {
}

if (!options.ignoreCreateEvents) {
for (const created of events.created) {
const uri = URI.revive(created);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
for (const { uri, lowerCaseFsPath } of events.created) {
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidCreate.fire(uri);
}
}
}
if (!options.ignoreChangeEvents) {
for (const changed of events.changed) {
const uri = URI.revive(changed);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
for (const { uri, lowerCaseFsPath } of events.changed) {
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidChange.fire(uri);
}
}
}
if (!options.ignoreDeleteEvents) {
for (const deleted of events.deleted) {
const uri = URI.revive(deleted);
if (parsedPattern(uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
for (const { uri, lowerCaseFsPath } of events.deleted) {
if (parsedPattern(ignoreCase ? lowerCaseFsPath : uri.fsPath) && (!excludeOutOfWorkspaceEvents || workspace.getWorkspaceFolder(uri))) {
this._onDidDelete.fire(uri);
}
}
Expand Down Expand Up @@ -247,18 +255,28 @@ interface IExtensionListener<E> {
(e: E): any;
}

class LazyRevivedFileSystemEvents implements FileSystemEvents {
interface RevivedFileSystemEvent {
readonly uri: URI;
readonly lowerCaseFsPath: string;
}

class LazyRevivedFileSystemEvents {

readonly session: number | undefined;

private _created = new Lazy(() => this._events.created.map(URI.revive) as URI[]);
get created(): URI[] { return this._created.value; }
private _created = new Lazy(() => this._events.created.map(LazyRevivedFileSystemEvents._revive));
get created(): RevivedFileSystemEvent[] { return this._created.value; }

private _changed = new Lazy(() => this._events.changed.map(URI.revive) as URI[]);
get changed(): URI[] { return this._changed.value; }
private _changed = new Lazy(() => this._events.changed.map(LazyRevivedFileSystemEvents._revive));
get changed(): RevivedFileSystemEvent[] { return this._changed.value; }

private _deleted = new Lazy(() => this._events.deleted.map(URI.revive) as URI[]);
get deleted(): URI[] { return this._deleted.value; }
private _deleted = new Lazy(() => this._events.deleted.map(LazyRevivedFileSystemEvents._revive));
get deleted(): RevivedFileSystemEvent[] { return this._deleted.value; }

private static _revive(uriComponents: UriComponents): RevivedFileSystemEvent {
const uri = URI.revive(uriComponents);
return { uri, lowerCaseFsPath: uri.fsPath.toLowerCase() };
}

constructor(private readonly _events: FileSystemEvents) {
this.session = this._events.session;
Expand All @@ -267,7 +285,7 @@ class LazyRevivedFileSystemEvents implements FileSystemEvents {

export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape {

private readonly _onFileSystemEvent = new Emitter<FileSystemEvents>();
private readonly _onFileSystemEvent = new Emitter<LazyRevivedFileSystemEvents>();

private readonly _onDidRenameFile = new Emitter<vscode.FileRenameEvent>();
private readonly _onDidCreateFile = new Emitter<vscode.FileCreateEvent>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,34 @@ import { IMainContext } from '../../common/extHost.protocol.js';
import { NullLogService } from '../../../../platform/log/common/log.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { ExtHostFileSystemInfo } from '../../common/extHostFileSystemInfo.js';
import { URI } from '../../../../base/common/uri.js';
import { FileSystemProviderCapabilities } from '../../../../platform/files/common/files.js';
import { IExtHostWorkspace } from '../../common/extHostWorkspace.js';
import { RelativePattern } from '../../common/extHostTypes.js';
import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';
import { ExtHostConfigProvider } from '../../common/extHostConfiguration.js';

suite('ExtHostFileSystemEventService', () => {

ensureNoDisposablesAreLeakedInTestSuite();

test('FileSystemWatcher ignore events properties are reversed #26851', function () {
const protocol: IMainContext = {
getProxy: () => { return undefined!; },
set: undefined!,
dispose: undefined!,
assertRegistered: undefined!,
drain: undefined!
};

const protocol: IMainContext = {
getProxy: () => { return undefined!; },
set: undefined!,
dispose: undefined!,
assertRegistered: undefined!,
drain: undefined!
};
const protocolWithProxy: IMainContext = {
getProxy: () => ({ $watch() { }, $unwatch() { }, dispose() { } }) as never,
set: undefined!,
dispose: undefined!,
assertRegistered: undefined!,
drain: undefined!
};

test('FileSystemWatcher ignore events properties are reversed #26851', function () {

const fileSystemInfo = new ExtHostFileSystemInfo();

Expand All @@ -38,4 +52,102 @@ suite('ExtHostFileSystemEventService', () => {
watcher2.dispose();
});

test('FileSystemWatcher matches case-insensitively via pre-lowercasing', function () {
const fileSystemInfo = new ExtHostFileSystemInfo();
// Default: no PathCaseSensitive capability → ignoreCase=true for string patterns

const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
};

const service = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!);
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, undefined!, fileSystemInfo, undefined!, '**/*.TXT', {});

const created: URI[] = [];
const sub = watcher.onDidCreate(uri => created.push(uri));

// lowercase path should match uppercase pattern on case-insensitive fs
service.$onFileEvent({
session: undefined,
created: [URI.file('/workspace/file.txt')],
changed: [],
deleted: []
});

assert.strictEqual(created.length, 1);

sub.dispose();
watcher.dispose();
});

test('FileSystemWatcher matches case-sensitively when PathCaseSensitive', function () {
const fileSystemInfo = new ExtHostFileSystemInfo();
fileSystemInfo.$acceptProviderInfos(URI.file('/'), FileSystemProviderCapabilities.PathCaseSensitive);

const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
};

const service = new ExtHostFileSystemEventService(protocol, new NullLogService(), undefined!);
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, undefined!, fileSystemInfo, undefined!, '**/*.TXT', {});

const created: URI[] = [];
const sub = watcher.onDidCreate(uri => created.push(uri));

// lowercase path should NOT match uppercase pattern on case-sensitive fs
service.$onFileEvent({
session: undefined,
created: [URI.file('/workspace/file.txt')],
changed: [],
deleted: []
});

assert.strictEqual(created.length, 0);

// uppercase path SHOULD match
service.$onFileEvent({
session: undefined,
created: [URI.file('/workspace/file.TXT')],
changed: [],
deleted: []
});

assert.strictEqual(created.length, 1);

sub.dispose();
watcher.dispose();
});

test('FileSystemWatcher matches relative pattern case-insensitively via pre-lowercasing', function () {
const fileSystemInfo = new ExtHostFileSystemInfo();
fileSystemInfo.$acceptProviderInfos(URI.file('/'), FileSystemProviderCapabilities.FileReadWrite); // no PathCaseSensitive → ignoreCase=true

const workspace: Pick<IExtHostWorkspace, 'getWorkspaceFolder'> = {
getWorkspaceFolder: () => ({ uri: URI.file('/workspace'), name: 'test', index: 0 })
};

const configProvider = {
getConfiguration: () => ({ get: () => ({}) })
} as unknown as ExtHostConfigProvider;

const service = new ExtHostFileSystemEventService(protocolWithProxy, new NullLogService(), undefined!);
const watcher = service.createFileSystemWatcher(workspace as IExtHostWorkspace, configProvider, fileSystemInfo, nullExtensionDescription, new RelativePattern('/Workspace', '**/*.TXT'), {});

const created: URI[] = [];
const sub = watcher.onDidCreate(uri => created.push(uri));

// lowercase path should match mixed-case base + uppercase extension on case-insensitive fs
service.$onFileEvent({
session: undefined,
created: [URI.file('/workspace/file.txt')],
changed: [],
deleted: []
});

assert.strictEqual(created.length, 1);

sub.dispose();
watcher.dispose();
});

});
Loading