Aller au contenu

Marqueurs de ligne et clic dans la marge

Un marqueur de ligne est un glyphe dans la colonne des signets de l'éditeur et/ou une teinte sur toute la ligne, attaché à une ligne d'un document et emporté avec elle quand l'utilisateur modifie le texte. Les points d'arrêt (pleins, creux, conditionnels), une flèche d'instruction courante et les bandes de modifications du contrôle de version sont tous des marqueurs de ligne. Atteignez le service via IPluginContext::lineMarkers() (PluginApi/ILineMarkerService.h).

Le plugin décrit à quoi ressemble un type de marqueur et où se trouvent les marqueurs de ce type ; le dessin, la marge et les emplacements de marqueurs du moteur d'édition appartiennent à l'éditeur. Aucune taille en pixels, aucun numéro du moteur ni aucun type Qt ne franchit la frontière.

Types

Enregistrez chaque type de marqueur une seule fois, dans initialize(), et conservez le jeton :

enum class LineMarkerGlyph : std::int32_t {
    None, Circle, CircleHollow, CircleBar, Arrow, Strip
};

struct LineMarkerKind {
    LineMarkerGlyph glyph = LineMarkerGlyph::Circle;
    std::string     glyphColor;  // a theme category you registered
    std::string     lineTint;    // a theme category; empty = no tint
    std::int32_t    zOrder = 0;  // higher draws above lower
};

m_breakpoint = ctx.lineMarkers().registerKind(kind);   // LineMarkerKindToken
if (!m_breakpoint.valid()) { /* no slot left: see below */ }
  • Les couleurs sont des identifiants de catégories de thème que vous avez vous-même enregistrées avec ctx.themePalette() : les marqueurs suivent ainsi le thème actif et l'utilisateur peut les recolorer dans l'éditeur de thèmes ; l'éditeur les réapplique à chaque changement de thème.
  • Les glyphes forment un petit ensemble fixe que tout hôte sait dessiner dans une marge étroite à n'importe quel zoom : Circle (un point d'arrêt), CircleHollow (un point d'arrêt désactivé ou non vérifié), CircleBar (un point d'arrêt conditionnel), Arrow (l'instruction courante), Strip (une ligne modifiée). None crée un type qui ne fait que teinter.
  • zOrder décide quel marqueur est dessiné au-dessus quand plusieurs partagent une ligne (la flèche de l'instruction courante au-dessus d'un point d'arrêt, un point d'arrêt au-dessus d'une bande de modifications) ; à valeur égale, le type enregistré le plus tard passe au-dessus. L'ordre vaut aussi entre plugins.
  • L'enregistrement peut échouer. Le moteur d'édition dispose d'un petit nombre fixe d'emplacements de marqueurs partagés par tous les plugins — neuf ; un type avec un glyphe et une teinte en utilise deux. Quand ils sont épuisés, le jeton revient vide et le journal dit pourquoi. Enregistrez une seule fois les types dont vous avez besoin et conservez-les ; n'enregistrez jamais un type par document ou par marqueur.
  • Détruire le jeton supprime tous les marqueurs de ce type de tous les documents et retire le type.

Marqueurs

using LineMarkerId = std::uint64_t;                 // 0 is never valid

LineMarkerId id = ctx.lineMarkers().addMarker(m_breakpoint.id(), doc, line);
std::int64_t now = ctx.lineMarkers().lineOf(id);     // -1 = gone
auto all = ctx.lineMarkers().markers(m_breakpoint.id(), doc); // {id, line} by line
ctx.lineMarkers().removeMarker(id);
ctx.lineMarkers().clearMarkers(m_breakpoint.id(), 0);           // 0 = every document

addMarker renvoie 0 — et ne place rien — lorsque le type n'est pas vivant, que le document n'est pas ouvert ou que la ligne est hors du document. Une ligne n'est pas bornée : un point d'arrêt sur la mauvaise ligne est pire qu'aucun. Un identifiant est une simple valeur, pas un jeton ; gardez-en autant qu'il vous faut.

Ce qui arrive ensuite à un marqueur :

  • Les modifications le déplacent avec sa ligne : des lignes insérées ou supprimées au-dessus le décalent, taper sur la ligne le conserve — c'est ainsi qu'un point d'arrêt suit le code.
  • Sa ligne est supprimée (elle fait partie d'une plage supprimée) : il survit et rejoint la ligne où commençait la plage supprimée. Deux marqueurs peuvent ainsi se retrouver sur une même ligne.
  • Tout le tampon est remplacé (un rechargement après une modification externe, IEditorService::setText) : chaque marqueur reste sur le même numéro de ligne ; celui dont la ligne n'existe plus est supprimé. L'éditeur ne compare pas l'ancien et le nouveau texte.
  • Le document est fermé : ses marqueurs sont supprimés et leurs identifiants deviennent caducs. L'éditeur ne conserve jamais les marqueurs — un plugin qui garde des marques au-delà d'une fermeture (des points d'arrêt) les tient dans son propre modèle et les rajoute à DocumentOpened (un fichier rouvert est un nouveau DocumentId).

Les marqueurs appartiennent au document : placés une fois, ils apparaissent dans chaque volet d'un document scindé. La minicarte, son aperçu au survol et la vue de comparaison ne les affichent jamais. Les signets ne sont pas des marqueurs de ce service ; ils gardent leurs propres appels IEditorService.

Le clic dans la marge

Chaque clic gauche dans la colonne des signets d'un éditeur est publié sous la forme de events::MarginClicked :

struct MarginClicked {
    DocumentId   doc;
    std::int64_t line;       // 0-based, level with the click
    EditorMargin margin;     // EditorMargin::Symbol
    KeyModifiers modifiers;  // key_modifier::kShift | kControl | kAlt | kMeta
};

Les noms des modificateurs suivent les chaînes de raccourcis portables : kControl est Ctrl sous Windows et Linux et Commande (⌘) sous macOS, kAlt est Alt / Option (⌥), kMeta est la touche Windows / Contrôle (⌃) sous macOS. Les clics dans la colonne de repli replient et les clics sur les numéros de ligne sélectionnent ; ni les uns ni les autres ne sont publiés.

Par défaut, l'éditeur bascule aussi un signet sur la ligne cliquée, comme il l'a toujours fait. Un plugin qui donne au clic un sens propre — un débogueur qui bascule un point d'arrêt d'un simple clic et modifie sa condition d'un clic avec modificateur — le revendique :

m_marginClaim = ctx.lineMarkers().claimMarginClick();   // MarginClickToken
m_clicks = ctx.events().subscribe<events::MarginClicked>(
    [this](const events::MarginClicked& e) { onMarginClick(e); });

Tant qu'au moins une revendication est vivante — de n'importe quel plugin —, l'éditeur laisse le clic aux plugins et se contente de publier l'événement ; détruire le dernier jeton rétablit la bascule des signets. L'événement est publié que quelqu'un détienne une revendication ou non : un plugin qui ne fait qu'observer les clics n'en a besoin d'aucune. Les commandes des signets et leurs raccourcis ne sont pas affectés. Les revendications sont comptées, pas mises en file : deux plugins qui revendiquent tous deux le clic le reçoivent tous deux et ne doivent pas tous deux y réagir.

Python

Le même service est ctx.line_markers pour les plugins Python, et le clic est @ctx.events.on_margin_clicked.