iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Intermédiaire

Extensions VS Code

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.

VS Code APITypeScriptesbuildvsceOpen VSXGitHub Actions
01Utiliser et gérer ses extensions02Générer et lancer sa première extension03Commandes, réglages et activation04Barre d'état, vues et décorations05Webviews : un panneau HTML dans l'éditeur06Diagnostics, corrections rapides et complétion07Tester et bundler l'extension08Publier sur le Marketplace et Open VSX
Chapitre 4·40 min

Barre d'état, vues et décorations

VS Code ne laisse pas une extension dessiner où elle veut. Il fournit des emplacements précis, chacun avec son API : barre d'état, notifications, listes de sélection, vues arborescentes, décorations dans l'éditeur. Cette contrainte garantit que toutes les extensions se ressemblent et respectent le thème de l'utilisateur.

Scanner tout le projet

Les vues ont besoin des marqueurs de tout le projet, pas seulement du fichier ouvert. On ajoute un scan du workspace :

src/workspace-scan.ts
import * as vscode from 'vscode'
import { scanText, TodoMatch } from './scanner'
import { getConfig } from './config'
 
export interface FileTodos {
  uri: vscode.Uri
  matches: TodoMatch[]
}
 
const decoder = new TextDecoder('utf-8')
 
export async function scanWorkspace(token?: vscode.CancellationToken): Promise<FileTodos[]> {
  const { tags, exclude } = getConfig()
  const uris = await vscode.workspace.findFiles(
    '**/*.{ts,tsx,js,jsx,vue,py,go,rs,java,cs,php,css,scss,html,md}',
    exclude,
    5000,
    token
  )
 
  const results: FileTodos[] = []
  for (const uri of uris) {
    if (token?.isCancellationRequested) break
    const bytes = await vscode.workspace.fs.readFile(uri)
    const matches = scanText(decoder.decode(bytes), tags)
    if (matches.length > 0) results.push({ uri, matches })
  }
  return results
}

Deux choix méritent explication. vscode.workspace.fs plutôt que le module fs de Node : il fonctionne aussi sur des fichiers distants (SSH, conteneurs, WSL) et dans vscode.dev. Le CancellationToken permet d'interrompre un scan long quand l'utilisateur annule.

Notifications et progression

Trois niveaux de notification, et des boutons qui renvoient le choix de l'utilisateur :

const choice = await vscode.window.showWarningMessage(
  '42 TODO sans ticket dans le projet.',
  'Afficher',
  'Ignorer'
)
if (choice === 'Afficher') {
  await vscode.commands.executeCommand('todoRadar.list.focus')
}

Une notification ne doit pas interrompre pour rien. Une extension qui affiche un message à chaque ouverture de fichier est désinstallée dans la journée.

Pour une opération longue, withProgress affiche une barre et un bouton d'annulation :

const results = await vscode.window.withProgress(
  {
    location: vscode.ProgressLocation.Notification,
    title: 'TODO Radar : scan du projet',
    cancellable: true,
  },
  (progress, token) => {
    progress.report({ message: 'Recherche des fichiers…' })
    return scanWorkspace(token)
  }
)

ProgressLocation.Window affiche une simple icône animée dans la barre d'état, plus discrète.

Listes de sélection

showQuickPick est la liste filtrable qu'on connaît par Ctrl+P. Parfait pour sauter à un TODO au clavier :

vscode.commands.registerCommand('todo-radar.goTo', async () => {
  const results = await scanWorkspace()
  const items = results.flatMap(({ uri, matches }) =>
    matches.map((m) => ({
      label: `$(check) ${m.tag}`,
      description: m.text,
      detail: `${vscode.workspace.asRelativePath(uri)}:${m.line + 1}`,
      uri,
      match: m,
    }))
  )
 
  const picked = await vscode.window.showQuickPick(items, {
    placeHolder: 'Aller à un marqueur',
    matchOnDescription: true,
    matchOnDetail: true,
  })
  if (!picked) return
 
  const position = new vscode.Position(picked.match.line, picked.match.column)
  await vscode.window.showTextDocument(picked.uri, {
    selection: new vscode.Range(position, position),
  })
})

