Utiliser, créer et publier une extension VS Code en TypeScript : commandes, configuration, vues, webviews, diagnostics, tests, bundling et publication sur le Marketplace et Open VSX.
Microsoft maintient un générateur officiel, generator-code, basé sur Yeoman. Il produit un projet TypeScript prêt à déboguer. On l'utilise une fois, puis on comprend chaque fichier qu'il a créé.
Il faut Node.js (version LTS) et Git. Pas besoin d'installer le générateur globalement :
npx --package yo --package generator-code -- yo codeLes réponses pour TODO Radar :
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? TODO Radar
? What's the identifier of your extension? todo-radar
? What's the description of your extension? Repère et liste les TODO, FIXME et HACK du projet
? Initialize a git repository? Yes
? Which bundler to use? esbuild
? Which package manager to use? npm
? Do you want to open the new folder with Visual Studio Code? Open with `code`On choisit esbuild dès maintenant. Une extension non bundlée embarque tout son node_modules, se charge lentement et pèse lourd. Le chapitre 7 détaille la configuration.
todo-radar/
├── .vscode/
│ ├── launch.json ← configuration F5 : lance l'Extension Development Host
│ ├── tasks.json ← tâche de build en mode watch
│ └── extensions.json ← extensions recommandées pour développer
├── src/
│ ├── extension.ts ← point d'entrée : activate() et deactivate()
│ └── test/
│ └── extension.test.ts
├── esbuild.js ← script de bundling
├── .vscode-test.mjs ← configuration des tests
├── .vscodeignore ← fichiers exclus du paquet publié
├── package.json ← le manifeste de l'extension
├── tsconfig.json
├── CHANGELOG.md
└── README.md ← devient la page du MarketplaceDeux fichiers comptent vraiment : package.json, qui déclare, et src/extension.ts, qui implémente.
package.jsonC'est un package.json npm classique, enrichi de champs propres à VS Code.
{
"name": "todo-radar",
"displayName": "TODO Radar",
"description": "Repère et liste les TODO, FIXME et HACK du projet",
"version": "0.0.1",
"engines": {
"vscode": "^1.100.0"
},
"categories": ["Other"],
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "todo-radar.helloWorld",
"title": "Hello World"
}
]
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && node esbuild.js",
"watch": "node esbuild.js --watch",
"package": "npm run check-types && node esbuild.js --production",
"check-types": "tsc --noEmit",
"test": "vscode-test"
}
}Les champs à connaître :
| Champ | Rôle |
|---|---|
name | Identifiant technique, en minuscules. Avec publisher, il forme l'ID unique publisher.name |
displayName | Nom affiché dans le Marketplace et le panneau Extensions |
engines.vscode | Version minimale de VS Code. L'extension ne s'installe pas sur une version plus ancienne |
main | Fichier JavaScript chargé à l'activation (le bundle, pas le TypeScript) |
activationEvents | Quand charger l'extension. Vide ici : les commandes déclarées suffisent depuis VS Code 1.74 |
contributes | Tout ce que l'extension ajoute à l'interface, de façon déclarative |
engines.vscode mérite réflexion. Le générateur y met votre version actuelle. Plus elle est récente, plus vous avez d'API, et moins vous touchez d'utilisateurs bloqués sur une version d'entreprise. Une version d'un an en arrière est un bon compromis.
La version de @types/vscode, dans les devDependencies, doit correspondre à engines.vscode. Si vous utilisez une API typée en 1.105 avec un moteur à 1.100, l'extension plantera chez ceux qui ont la 1.100.
extension.tsimport * as vscode from 'vscode'
export function activate(context: vscode.ExtensionContext) {
console.log('TODO Radar est actif')
const disposable = vscode.commands.registerCommand('todo-radar.helloWorld', () => {
vscode.window.showInformationMessage('Hello World from TODO Radar!')
})
context.subscriptions.push(disposable)
}
export function deactivate() {}Trois idées dans ces quelques lignes.
activate est appelée une seule fois, au premier événement d'activation. On y enregistre tout ce dont l'extension a besoin.
Le lien entre manifeste et code passe par l'identifiant de commande. todo-radar.helloWorld est déclaré dans contributes.commands (pour que la commande apparaisse dans la palette) et implémenté par registerCommand. Si les deux chaînes diffèrent, la commande s'affiche mais renvoie « command not found ».
Tout ce qu'on enregistre renvoie un Disposable. On le pousse dans context.subscriptions ; VS Code libère tout à la désactivation. Oublier cette ligne, c'est laisser des écouteurs actifs après un rechargement.
Appuyez sur F5. VS Code compile le projet en mode watch puis ouvre une seconde fenêtre, titrée [Extension Development Host]. Votre extension y est chargée depuis les sources.
Dans cette fenêtre, ouvrez la palette (Ctrl+Shift+P) et tapez « Hello World ». La notification apparaît. Dans la fenêtre d'origine, la console de débogage affiche TODO Radar est actif.
Le cycle de développement devient :
src/extension.ts et enregistrer (la tâche watch recompile) ;Ctrl+R ou Developer: Reload Window ;Les points d'arrêt posés dans le TypeScript fonctionnent : les source maps relient le bundle au code source.
Première brique de TODO Radar : compter les marqueurs dans le fichier ouvert. On isole la logique de détection dans un module sans dépendance à vscode, ce qui la rendra testable en pur Node.js au chapitre 7.
export interface TodoMatch {
tag: string
text: string
line: number
column: number
}
export const DEFAULT_TAGS = ['TODO', 'FIXME', 'HACK']
export function scanText(source: string, tags: string[] = DEFAULT_TAGS): TodoMatch[] {
const pattern = new RegExp(`\\b(${tags.join('|')})\\b[:\\s]*(.*)`)
const matches: TodoMatch[] = []
source.split(/\r?\n/).forEach((content, line) => {
const m = pattern.exec(content)
if (m) {
matches.push({ tag: m[1], text: m[2].trim(), line, column: m.index })
}
})
return matches
}Si l'expression régulière vous semble opaque, l'article sur les expressions régulières décortique les groupes de capture et \b.
On branche ce scanner sur une commande :
import * as vscode from 'vscode'
import { scanText } from './scanner'
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('todo-radar.countInFile', () => {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showWarningMessage('Aucun fichier ouvert.')
return
}
const matches = scanText(editor.document.getText())
vscode.window.showInformationMessage(
`${matches.length} marqueur(s) dans ${vscode.workspace.asRelativePath(editor.document.uri)}`
)
})
)
}
export function deactivate() {}Et dans le manifeste, on remplace la commande générée :
"contributes": {
"commands": [
{
"command": "todo-radar.countInFile",
"title": "Compter les TODO du fichier",
"category": "TODO Radar"
}
]
}category préfixe la commande dans la palette : elle s'affiche comme TODO Radar: Compter les TODO du fichier, ce qui la rend facile à retrouver.
vscode en un coup d'œilL'API est grande, mais elle s'organise en quelques espaces de noms :
| Espace | Contenu |
|---|---|
vscode.commands | Enregistrer et exécuter des commandes |
vscode.window | Éditeurs ouverts, notifications, saisies, barre d'état, vues, webviews |
vscode.workspace | Fichiers du projet, configuration, événements de modification, système de fichiers |
vscode.languages | Diagnostics, complétion, survol, actions de code, tout ce qui touche à un langage |
vscode.env | Presse-papiers, langue, ouverture d'URL externes |
vscode.extensions | Accès aux autres extensions installées |
Le typage de @types/vscode est la meilleure documentation : survolez une fonction, la description et les exemples sont dans les commentaires. Si les types vous freinent encore, le guide TypeScript pour développeurs JavaScript couvre ce qu'il faut pour suivre ce cours.
L'extension démarre, une commande fonctionne. Le chapitre suivant ajoute ce qui fait une extension utilisable : réglages, raccourcis, menus contextuels et activation au bon moment.