Line markers & the margin click¶
A line marker is a glyph in the editor's bookmark column and/or a tint
across the whole line, attached to a line of a document and carried along as
the user edits. Breakpoints (filled, hollow, conditional), a current-statement
arrow and version-control change strips are all line markers. Reach the
service through IPluginContext::lineMarkers()
(PluginApi/ILineMarkerService.h).
The plugin describes what a kind of marker looks like and where markers of that kind sit; the editor owns the drawing, the margin and the editing engine's marker slots. No pixel size, engine number or Qt type crosses the boundary.
Kinds¶
Register each kind of marker once, at initialize(), and keep the token:
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 */ }
- Colours are ids of theme categories you registered yourself with
ctx.themePalette(), so markers follow the active theme and the user can recolour them in the Theme Editor; the editor re-applies them on every theme change. - Glyphs are a small fixed set every host can draw in a narrow margin at
any zoom:
Circle(a breakpoint),CircleHollow(a disabled or unverified breakpoint),CircleBar(a conditional breakpoint),Arrow(the current statement),Strip(a changed line).Nonemakes a tint-only kind. - zOrder decides which marker draws on top when several share a line (the current-statement arrow above a breakpoint, a breakpoint above a change strip); equal values stack the later-registered kind on top. The order holds across plugins.
- Registration can fail. The editing engine has a small, fixed number of marker slots shared by every plugin — nine; a kind with a glyph and a tint uses two. When they run out the token comes back empty and the log says why. Register the kinds you need once and keep them; never register a kind per document or per marker.
- Destroying the token removes every marker of that kind from every document and retires the kind.
Markers¶
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 returns 0 — and places nothing — when the kind is not live, the
document is not open, or the line is outside the document. A line is not
clamped: a breakpoint on the wrong line is worse than none. A handle is plain
data, not a token; hold as many as you need.
What happens to a marker afterwards:
- Edits move it with its line: lines inserted or deleted above shift it, typing on the line keeps it — this is how a breakpoint follows the code.
- Its line is deleted (part of a removed range): it survives and joins the line where the deleted range began. Two markers may end up on one line.
- The whole buffer is replaced (a reload after an outside change,
IEditorService::setText): each marker stays on the same line number; one whose line no longer exists is removed. The editor does not diff old and new text. - The document is closed: its markers are removed and their handles go
dead. The editor never persists markers — a plugin that keeps marks across
a close (breakpoints) keeps them in its own model and adds them again on
DocumentOpened(a reopened file is a newDocumentId).
Markers belong to the document: placed once, they show in every pane of a
split document. The minimap, its hover
preview and the compare view never show them. Bookmarks are not line markers
of this service; they keep their own IEditorService calls.
The margin click¶
Every left click in the bookmark column of an editor is published as
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
};
The modifier names follow the portable shortcut strings: kControl is Ctrl on
Windows and Linux and Command (⌘) on macOS, kAlt is Alt / Option (⌥),
kMeta is the Windows key / Control (⌃) on macOS. Clicks in the fold column
fold and clicks on the line numbers select; neither is published.
By default the editor also toggles a bookmark on the clicked line, as it always has. A plugin that gives the click a meaning of its own — a debugger toggling a breakpoint on a plain click and editing its condition on a modified one — claims it:
m_marginClaim = ctx.lineMarkers().claimMarginClick(); // MarginClickToken
m_clicks = ctx.events().subscribe<events::MarginClicked>(
[this](const events::MarginClicked& e) { onMarginClick(e); });
While at least one claim is alive — from any plugin — the editor leaves the click to the plugins and only publishes the event; destroying the last token brings the bookmark toggle back. The event is published whether or not anybody holds a claim, so a plugin that only watches clicks needs none. The bookmark commands and their shortcuts are unaffected. Claims are counted, not queued: two plugins that both claim the click both receive it, and must not both act on it.
Python¶
The same service is ctx.line_markers for Python plugins,
and the click is @ctx.events.on_margin_clicked.