$(check) est une codicon, l'une des centaines d'icônes intégrées à VS Code. La syntaxe $(nom) fonctionne dans les libellés, la barre d'état et les titres de commandes. La liste complète est sur le site de documentation de l'API, page Product Icon Reference.

Pour une saisie libre, showInputBox accepte une fonction de validation :

const tag = await vscode.window.showInputBox({
  prompt: 'Nouveau marqueur à suivre',
  placeHolder: 'NOTE',
  validateInput: (value) => (/^[A-Z]+$/.test(value) ? null : 'Majuscules uniquement'),
})

La barre d'état

Le compteur du fichier courant va en bas à gauche :

src/status-bar.ts
import * as vscode from 'vscode'
import { scanText } from './scanner'
import { getConfig } from './config'
 
export function createStatusBar(context: vscode.ExtensionContext) {
  const item = vscode.window.createStatusBarItem(
    'todoRadar.count',
    vscode.StatusBarAlignment.Left,
    100
  )
  item.name = 'TODO Radar'
  item.command = 'todo-radar.goTo'
  context.subscriptions.push(item)
 
  const update = () => {
    const editor = vscode.window.activeTextEditor
    if (!editor) {
      item.hide()
      return
    }
    const count = scanText(editor.document.getText(), getConfig().tags).length
    item.text = `$(checklist) ${count}`
    item.tooltip = `${count} marqueur(s) dans ce fichier — cliquer pour naviguer`
    item.backgroundColor =
      count > 10 ? new vscode.ThemeColor('statusBarItem.warningBackground') : undefined
    item.show()
  }
 
  context.subscriptions.push(
    vscode.window.onDidChangeActiveTextEditor(update),
    vscode.workspace.onDidChangeTextDocument((e) => {
      if (e.document === vscode.window.activeTextEditor?.document) update()
    })
  )
  update()
}

ThemeColor référence une couleur du thème actif au lieu d'un code hexadécimal. L'extension reste lisible en thème clair, sombre et contraste élevé. Pour la barre d'état, seules statusBarItem.warningBackground et statusBarItem.errorBackground sont acceptées comme fond.

onDidChangeTextDocument se déclenche à chaque frappe. Sur un gros fichier, rescanner à chaque caractère se sent. On ajoutera un délai (debounce) au chapitre 6, quand le même problème touchera les diagnostics.

Une vue latérale

La pièce principale de TODO Radar : une icône dans la barre d'activité, une vue qui liste les fichiers et leurs marqueurs.

D'abord la déclaration :

package.json
"viewsContainers": {
  "activitybar": [
    {
      "id": "todo-radar",
      "title": "TODO Radar",
      "icon": "media/radar.svg"
    }
  ]
},
"views": {
  "todo-radar": [
    {
      "id": "todoRadar.list",
      "name": "Marqueurs"
    }
  ]
},
"viewsWelcome": [
  {
    "view": "todoRadar.list",
    "contents": "Aucun marqueur trouvé pour l'instant.\n[Scanner le projet](command:todo-radar.refresh)"
  }
]

L'icône de la barre d'activité est un SVG monochrome de 24×24, sans couleur imposée : VS Code la teinte selon le thème. viewsWelcome s'affiche quand la vue est vide ; la syntaxe [texte](command:id) crée un bouton.

Ensuite le fournisseur de données. Un TreeDataProvider répond à deux questions : quels sont les enfants d'un élément, et comment afficher un élément.

src/todo-tree.ts
import * as vscode from 'vscode'
import { FileTodos, scanWorkspace } from './workspace-scan'
import { TodoMatch } from './scanner'
 
type Node =
  | { kind: 'file'; file: FileTodos }
  | { kind: 'match'; uri: vscode.Uri; match: TodoMatch }
 
export class TodoTreeProvider implements vscode.TreeDataProvider<Node> {
  private files: FileTodos[] = []
  private readonly changed = new vscode.EventEmitter<Node | undefined>()
  readonly onDidChangeTreeData = this.changed.event
 
  async refresh() {
    this.files = await scanWorkspace()
    this.changed.fire(undefined)
  }
 
  get total() {
    return this.files.reduce((sum, f) => sum + f.matches.length, 0)
  }
 
