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 6·35 min

Diagnostics, corrections rapides et complétion

Les vagues jaunes sous le code, l'ampoule qui propose une correction, la liste de suggestions pendant la frappe : ce sont des fournisseurs de langage. Une extension les enregistre auprès de vscode.languages, et VS Code les appelle au bon moment. TODO Radar va s'en servir pour signaler les TODO sans ticket.

La règle à implémenter

Quand todoRadar.requireTicket est activé, un TODO doit référencer un ticket :

// TODO #142 : gérer la pagination          ← accepté
// TODO PROJ-87 : cache Redis                ← accepté
// TODO : nettoyer cette fonction            ← signalé

On ajoute la détection au scanner, toujours sans dépendance à vscode :

src/scanner.ts
const TICKET = /(#\d+|\b[A-Z][A-Z0-9]+-\d+\b)/
 
export function hasTicket(match: TodoMatch): boolean {
  return TICKET.test(match.text)
}

Publier des diagnostics

Un diagnostic est un problème attaché à une plage de texte. Il apparaît souligné dans l'éditeur, dans le panneau Problems et dans la barre de défilement.

src/diagnostics.ts
import * as vscode from 'vscode'
import { scanText, hasTicket } from './scanner'
import { getConfig } from './config'
 
export const DIAGNOSTIC_CODE = 'todo-without-ticket'
 
export function registerDiagnostics(context: vscode.ExtensionContext) {
  const collection = vscode.languages.createDiagnosticCollection('todoRadar')
  context.subscriptions.push(collection)
 
  const check = (document: vscode.TextDocument) => {
    const { tags, requireTicket } = getConfig()
    if (!requireTicket || document.uri.scheme !== 'file') {
      collection.delete(document.uri)
      return
    }
 
    const diagnostics = scanText(document.getText(), tags)
      .filter((m) => m.tag === 'TODO' && !hasTicket(m))
      .map((m) => {
        const range = new vscode.Range(m.line, m.column, m.line, m.column + m.tag.length)
        const diagnostic = new vscode.Diagnostic(
          range,
          'TODO sans référence de ticket',
          vscode.DiagnosticSeverity.Warning
        )
        diagnostic.source = 'TODO Radar'
        diagnostic.code = DIAGNOSTIC_CODE
        return diagnostic
      })
 
    collection.set(document.uri, diagnostics)
  }
 
  // Attendre une pause dans la frappe avant de recalculer
  const timers = new Map<string, NodeJS.Timeout>()
  const checkLater = (document: vscode.TextDocument) => {
    const key = document.uri.toString()
    clearTimeout(timers.get(key))
    timers.set(key, setTimeout(() => check(document), 300))
  }
 
  vscode.workspace.textDocuments.forEach(check)
  context.subscriptions.push(
    vscode.workspace.onDidOpenTextDocument(check),
    vscode.workspace.onDidChangeTextDocument((e) => checkLater(e.document)),
    vscode.workspace.onDidCloseTextDocument((doc) => collection.delete(doc.uri)),
    vscode.workspace.onDidChangeConfiguration((e) => {
      if (e.affectsConfiguration('todoRadar')) vscode.workspace.textDocuments.forEach(check)
    })
  )
}

Le délai de 300 ms (debounce) évite de rescanner à chaque caractère. C'est la valeur qu'utilisent la plupart des linters. On l'applique aussi à la barre d'état et aux décorations du chapitre 4.

Les quatre niveaux de gravité ont un sens précis pour l'utilisateur :

GravitéAffichageUsage
ErrorVague rougeLe code ne fonctionnera pas
WarningVague jauneProblème probable, à corriger
InformationVague bleueRemarque
HintTrois points gris discretsSuggestion, absent du panneau Problems

Un TODO sans ticket n'empêche rien de fonctionner : Warning suffit, Error serait abusif.

Proposer une correction rapide

L'ampoule (Ctrl+.) affiche les code actions. On propose d'insérer un modèle de référence à la place du TODO signalé :

src/quick-fix.ts
import * as vscode from 'vscode'
import { DIAGNOSTIC_CODE } from './diagnostics'
 
export class AddTicketFix implements vscode.CodeActionProvider {
  static readonly kinds = [vscode.CodeActionKind.QuickFix]
 
  provideCodeActions(
    document: vscode.TextDocument,
    _range: vscode.Range,
    context: vscode.CodeActionContext
  ): vscode.CodeAction[] {
    return context.diagnostics
      .filter((d) => d.code === DIAGNOSTIC_CODE)
      .map((diagnostic) => {
        const action = new vscode.CodeAction(
          'Ajouter une référence de ticket',
          vscode.CodeActionKind.QuickFix
        )
        action.edit = new vscode.WorkspaceEdit()
        action.edit.insert(document.uri, diagnostic.range.end, ' #000')
        action.diagnostics = [diagnostic]
        action.isPreferred = true
        return action
      })
  }
}
src/extension.ts
context.subscriptions.push(
  vscode.languages.registerCodeActionsProvider({ scheme: 'file' }, new AddTicketFix(), {
    providedCodeActionKinds: AddTicketFix.kinds,
  })
)

WorkspaceEdit décrit la modification sans l'appliquer : VS Code l'exécute quand l'utilisateur choisit l'action, et elle s'annule avec Ctrl+Z comme une frappe normale. isPreferred permet de l'appliquer directement avec la commande Auto Fix.

Le premier argument, { scheme: 'file' }, est un sélecteur de document. Il restreint le fournisseur à certains fichiers : { language: 'typescript' }, { pattern: '**/*.test.ts' }, ou une combinaison.

Complétion

Quand l'utilisateur tape TODO #, on lui suggère les numéros de tickets récemment utilisés dans le projet. Le fournisseur reçoit le document et la position du curseur :

src/completion.ts
import * as vscode from 'vscode'
 
export function registerTicketCompletion(
  context: vscode.ExtensionContext,
  recentTickets: () => string[]
) {
  const provider: vscode.CompletionItemProvider = {
    provideCompletionItems(document, position) {
      const before = document.lineAt(position).text.slice(0, position.character)
      if (!/\bTODO\s+#$/.test(before)) return undefined
 
      return recentTickets().map((ticket, index) => {
        const item = new vscode.CompletionItem(ticket, vscode.CompletionItemKind.Reference)
        item.insertText = ticket.replace('#', '')
        item.detail = 'Ticket déjà référencé dans le projet'
        item.sortText = String(index).padStart(3, '0')
        return item
      })
    },
  }
 
  context.subscriptions.push(
    vscode.languages.registerCompletionItemProvider({ scheme: 'file' }, provider, '#')
  )
}

Le dernier argument, '#', est un caractère déclencheur : la liste s'ouvre dès qu'il est tapé, sans attendre Ctrl+Espace. sortText contrôle l'ordre, sinon VS Code trie par libellé.

Survol

Un HoverProvider affiche une info-bulle au survol. Pour TODO Radar : l'âge du TODO, tiré de Git, serait idéal, mais on se contente ici d'un rappel de la règle et d'un lien.

vscode.languages.registerHoverProvider({ scheme: 'file' }, {
  provideHover(document, position) {
    const range = document.getWordRangeAtPosition(position, /\b(TODO|FIXME|HACK)\b/)
    if (!range) return undefined
 
    const markdown = new vscode.MarkdownString(
      `**${document.getText(range)}** suivi par TODO Radar\n\n` +
        '[Voir tous les marqueurs](command:todoRadar.list.focus)'
    )
    markdown.isTrusted = true
    return new vscode.Hover(markdown, range)
  },
})

isTrusted autorise les liens command: dans le Markdown. Sans lui, ils sont désactivés : un texte de document ne doit pas pouvoir déclencher de commande à votre insu.

Snippets et grammaires : sans une ligne de code

Certaines fonctions de langage sont purement déclaratives. Un fichier de snippets :

snippets/todo.code-snippets
{
  "TODO avec ticket": {
    "prefix": "todo",
    "body": ["// TODO #${1:ticket} : ${2:description}"],
    "description": "Commentaire TODO avec référence de ticket"
  }
}
package.json
"snippets": [
  { "language": "typescript", "path": "./snippets/todo.code-snippets" },
  { "language": "javascript", "path": "./snippets/todo.code-snippets" }
]

${1:ticket} crée un point d'arrêt de tabulation avec une valeur par défaut. Tab passe au suivant.

Pour la coloration syntaxique d'un nouveau langage, on déclare languages et grammars avec une grammaire TextMate (fichier JSON d'expressions régulières). C'est un sujet à part entière, peu utile pour TODO Radar.

Le Language Server Protocol

Tout ce chapitre s'exécute dans l'extension host. Pour un vrai langage (analyse de tout le projet, typage, renommage), on déporte le travail dans un serveur de langage, un processus séparé qui parle le Language Server Protocol (LSP).

ApprocheAvantagesInconvénients
Fournisseurs dans l'extensionSimple, une seule base de codeBloque l'extension host si le calcul est lourd, réservé à VS Code
Serveur LSPProcessus isolé, serveur réutilisable dans Neovim, Zed, JetBrains, HelixDeux projets à maintenir, protocole à apprendre

Microsoft fournit vscode-languageclient côté extension et vscode-languageserver côté serveur, et un exemple complet (lsp-sample dans le dépôt microsoft/vscode-extension-samples). Le serveur peut être écrit dans n'importe quel langage : gopls est en Go, rust-analyzer en Rust. L'idée est proche de celle du Model Context Protocol côté IA : un protocole standard pour qu'un outil serve plusieurs clients.

Pour TODO Radar, des fournisseurs directs suffisent largement.


L'extension est complète fonctionnellement. Avant de la publier, il faut la tester et réduire son paquet à l'essentiel : c'est le chapitre suivant.

Précédent
Webviews : un panneau HTML dans l'éditeur
Suivant
Tester et bundler l'extension

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