microsoft / microsoft/vscode

`workspaceContains` glob activation events are silently defeated by default `files.exclude` (`**/.git`, `**/.svn`, `**/.hg`), so extensions cannot activate on descendant SCM metadata

Open
#326,423 4 comments 0 reactions 0 assignees View on GitHub
extension-activation feature-request
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

- VS Code Version: 1.129.0 (commit `125df4672b8a6a34975303c6b0baa124e560a4f7`)
- OS Version: Windows 11 Pro 10.0.26200
- Does this issue occur when all extensions are disabled: N/A (behavior of extension activation itself; reproduced with a minimal probe extension in an isolated `--user-data-dir`/`--extensions-dir`, default settings)

### Summary

Any `workspaceContains` activation event containing `*` or `?` is routed through file search, and that search applies the configured excludes — including the *default* `files.exclude` entries `**/.git`, `**/.svn`, `**/.hg`. As a result, a glob activation event that targets SCM metadata (for example `workspaceContains:**/.svn/wc.db` for a Subversion extension) can never fire, even when the file demonstrably exists in the workspace. Exact (glob-free) patterns use a separate `exists()` branch and are unaffected — but exact patterns cannot express "a working copy in some child directory whose name I don't know".

The practical consequence: an SCM extension for Subversion/Mercurial (or anything keyed off dot-directory metadata) has no `workspaceContains` form that activates when the working copy is a *descendant* of the opened folder. The only workarounds are `onStartupFinished`/`*` (activating in every workspace, which `workspaceContains` exists to avoid) or asking the user to open the working-copy root.

This is silent: nothing in the extension host log indicates the pattern was suppressed by an exclude; the extension simply never activates.

### Minimal repro

Probe extension (plain JS, no build step) — one activation event per extension so the fired event is unambiguous; on activation it writes a marker file:

```json
{
"name": "svnprobe-glob-file",
"publisher": "svnprobe",
"version": "0.0.1",
"engines": { "vscode": "^1.85.0" },
"main": "./extension.js",
"activationEvents": ["workspaceContains:**/.svn/wc.db"],
"capabilities": { "untrustedWorkspaces": { "supported": true } }
}
```

Fixtures (file contents irrelevant; existence matters):

```
wc-root/ .svn/wc.db # working copy at workspace root
parent-ws/ child/.svn/wc.db # working copy in a child dir (the real-world case)
no-svn/ plain.txt # negative control
```

Steps:
1. Install probes into an isolated instance: `code --user-data-dir --extensions-dir --install-extension `.
2. Open each fixture folder; wait past the workspaceContains window; check markers.

### Observed (3 identical runs, isolated instance, default settings)

| activation event | wc-root (`.svn/wc.db` at root) | parent-ws (`child/.svn/wc.db`) | no-svn |
|---|---|---|---|
| `workspaceContains:.svn/wc.db` (exact) | activates | no (correct: path not at root) | no |
| `workspaceContains:**/.svn/wc.db` | **no** | **no** | no |
| `workspaceContains:**/.svn` | no | no | no |
| `workspaceContains:*/.svn` | no | no | no |
| `workspaceContains:*/*/.svn` | no | no | no |

Note the wc-root row: the exact pattern and the `**/` glob describe the *same existing file*, in the same launch, and only the exact one fires.

Public-API confirmation from inside the activated probe (same workspace, `child/.svn/wc.db` present):

```js
await vscode.workspace.findFiles('**/.svn/wc.db', undefined, 10) // [] — default excludes applied
await vscode.workspace.findFiles('**/.svn/wc.db', null, 10) // [ '/.svn/wc.db', '/child/.svn/wc.db' ]
```

### Source analysis (at `125df467`)