  getChildren(node?: Node): Node[] {
    if (!node) return this.files.map((file) => ({ kind: 'file', file }))
    if (node.kind === 'file') {
      return node.file.matches.map((match) => ({ kind: 'match', uri: node.file.uri, match }))
    }
    return []
  }
 
  getTreeItem(node: Node): vscode.TreeItem {
    if (node.kind === 'file') {
      const item = new vscode.TreeItem(
        node.file.uri,
        vscode.TreeItemCollapsibleState.Expanded
      )
      item.description = `${node.file.matches.length}`
      return item
    }
 
    const { match, uri } = node
    const item = new vscode.TreeItem(match.text || match.tag)
    item.description = `${match.tag} · ligne ${match.line + 1}`
    item.iconPath = new vscode.ThemeIcon(match.tag === 'FIXME' ? 'bug' : 'check')
    item.contextValue = 'todoMatch'
    item.command = {
      command: 'vscode.open',
      title: 'Ouvrir',
      arguments: [uri, { selection: new vscode.Range(match.line, match.column, match.line, match.column) }],
    }
    return item
  }
}

Quelques détails qui font la différence :

  • passer une Uri au constructeur de TreeItem lui donne automatiquement le nom du fichier et l'icône du thème d'icônes de l'utilisateur ;
  • description s'affiche en gris à droite du libellé ;
  • contextValue sert à cibler cet élément dans un menu (viewItem == todoMatch) ;
  • command rend l'élément cliquable ; vscode.open est une commande intégrée.

L'enregistrement dans activate :

src/extension.ts
const tree = new TodoTreeProvider()
const view = vscode.window.createTreeView('todoRadar.list', {
  treeDataProvider: tree,
  showCollapseAll: true,
})
context.subscriptions.push(view)
 
const refresh = async () => {
  await tree.refresh()
  view.badge = { value: tree.total, tooltip: `${tree.total} marqueurs` }
  await vscode.commands.executeCommand('setContext', 'todoRadar.hasTodos', tree.total > 0)
}
 
context.subscriptions.push(
  vscode.commands.registerCommand('todo-radar.refresh', refresh),
  vscode.workspace.onDidSaveTextDocument(refresh)
)
refresh()

view.badge affiche le total en pastille sur l'icône de la barre d'activité. Un bouton de rafraîchissement dans la barre de titre de la vue complète l'ensemble :

package.json
"commands": [
  {
    "command": "todo-radar.refresh",
    "title": "Rafraîchir",
    "category": "TODO Radar",
    "icon": "$(refresh)"
  }
],
"menus": {
  "view/title": [
    {
      "command": "todo-radar.refresh",
      "when": "view == todoRadar.list",
      "group": "navigation"
    }
  ]
}

Surligner dans l'éditeur

Dernière brique visuelle : colorer les marqueurs dans le texte. Une décoration se crée une fois, puis s'applique à des plages :

src/decorations.ts
import * as vscode from 'vscode'
import { scanText } from './scanner'
import { getConfig } from './config'
 
const todoDecoration = vscode.window.createTextEditorDecorationType({
  backgroundColor: new vscode.ThemeColor('editor.findMatchHighlightBackground'),
  overviewRulerColor: new vscode.ThemeColor('editorOverviewRuler.warningForeground'),
  overviewRulerLane: vscode.OverviewRulerLane.Right,
  fontWeight: 'bold',
})
 
export function decorate(editor: vscode.TextEditor) {
  const ranges = scanText(editor.document.getText(), getConfig().tags).map(
    (m) => new vscode.Range(m.line, m.column, m.line, m.column + m.tag.length)
  )
  editor.setDecorations(todoDecoration, ranges)
}

overviewRulerColor ajoute un repère dans la barre de défilement, ce qui permet de repérer les marqueurs d'un fichier de 2 000 lignes d'un coup d'œil. On appelle decorate dans les mêmes événements que la barre d'état.


TODO Radar a désormais une interface native. Quand une extension a besoin de plus qu'une liste ou un compteur, il reste la webview, un panneau HTML libre : c'est le sujet du chapitre suivant.

Précédent
Commandes, réglages et activation
Suivant
Webviews : un panneau HTML dans l'éditeur

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main