@maccesar/aiskills 1.12.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -9
- package/lib/cleanup.js +29 -0
- package/lib/commands/skills.js +110 -9
- package/lib/config.js +18 -9
- package/lib/installer.js +5 -3
- package/lib/symlink.js +45 -3
- package/lib/utils.js +41 -0
- package/package.json +2 -2
- package/skills/audit-codebase/SKILL.md +70 -0
- package/skills/audit-codebase/agents/openai.yaml +4 -0
- package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
- package/skills/audit-codebase/references/report-format.md +119 -0
- package/skills/humaniza/SKILL.md +55 -4
- package/skills/humaniza/references/ai-patterns-es.md +40 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +16 -0
- package/skills/humaniza/references/lexicon-es-mx.md +18 -0
- package/skills/humaniza/references/structures-es.md +132 -0
- package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
- package/skills/refactoring-ui/SKILL.md +65 -29
- package/skills/refactoring-ui/references/05-motion.md +124 -0
- package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
- package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
- package/skills/session-log/SKILL.md +524 -0
- package/skills/session-log/evals/README.md +79 -0
- package/skills/session-log/evals/ab-ronda-1.md +115 -0
- package/skills/session-log/evals/ab-ronda-2.md +174 -0
- package/skills/session-log/evals/defecto-experimento.md +12 -0
- package/skills/session-log/evals/evals.json +147 -0
- package/skills/session-log/evals/trigger-eval.json +23 -0
- package/skills/session-log/references/file-layout.md +336 -0
- package/skills/session-log/references/verification.md +173 -0
- package/skills/stitch-showcase/SKILL.md +24 -232
- package/skills/stitch-showcase/references/07-theme-system.md +12 -0
- package/skills/stitch-showcase/references/08-type-detection.md +9 -1
- package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
- package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
- package/skills/stitch-showcase/references/13-language-detection.md +82 -0
- package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
- package/skills/stitch-showcase/references/15-build-flags.md +71 -0
- package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
- package/skills/stitch-showcase/references/index.html +25 -19
- package/skills/stitch-showcase/references/viewer.html +24 -12
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
- package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
- package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
- package/skills/vscode-extension-dev/SKILL.md +90 -41
- package/skills/vscode-extension-dev/references/api-additional.md +168 -0
- package/skills/vscode-extension-dev/references/api-progress.md +55 -0
- package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
- package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
- package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
- package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
- package/skills/vscode-extension-dev/references/api-webview.md +149 -0
- package/skills/vscode-extension-dev/references/architecture.md +67 -0
- package/skills/vscode-extension-dev/references/debugger.md +179 -0
- package/skills/vscode-extension-dev/references/lsp.md +175 -0
- package/skills/vscode-extension-dev/references/notebooks.md +208 -0
- package/skills/vscode-extension-dev/references/testing.md +208 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +0 -625
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Language Server Protocol (LSP)
|
|
2
|
+
|
|
3
|
+
Build a language extension by running an out-of-process **language server** and connecting it to VS Code via the `vscode-languageclient` npm package. The server can be written in any language; only the client lives inside the extension.
|
|
4
|
+
|
|
5
|
+
Official guide: https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
|
|
6
|
+
Protocol spec: https://microsoft.github.io/language-server-protocol/
|
|
7
|
+
|
|
8
|
+
## When to Use LSP vs Direct API
|
|
9
|
+
|
|
10
|
+
| Need | Use |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Diagnostics, completion, hover for ONE editor (VS Code only) | Direct API (`vscode.languages.register*Provider`) |
|
|
13
|
+
| Same language features in VS Code + other LSP clients (Neovim, Sublime, etc.) | LSP |
|
|
14
|
+
| Heavy parsing/analysis you want isolated from the extension host | LSP (server runs in its own process) |
|
|
15
|
+
| Sharing logic with a CLI or CI tool | LSP (server is reusable) |
|
|
16
|
+
|
|
17
|
+
**Rule of thumb**: write LSP if the features are non-trivial OR you want portability. Use direct API for a one-off completion provider in a single language.
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
my-language-ext/
|
|
23
|
+
├── client/
|
|
24
|
+
│ ├── src/extension.ts # The VS Code extension (the client)
|
|
25
|
+
│ └── tsconfig.json
|
|
26
|
+
├── server/
|
|
27
|
+
│ ├── src/server.ts # Language server (Node-based example)
|
|
28
|
+
│ └── tsconfig.json
|
|
29
|
+
├── package.json # Extension manifest (covers both)
|
|
30
|
+
└── tsconfig.json # Root, with project references
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The extension's `package.json` lists `vscode-languageclient` as a runtime dependency and uses `activationEvents: ["onLanguage:<langId>"]`.
|
|
34
|
+
|
|
35
|
+
## Client (extension side)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import * as vscode from 'vscode';
|
|
40
|
+
import {
|
|
41
|
+
LanguageClient,
|
|
42
|
+
LanguageClientOptions,
|
|
43
|
+
ServerOptions,
|
|
44
|
+
TransportKind,
|
|
45
|
+
} from 'vscode-languageclient/node';
|
|
46
|
+
|
|
47
|
+
let client: LanguageClient | undefined;
|
|
48
|
+
|
|
49
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
50
|
+
const serverModule = context.asAbsolutePath(
|
|
51
|
+
path.join('server', 'out', 'server.js'),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const serverOptions: ServerOptions = {
|
|
55
|
+
run: { module: serverModule, transport: TransportKind.ipc },
|
|
56
|
+
debug: {
|
|
57
|
+
module: serverModule,
|
|
58
|
+
transport: TransportKind.ipc,
|
|
59
|
+
options: { execArgv: ['--nolazy', '--inspect=6009'] },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const clientOptions: LanguageClientOptions = {
|
|
64
|
+
documentSelector: [{ scheme: 'file', language: 'myLang' }],
|
|
65
|
+
synchronize: {
|
|
66
|
+
fileEvents: vscode.workspace.createFileSystemWatcher('**/.myLangConfig'),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
client = new LanguageClient(
|
|
71
|
+
'myLangServer',
|
|
72
|
+
'My Language Server',
|
|
73
|
+
serverOptions,
|
|
74
|
+
clientOptions,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
client.start();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function deactivate(): Promise<void> {
|
|
81
|
+
if (client) {
|
|
82
|
+
await client.stop();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`TransportKind.ipc` is the recommended transport for Node-based servers (process IPC). For non-Node servers, spawn with `TransportKind.stdio` or `TransportKind.socket`.
|
|
88
|
+
|
|
89
|
+
## Server (minimal Node example)
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import {
|
|
93
|
+
createConnection,
|
|
94
|
+
TextDocuments,
|
|
95
|
+
ProposedFeatures,
|
|
96
|
+
InitializeParams,
|
|
97
|
+
TextDocumentSyncKind,
|
|
98
|
+
CompletionItem,
|
|
99
|
+
CompletionItemKind,
|
|
100
|
+
} from 'vscode-languageserver/node';
|
|
101
|
+
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
102
|
+
|
|
103
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
104
|
+
const documents = new TextDocuments(TextDocument);
|
|
105
|
+
|
|
106
|
+
connection.onInitialize((_params: InitializeParams) => ({
|
|
107
|
+
capabilities: {
|
|
108
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
109
|
+
completionProvider: { resolveProvider: false, triggerCharacters: ['.'] },
|
|
110
|
+
hoverProvider: true,
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
connection.onCompletion((_params): CompletionItem[] => [
|
|
115
|
+
{ label: 'hello', kind: CompletionItemKind.Keyword },
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
connection.onHover((params) => {
|
|
119
|
+
const doc = documents.get(params.textDocument.uri);
|
|
120
|
+
if (!doc) return null;
|
|
121
|
+
return { contents: { kind: 'markdown', value: 'Hover content' } };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
documents.listen(connection);
|
|
125
|
+
connection.listen();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Capabilities You Typically Register
|
|
129
|
+
|
|
130
|
+
| Capability | Server method | Client receives |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| Completion | `onCompletion` | items shown in IntelliSense |
|
|
133
|
+
| Hover | `onHover` | tooltip on hover |
|
|
134
|
+
| Diagnostics | `connection.sendDiagnostics({ uri, diagnostics })` | Problems panel + squiggles |
|
|
135
|
+
| Definition | `onDefinition` | "Go to Definition" target |
|
|
136
|
+
| References | `onReferences` | "Find All References" results |
|
|
137
|
+
| Document symbols | `onDocumentSymbol` | breadcrumbs + outline |
|
|
138
|
+
| Formatting | `onDocumentFormatting` | "Format Document" |
|
|
139
|
+
| Code actions | `onCodeAction` | quick fixes / refactors |
|
|
140
|
+
| Rename | `onRenameRequest` | symbol rename |
|
|
141
|
+
|
|
142
|
+
Each capability must be declared in the `InitializeResult.capabilities` AND have a matching server handler. Mismatch = silent failure.
|
|
143
|
+
|
|
144
|
+
## Diagnostics (Push Model)
|
|
145
|
+
|
|
146
|
+
Send diagnostics whenever a document changes:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
documents.onDidChangeContent((event) => {
|
|
150
|
+
const diagnostics = validate(event.document);
|
|
151
|
+
connection.sendDiagnostics({
|
|
152
|
+
uri: event.document.uri,
|
|
153
|
+
diagnostics,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Diagnostic structure:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
{
|
|
162
|
+
severity: DiagnosticSeverity.Error, // Error | Warning | Information | Hint
|
|
163
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } },
|
|
164
|
+
message: 'Unexpected token',
|
|
165
|
+
source: 'my-lang',
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Anti-Patterns
|
|
170
|
+
|
|
171
|
+
- ❌ Re-parsing the whole document on every keystroke — use `TextDocumentSyncKind.Incremental` and process deltas
|
|
172
|
+
- ❌ Heavy work inside `onInitialize` — it blocks editor startup; do it lazily on first request
|
|
173
|
+
- ❌ Forgetting to `await client.stop()` in `deactivate()` — leaves the server process alive
|
|
174
|
+
- ❌ Hardcoding paths in `serverModule` — always go through `context.asAbsolutePath`
|
|
175
|
+
- ❌ Mixing client and server types — `vscode-languageclient/node` is for the client, `vscode-languageserver/node` is for the server
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Notebook Extensions
|
|
2
|
+
|
|
3
|
+
VS Code supports notebooks (Jupyter-style) through three independent extension points: **serializers** (read/write file format), **controllers** (execute cells), and **renderers** (display rich output). You can implement any combination.
|
|
4
|
+
|
|
5
|
+
Official guide: https://code.visualstudio.com/api/extension-guides/notebook
|
|
6
|
+
|
|
7
|
+
## The Three Roles
|
|
8
|
+
|
|
9
|
+
| Role | What it does | API |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Serializer | Converts file bytes ↔ in-memory `NotebookData` | `NotebookSerializer` |
|
|
12
|
+
| Controller | Runs a cell, produces outputs | `NotebookController` |
|
|
13
|
+
| Renderer | Webview that displays a specific output mime type | `package.json#contributes.notebookRenderer` |
|
|
14
|
+
|
|
15
|
+
A `.ipynb`-style extension typically provides serializer + controller. A renderer-only extension can extend ANY notebook (e.g., render a custom plot mime type).
|
|
16
|
+
|
|
17
|
+
## Notebook Serializer
|
|
18
|
+
|
|
19
|
+
Used to load and save the notebook file format.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import * as vscode from 'vscode';
|
|
23
|
+
|
|
24
|
+
class MyNotebookSerializer implements vscode.NotebookSerializer {
|
|
25
|
+
async deserializeNotebook(
|
|
26
|
+
content: Uint8Array,
|
|
27
|
+
_token: vscode.CancellationToken,
|
|
28
|
+
): Promise<vscode.NotebookData> {
|
|
29
|
+
const text = new TextDecoder().decode(content);
|
|
30
|
+
const parsed = JSON.parse(text);
|
|
31
|
+
|
|
32
|
+
const cells = parsed.cells.map((c: any) =>
|
|
33
|
+
new vscode.NotebookCellData(
|
|
34
|
+
c.kind === 'code'
|
|
35
|
+
? vscode.NotebookCellKind.Code
|
|
36
|
+
: vscode.NotebookCellKind.Markup,
|
|
37
|
+
c.source,
|
|
38
|
+
c.language ?? 'plaintext',
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return new vscode.NotebookData(cells);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async serializeNotebook(
|
|
46
|
+
data: vscode.NotebookData,
|
|
47
|
+
_token: vscode.CancellationToken,
|
|
48
|
+
): Promise<Uint8Array> {
|
|
49
|
+
const out = {
|
|
50
|
+
cells: data.cells.map((c) => ({
|
|
51
|
+
kind: c.kind === vscode.NotebookCellKind.Code ? 'code' : 'markdown',
|
|
52
|
+
language: c.languageId,
|
|
53
|
+
source: c.value,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
return new TextEncoder().encode(JSON.stringify(out, null, 2));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
61
|
+
context.subscriptions.push(
|
|
62
|
+
vscode.workspace.registerNotebookSerializer(
|
|
63
|
+
'my-notebook', // notebook type id (see package.json)
|
|
64
|
+
new MyNotebookSerializer(),
|
|
65
|
+
{ transientOutputs: false }, // false = persist outputs in file
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`package.json`:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"contributes": {
|
|
75
|
+
"notebooks": [
|
|
76
|
+
{
|
|
77
|
+
"type": "my-notebook",
|
|
78
|
+
"displayName": "My Notebook",
|
|
79
|
+
"selector": [{ "filenamePattern": "*.mynb" }]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Notebook Controller
|
|
86
|
+
|
|
87
|
+
Runs cells and produces outputs.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
class MyController {
|
|
91
|
+
readonly controllerId = 'my-controller';
|
|
92
|
+
readonly notebookType = 'my-notebook';
|
|
93
|
+
readonly label = 'My Kernel';
|
|
94
|
+
readonly supportedLanguages = ['python', 'plaintext'];
|
|
95
|
+
|
|
96
|
+
private readonly _controller: vscode.NotebookController;
|
|
97
|
+
private _executionOrder = 0;
|
|
98
|
+
|
|
99
|
+
constructor() {
|
|
100
|
+
this._controller = vscode.notebooks.createNotebookController(
|
|
101
|
+
this.controllerId,
|
|
102
|
+
this.notebookType,
|
|
103
|
+
this.label,
|
|
104
|
+
);
|
|
105
|
+
this._controller.supportedLanguages = this.supportedLanguages;
|
|
106
|
+
this._controller.supportsExecutionOrder = true;
|
|
107
|
+
this._controller.executeHandler = this._execute.bind(this);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async _execute(
|
|
111
|
+
cells: vscode.NotebookCell[],
|
|
112
|
+
_notebook: vscode.NotebookDocument,
|
|
113
|
+
_controller: vscode.NotebookController,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
for (const cell of cells) {
|
|
116
|
+
const exec = this._controller.createNotebookCellExecution(cell);
|
|
117
|
+
exec.executionOrder = ++this._executionOrder;
|
|
118
|
+
exec.start(Date.now());
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await this._runCell(cell);
|
|
122
|
+
await exec.replaceOutput([
|
|
123
|
+
new vscode.NotebookCellOutput([
|
|
124
|
+
vscode.NotebookCellOutputItem.text(result, 'text/plain'),
|
|
125
|
+
]),
|
|
126
|
+
]);
|
|
127
|
+
exec.end(true, Date.now());
|
|
128
|
+
} catch (err) {
|
|
129
|
+
await exec.replaceOutput([
|
|
130
|
+
new vscode.NotebookCellOutput([
|
|
131
|
+
vscode.NotebookCellOutputItem.error(err as Error),
|
|
132
|
+
]),
|
|
133
|
+
]);
|
|
134
|
+
exec.end(false, Date.now());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private async _runCell(cell: vscode.NotebookCell): Promise<string> {
|
|
140
|
+
// Real kernels: send cell.document.getText() to a backend, await result
|
|
141
|
+
return `Echo: ${cell.document.getText()}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
dispose(): void {
|
|
145
|
+
this._controller.dispose();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Register the controller in `activate()` and push to `context.subscriptions`.
|
|
151
|
+
|
|
152
|
+
## Output Mime Types
|
|
153
|
+
|
|
154
|
+
Common mime types VS Code knows how to render natively:
|
|
155
|
+
|
|
156
|
+
- `text/plain`
|
|
157
|
+
- `text/markdown`
|
|
158
|
+
- `text/html`
|
|
159
|
+
- `image/png`, `image/jpeg`, `image/svg+xml`
|
|
160
|
+
- `application/json`
|
|
161
|
+
- `application/x.notebook.error` (use `NotebookCellOutputItem.error(err)`)
|
|
162
|
+
- `application/vnd.code.notebook.stdout` / `.stderr` (use `.stdout(str)` / `.stderr(str)`)
|
|
163
|
+
|
|
164
|
+
For anything else, ship a notebook renderer (next section).
|
|
165
|
+
|
|
166
|
+
## Notebook Renderer
|
|
167
|
+
|
|
168
|
+
A renderer is a webview that takes a typed output and produces DOM. Declared entirely in `package.json` + a script that exports `activate`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
"contributes": {
|
|
172
|
+
"notebookRenderer": [
|
|
173
|
+
{
|
|
174
|
+
"id": "my-plot-renderer",
|
|
175
|
+
"displayName": "My Plot",
|
|
176
|
+
"entrypoint": "./out/renderer.js",
|
|
177
|
+
"mimeTypes": ["application/x.my-plot+json"]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`renderer.ts` — build the DOM with safe APIs, never inject untrusted strings as HTML:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import type { ActivationFunction } from 'vscode-notebook-renderer';
|
|
187
|
+
|
|
188
|
+
export const activate: ActivationFunction = () => ({
|
|
189
|
+
renderOutputItem(outputItem, element) {
|
|
190
|
+
const data = outputItem.json();
|
|
191
|
+
|
|
192
|
+
const pre = document.createElement('pre');
|
|
193
|
+
pre.textContent = JSON.stringify(data, null, 2);
|
|
194
|
+
element.replaceChildren(pre);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Renderers run in a separate iframe — they cannot call `vscode.*` directly. Communicate with the controller via the messaging API exposed in `renderer.activate`.
|
|
200
|
+
|
|
201
|
+
## Anti-Patterns
|
|
202
|
+
|
|
203
|
+
- ❌ Writing to disk synchronously in the serializer — VS Code calls it on the main thread
|
|
204
|
+
- ❌ Forgetting `exec.end(success, endTime)` — the cell stays in "running" state forever
|
|
205
|
+
- ❌ Reusing `executionOrder` numbers — must monotonically increase per notebook session
|
|
206
|
+
- ❌ Heavy DOM building in the renderer without batching — laggy scroll on large notebooks
|
|
207
|
+
- ❌ Bundling a notebook renderer with `vscode` as a dependency — renderers run in an iframe, not the extension host; they have NO `vscode` import
|
|
208
|
+
- ❌ Setting `innerHTML` from untrusted output data — XSS in the notebook viewer. Build DOM with `createElement` + `textContent`
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Testing Extensions (Advanced)
|
|
2
|
+
|
|
3
|
+
Beyond the basic Mocha setup covered in `architecture.md`: configuring `.vscode-test.mjs` for multiple suites, isolating unit tests from the VS Code runtime, mocking the `vscode` module, running tests in CI, and measuring coverage.
|
|
4
|
+
|
|
5
|
+
Official guide: https://code.visualstudio.com/api/working-with-extensions/testing-extension
|
|
6
|
+
|
|
7
|
+
## Two Test Layers
|
|
8
|
+
|
|
9
|
+
| Layer | Runs in | Imports `vscode`? | Speed |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| Unit | Plain Node (Mocha/Vitest) | No — only `services/`, pure logic | Fast |
|
|
12
|
+
| Integration | VS Code Extension Host (Electron) via `@vscode/test-electron` | Yes | Slow |
|
|
13
|
+
|
|
14
|
+
Keep `services/` free of `vscode` imports so unit tests don't need the Electron runtime. Integration tests cover wiring (commands registered, providers connected, activation works).
|
|
15
|
+
|
|
16
|
+
## `.vscode-test.mjs` (Multi-Suite)
|
|
17
|
+
|
|
18
|
+
The default file in `architecture.md` runs one suite. For real extensions, split by concern:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { defineConfig } from '@vscode/test-cli';
|
|
22
|
+
|
|
23
|
+
export default defineConfig([
|
|
24
|
+
{
|
|
25
|
+
label: 'integration',
|
|
26
|
+
files: 'out/test/integration/**/*.test.js',
|
|
27
|
+
workspaceFolder: './test/fixtures/workspace-basic',
|
|
28
|
+
mocha: { timeout: 20000, ui: 'tdd' },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'multi-root',
|
|
32
|
+
files: 'out/test/multi-root/**/*.test.js',
|
|
33
|
+
workspaceFolder: './test/fixtures/workspace-multi.code-workspace',
|
|
34
|
+
mocha: { timeout: 30000, ui: 'tdd' },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'insiders',
|
|
38
|
+
version: 'insiders',
|
|
39
|
+
files: 'out/test/integration/**/*.test.js',
|
|
40
|
+
workspaceFolder: './test/fixtures/workspace-basic',
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run a specific suite: `vscode-test --label integration`.
|
|
46
|
+
|
|
47
|
+
## Workspace Fixtures
|
|
48
|
+
|
|
49
|
+
Integration tests need a real folder on disk because the editor expects a workspace:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
test/fixtures/
|
|
53
|
+
├── workspace-basic/
|
|
54
|
+
│ ├── .vscode/
|
|
55
|
+
│ │ └── settings.json
|
|
56
|
+
│ ├── src/
|
|
57
|
+
│ │ └── sample.txt
|
|
58
|
+
│ └── package.json
|
|
59
|
+
└── workspace-multi.code-workspace
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
// workspace-multi.code-workspace
|
|
64
|
+
{
|
|
65
|
+
"folders": [
|
|
66
|
+
{ "path": "workspace-basic" },
|
|
67
|
+
{ "path": "workspace-secondary" }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Mutating fixtures in a test? Copy to a temp dir in `suiteSetup` and point the test there — leaving committed fixtures dirty between runs causes flakes.
|
|
73
|
+
|
|
74
|
+
## Mocking the `vscode` Module (Unit Tests)
|
|
75
|
+
|
|
76
|
+
`vscode` is provided by the Electron host — it has no npm package. Unit tests outside the host can't `import 'vscode'` directly. Two ways around this:
|
|
77
|
+
|
|
78
|
+
**1. Dependency injection** (preferred): never import `vscode` in `services/`. Pass the bits you need as parameters.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// services/processor.ts — pure, no vscode import
|
|
82
|
+
export interface FileReader {
|
|
83
|
+
read(path: string): Promise<string>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function process(reader: FileReader, path: string): Promise<number> {
|
|
87
|
+
const text = await reader.read(path);
|
|
88
|
+
return text.split('\n').length;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Test it with a fake reader. The VS Code layer adapts `vscode.workspace.fs` to the `FileReader` interface.
|
|
93
|
+
|
|
94
|
+
**2. Module aliasing** (when DI isn't feasible): map `vscode` to a stub via the test runner.
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// vitest.config.ts (or jest moduleNameMapper)
|
|
98
|
+
import { defineConfig } from 'vitest/config';
|
|
99
|
+
|
|
100
|
+
export default defineConfig({
|
|
101
|
+
test: {
|
|
102
|
+
alias: {
|
|
103
|
+
vscode: new URL('./test/stubs/vscode.ts', import.meta.url).pathname,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// test/stubs/vscode.ts — just enough surface to compile
|
|
111
|
+
export const window = {
|
|
112
|
+
showInformationMessage: (..._args: unknown[]) => Promise.resolve(undefined),
|
|
113
|
+
showErrorMessage: (..._args: unknown[]) => Promise.resolve(undefined),
|
|
114
|
+
activeTextEditor: undefined,
|
|
115
|
+
};
|
|
116
|
+
export const workspace = {
|
|
117
|
+
workspaceFolders: undefined as readonly unknown[] | undefined,
|
|
118
|
+
getConfiguration: () => ({ get: () => undefined }),
|
|
119
|
+
};
|
|
120
|
+
export const Uri = { file: (p: string) => ({ fsPath: p }) };
|
|
121
|
+
// add more as needed
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Aliasing is brittle — keep the stub minimal and prefer DI for non-trivial logic.
|
|
125
|
+
|
|
126
|
+
## CI: GitHub Actions
|
|
127
|
+
|
|
128
|
+
VS Code tests need a display. On Linux runners, wrap with `xvfb`:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
name: CI
|
|
132
|
+
on: [push, pull_request]
|
|
133
|
+
|
|
134
|
+
jobs:
|
|
135
|
+
test:
|
|
136
|
+
strategy:
|
|
137
|
+
matrix:
|
|
138
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
139
|
+
runs-on: ${{ matrix.os }}
|
|
140
|
+
steps:
|
|
141
|
+
- uses: actions/checkout@v4
|
|
142
|
+
- uses: actions/setup-node@v4
|
|
143
|
+
with:
|
|
144
|
+
node-version: 20
|
|
145
|
+
- run: npm ci
|
|
146
|
+
- run: npm run compile
|
|
147
|
+
- name: Run tests (Linux)
|
|
148
|
+
if: runner.os == 'Linux'
|
|
149
|
+
run: xvfb-run -a npm test
|
|
150
|
+
- name: Run tests (non-Linux)
|
|
151
|
+
if: runner.os != 'Linux'
|
|
152
|
+
run: npm test
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Coverage
|
|
156
|
+
|
|
157
|
+
`@vscode/test-electron` runs in a real Electron process, so traditional Istanbul instrumentation can't simply wrap `node`. Use `c8` against the compiled output:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
// package.json
|
|
161
|
+
"scripts": {
|
|
162
|
+
"test:coverage": "c8 --reporter=text --reporter=lcov vscode-test"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`c8` reads V8's native coverage and produces an `lcov.info` you can upload to Codecov/Coveralls. Coverage of integration-test execution paths only — unit tests should be measured separately and merged.
|
|
167
|
+
|
|
168
|
+
## Stable Test Patterns
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import * as assert from 'assert';
|
|
172
|
+
import * as vscode from 'vscode';
|
|
173
|
+
|
|
174
|
+
suite('Activation', () => {
|
|
175
|
+
suiteSetup(async () => {
|
|
176
|
+
const ext = vscode.extensions.getExtension('publisher.my-extension');
|
|
177
|
+
assert.ok(ext, 'Extension not found — check publisher and name in package.json');
|
|
178
|
+
await ext!.activate();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('registers expected commands', async () => {
|
|
182
|
+
const commands = await vscode.commands.getCommands(true);
|
|
183
|
+
for (const id of ['myExt.run', 'myExt.refresh']) {
|
|
184
|
+
assert.ok(commands.includes(id), `Missing command: ${id}`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('respects user setting', async () => {
|
|
189
|
+
const config = vscode.workspace.getConfiguration('myExt');
|
|
190
|
+
await config.update('mode', 'strict', vscode.ConfigurationTarget.Workspace);
|
|
191
|
+
|
|
192
|
+
// ...exercise the feature
|
|
193
|
+
|
|
194
|
+
await config.update('mode', undefined, vscode.ConfigurationTarget.Workspace);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Always restore mutated settings/state in `teardown` or `suiteTeardown` — workspace state persists across test files.
|
|
200
|
+
|
|
201
|
+
## Anti-Patterns
|
|
202
|
+
|
|
203
|
+
- ❌ Putting business logic that doesn't touch the editor inside `extension.ts` — forces every test to spin up Electron
|
|
204
|
+
- ❌ Sharing state between tests via module-level variables — order-dependent failures
|
|
205
|
+
- ❌ `setTimeout`/`sleep` to "wait for activation" — use `await ext.activate()` and the `onDidStartDebugSession` (or similar) events
|
|
206
|
+
- ❌ Hardcoding absolute paths in fixtures — breaks on CI runners
|
|
207
|
+
- ❌ Running `xvfb-run` on macOS/Windows — only needed on Linux
|
|
208
|
+
- ❌ Trusting that a fresh test instance is clean — clear workspace state and SecretStorage explicitly when behavior depends on them
|