1. Pattern routing — anything with `*`/`?` goes to search, not `exists()`:
[`src/vs/workbench/services/extensions/common/workspaceContains.ts#L42-L49`](https://github.com/microsoft/vscode/blob/125df4672b8a6a34975303c6b0baa124e560a4f7/src/vs/workbench/services/extensions/common/workspaceContains.ts#L42-L49)
```ts
if (fileNameOrGlob.indexOf('*') >= 0 || fileNameOrGlob.indexOf('?') >= 0 || host.forceUsingSearch) {
globPatterns.push(fileNameOrGlob);
} else {
fileNames.push(fileNameOrGlob);
}
```
Exact names use `host.exists(...)` in `_activateIfFileName` (L71-L79), which never consults excludes.

2. The glob branch builds an ordinary file-search query **without** `disregardExcludeSettings`:
[`workspaceContains.ts#L113-L128`](https://github.com/microsoft/vscode/blob/125df4672b8a6a34975303c6b0baa124e560a4f7/src/vs/workbench/services/extensions/common/workspaceContains.ts#L113-L128)
```ts
const query = queryBuilder.file(folders.map(...), {
_reason: 'checkExists',
includePattern: includes,
exists: true
});
```

3. QueryBuilder therefore folds in configured excludes:
[`src/vs/workbench/services/search/common/queryBuilder.ts#L428-L431`](https://github.com/microsoft/vscode/blob/125df4672b8a6a34975303c6b0baa124e560a4f7/src/vs/workbench/services/search/common/queryBuilder.ts#L428-L431)
```ts
private getExcludesForFolder(folderConfig: ISearchConfiguration, options: ICommonQueryBuilderOptions): glob.IExpression | undefined {
return options.disregardExcludeSettings ?
undefined :
getExcludes(folderConfig, !options.disregardSearchExcludeSettings);
}
```

4. And `files.exclude` *defaults* to excluding SCM metadata everywhere:
[`src/vs/workbench/contrib/files/browser/files.contribution.ts#L153-L158`](https://github.com/microsoft/vscode/blob/125df4672b8a6a34975303c6b0baa124e560a4f7/src/vs/workbench/contrib/files/browser/files.contribution.ts#L153-L158)
```ts
'default': {
...{ '**/.git': true, '**/.svn': true, '**/.hg': true, '**/.DS_Store': true, '**/Thumbs.db': true },
```

Hidden-file handling is not the blocker: the ripgrep provider passes `--hidden` ([`ripgrepFileSearch.ts#L31`](https://github.com/microsoft/vscode/blob/125df4672b8a6a34975303c6b0baa124e560a4f7/src/vs/workbench/services/search/node/ripgrepFileSearch.ts#L31)).

### Expected

`workspaceContains` is documented as activating "whenever a folder is opened that contains at least one file that matches [the] glob pattern". Activation is a statement about workspace *content*; `files.exclude` is a *display/search* filter. A user hiding `.svn` from the Explorer does not intend to disable their Subversion extension — and here it is not even a user choice, it is the product default, so the manifest surface (`workspaceContains:**/.svn/...`) is dead on arrival for every user.

### Suggested fix direction

Have `checkGlobFileExists` build its query with `disregardExcludeSettings: true` (and `disregardIgnoreFiles: true`), matching the semantics of the exact-path branch, which already ignores excludes.

If that is considered too costly — excludes were deliberately applied to these searches for performance in #34711 (avoiding `node_modules`/`.git` walks) — a bounded middle ground is the one already suggested in #34711's discussion: only disregard an exclude when the activation glob explicitly names that directory (e.g. a pattern containing a literal `.svn` segment suppresses the `**/.svn` exclude for this query only). That keeps `**/*.ts`-style patterns cheap while making `**/.svn/wc.db` mean what it says. At minimum, the current behavior deserves a documentation note and an extension-host log line when a `workspaceContains` search returns empty solely due to excludes.

### Related issues

- #34711 — `workspaceContains` starts a search over full workspace, including `.git/`, `node_modules/` (closed 2018; the perf motivation for applying excludes, and the origin of the "how do you then activate on `.git/HEAD`?" question this issue is the answer to)
- #323964 — `.gitignore` will prevent an extension from launching (open; same mechanism via `search.useIgnoreFiles` instead of default `files.exclude`)
- #2739 — `activationEvents.workspaceContains` doesn't fire for directory (open; independent second blocker: directory-shaped patterns like `**/.svn` never match because file search matches files — reproduced here: `findFiles('**/.svn', null)` is also empty)
- #242245 — newly installed extensions not activating on `workspaceContains` trigger (adjacent reliability report)

Contributor guide

Open the contributing guide

Research direction

Start with src/vs/workbench/services/extensions/common/workspaceContains.ts and trace its glob query into src/vs/workbench/services/search/common/queryBuilder.ts; compare that path with the exact-name branch. Reproduce using the minimal probe and workspace fixtures described in the issue. Done means a workspaceContains glob can detect the matching SCM metadata despite default excludes, or the chosen behavior is documented and surfaced in the extension-host logs.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
developer-experience, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.