Skip to content

Commit 488efc7

Browse files
Merge branch 'sync/main-to-internal-main-2026-06-01' into 'internal_main'
chore(sync): plotjuggler/main → internal_main 2026-06-01 (dialog caret + 0.5.1 + conan CI) See merge request client-projets/p.2026-plotjuggler/plotjuggler_core!190
2 parents 44db75d + 30dd176 commit 488efc7

14 files changed

Lines changed: 301 additions & 25 deletions

.github/workflows/release.yml

Lines changed: 153 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
name: Release
22

33
# Triggers on tag push (e.g. `git push origin v0.2.0`). Builds the Conan
4-
# package, uploads it to the project's Cloudsmith remote, and publishes a
5-
# GitHub Release with auto-generated notes.
4+
# package on every consumer platform (Linux, macOS, Windows), uploads each
5+
# binary to the project's Cloudsmith remote, and publishes a GitHub Release
6+
# with auto-generated notes.
67
#
7-
# Secrets required (set in repo Settings → Secrets and variables → Actions):
8-
# CLOUDSMITH_USER — Cloudsmith username (e.g. "davide-faconti")
9-
# CLOUDSMITH_API_KEY — Cloudsmith API key with write access to the
8+
# Why a matrix: Cloudsmith stores one binary package per platform/compiler
9+
# (package_id). A single ubuntu job only ever publishes a Linux/gcc binary, so
10+
# macOS and Windows consumers are forced to build plotjuggler_core from source
11+
# via `--build=missing`. That is slow and, on macOS specifically, fragile: a
12+
# from-source build depends on the recipe's exported sources being intact in
13+
# the cache, which breaks (e.g. after a version is re-published with a new
14+
# recipe revision) with "exports_sources but sources not found in local cache".
15+
# Publishing a binary per platform means consumers download instead of compile.
16+
#
17+
# Secrets required (set in repo Settings -> Secrets and variables -> Actions):
18+
# CLOUDSMITH_USER - Cloudsmith username (e.g. "davide-faconti")
19+
# CLOUDSMITH_API_KEY - Cloudsmith API key with write access to the
1020
# plotjuggler/plotjuggler repository
1121
#
12-
# Manual trigger: workflow_dispatch lets you re-run for an existing tag if
13-
# the first attempt failed (e.g. flaky upload). Set `tag` input to e.g. v0.1.0.
22+
# Manual trigger: workflow_dispatch lets you re-run for an existing tag if the
23+
# first attempt failed (e.g. flaky upload on one platform). Set `tag` input to
24+
# e.g. v0.1.0. NOTE: do not *move* an already-released tag to a new commit —
25+
# cut a new patch version instead. Moving a tag changes the recipe revision on
26+
# the remote and orphans any consumer mid-resolve. The `prepare` job enforces
27+
# this: it refuses to publish a version that already exists on Cloudsmith under
28+
# a different recipe revision (override with the `allow_republish` input only to
29+
# repair a botched release).
1430

1531
on:
1632
push:
@@ -21,16 +37,29 @@ on:
2137
tag:
2238
description: 'Tag to (re-)release (e.g. v0.1.0). Must already exist.'
2339
required: true
40+
allow_republish:
41+
description: 'Bypass the re-publish guard (only to repair a botched release of an existing version)'
42+
type: boolean
43+
default: false
44+
required: false
2445

2546
concurrency:
2647
group: release-${{ github.ref }}
2748
cancel-in-progress: false # never cancel an in-flight release
2849

2950
jobs:
30-
release:
51+
# ---------------------------------------------------------------------------
52+
# Resolve the version/tag once and verify it matches conanfile.py, so the
53+
# build matrix and the GitHub Release all agree on a single source of truth.
54+
# ---------------------------------------------------------------------------
55+
prepare:
3156
runs-on: ubuntu-22.04
3257
permissions:
33-
contents: write # required to create the GitHub Release
58+
contents: read
59+
outputs:
60+
ref: ${{ steps.ref.outputs.ref }}
61+
tag: ${{ steps.ref.outputs.tag }}
62+
version: ${{ steps.ref.outputs.version }}
3463
steps:
3564
- name: Resolve ref
3665
id: ref
@@ -54,11 +83,6 @@ jobs:
5483

5584
- uses: conan-io/setup-conan@v1
5685

57-
- name: Detect Conan profile
58-
run: |
59-
conan profile detect --force
60-
conan profile show
61-
6286
- name: Verify recipe version matches tag
6387
# Prevents the common footgun of tagging vX.Y.Z but forgetting to bump
6488
# `version` in conanfile.py. Fails fast before we publish.
@@ -70,7 +94,105 @@ jobs:
7094
fi
7195
echo "Version match: ${recipe_version}"
7296
97+
- name: Guard against re-publishing an existing version with different sources
98+
# The incident this matrix fixes was triggered by *moving* a released tag:
99+
# v0.5.0 was published twice from two different commits, so the second run
100+
# produced a new recipe revision that replaced the first and orphaned any
101+
# consumer that had resolved the original mid-build. This guard computes
102+
# the recipe revision THIS release would publish and compares it to what is
103+
# already on Cloudsmith for this version:
104+
# * version not published yet -> proceed (first release)
105+
# * same recipe revision present -> proceed (idempotent re-run after a
106+
# flaky/partial upload)
107+
# * a different revision present -> FAIL (sources changed under an
108+
# already-released version)
109+
# prepare runs on Linux (LF); the build matrix forces an LF checkout, so
110+
# both compute the same canonical recipe revision.
111+
env:
112+
CLOUDSMITH_USER: ${{ secrets.CLOUDSMITH_USER }}
113+
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
114+
run: |
115+
if [[ "${{ inputs.allow_republish }}" == "true" ]]; then
116+
echo "allow_republish=true -> skipping the re-publish guard"
117+
exit 0
118+
fi
119+
version="${{ steps.ref.outputs.version }}"
120+
121+
# Recipe revision this release would publish (deterministic from the
122+
# checked-out sources, independent of build settings).
123+
conan profile detect --force >/dev/null 2>&1 || true
124+
local_rrev=$(conan export . --format=json \
125+
| python3 -c "import json,sys; print(json.load(sys.stdin)['reference'].split('#',1)[1])")
126+
echo "This release would publish recipe revision: ${local_rrev}"
127+
128+
conan remote add plotjuggler-cloudsmith https://conan.cloudsmith.io/plotjuggler/plotjuggler --force
129+
if [[ -n "$CLOUDSMITH_USER" && -n "$CLOUDSMITH_API_KEY" ]]; then
130+
conan remote login plotjuggler-cloudsmith "$CLOUDSMITH_USER" -p "$CLOUDSMITH_API_KEY"
131+
fi
132+
133+
conan list "plotjuggler_core/${version}#*" -r plotjuggler-cloudsmith --format=json \
134+
> /tmp/remote_revs.json 2>/dev/null || true
135+
136+
if grep -q "RECIPEUNKNOWN" /tmp/remote_revs.json; then
137+
echo "Version ${version} is not yet published — first release, proceeding."
138+
elif grep -q '"error"' /tmp/remote_revs.json; then
139+
echo "::warning::Unexpected error reading published revisions for ${version} (transient?). Proceeding without the guard."
140+
cat /tmp/remote_revs.json
141+
elif grep -q "${local_rrev}" /tmp/remote_revs.json; then
142+
echo "Recipe revision ${local_rrev} is already published for ${version} — idempotent re-run, proceeding."
143+
else
144+
echo "::error::plotjuggler_core/${version} is already published with a different recipe revision."
145+
echo "::error::This build would publish ${local_rrev}, which is not on the remote. Refusing to overwrite a released version."
146+
echo "::error::Cut a new version, or re-run via workflow_dispatch with allow_republish=true to repair a botched release."
147+
echo "Currently published for ${version}:"
148+
conan list "plotjuggler_core/${version}#*" -r plotjuggler-cloudsmith 2>/dev/null || true
149+
exit 1
150+
fi
151+
152+
# ---------------------------------------------------------------------------
153+
# Build + upload one Conan binary per consumer platform. fail-fast: false so
154+
# a transient failure on one OS still publishes the others (re-run the
155+
# workflow_dispatch for the failed leg); the GitHub Release is gated on ALL
156+
# legs succeeding so we never advertise an incomplete release.
157+
# ---------------------------------------------------------------------------
158+
build:
159+
needs: prepare
160+
permissions:
161+
contents: read
162+
strategy:
163+
fail-fast: false
164+
matrix:
165+
os: [ubuntu-22.04, macos-15-intel, windows-latest]
166+
runs-on: ${{ matrix.os }}
167+
defaults:
168+
run:
169+
shell: bash # Git Bash on Windows; conan/cmake are on PATH everywhere
170+
steps:
171+
- name: Force LF line endings (consistent recipe revision across OSes)
172+
# No .gitattributes in the repo, so on Windows git would convert text
173+
# files to CRLF on checkout, changing the recipe revision hash and
174+
# splitting the package across two revisions. Force LF everywhere so every
175+
# matrix leg computes and uploads the SAME recipe revision (matching the
176+
# one prepare's guard validated on Linux).
177+
run: |
178+
git config --global core.autocrlf false
179+
git config --global core.eol lf
180+
181+
- uses: actions/checkout@v4
182+
with:
183+
ref: ${{ needs.prepare.outputs.ref }}
184+
185+
- uses: conan-io/setup-conan@v1
186+
187+
- name: Detect Conan profile
188+
run: |
189+
conan profile detect --force
190+
conan profile show
191+
73192
- name: Build Conan package
193+
# Same settings consumers use (-s build_type=Release -s
194+
# compiler.cppstd=20) so the published package_id matches what
195+
# downstream `conan install` resolves on each platform.
74196
run: |
75197
conan create . \
76198
--build=missing \
@@ -91,19 +213,32 @@ jobs:
91213
conan remote login plotjuggler-cloudsmith "$CLOUDSMITH_USER" -p "$CLOUDSMITH_API_KEY"
92214
93215
- name: Upload package to Cloudsmith
216+
# Each matrix leg uploads the recipe + its own binary. The recipe
217+
# revision is identical across platforms (same recipe content), so
218+
# concurrent recipe uploads are idempotent — only the per-platform
219+
# binary differs. `--check` verifies integrity before/after transfer.
94220
run: |
95-
conan upload "plotjuggler_core/${{ steps.ref.outputs.version }}" \
221+
conan upload "plotjuggler_core/${{ needs.prepare.outputs.version }}" \
96222
-r plotjuggler-cloudsmith \
97223
--confirm \
98224
--check
99225
226+
# ---------------------------------------------------------------------------
227+
# Cut the GitHub Release once, only after every platform binary is published.
228+
# ---------------------------------------------------------------------------
229+
github-release:
230+
needs: [prepare, build]
231+
runs-on: ubuntu-22.04
232+
permissions:
233+
contents: write # required to create the GitHub Release
234+
steps:
100235
- name: Create GitHub Release
101236
# softprops/action-gh-release: handles auto-generated notes + idempotent
102237
# re-runs (skips if a release for the tag already exists).
103238
uses: softprops/action-gh-release@v2
104239
with:
105-
tag_name: ${{ steps.ref.outputs.tag }}
106-
name: plotjuggler_core ${{ steps.ref.outputs.tag }}
240+
tag_name: ${{ needs.prepare.outputs.tag }}
241+
name: plotjuggler_core ${{ needs.prepare.outputs.tag }}
107242
generate_release_notes: true
108243
body: |
109244
## Install via Conan
@@ -115,7 +250,7 @@ jobs:
115250
Add to your `conanfile.py` / `conanfile.txt`:
116251
117252
```python
118-
requires = ("plotjuggler_core/${{ steps.ref.outputs.version }}",)
253+
requires = ("plotjuggler_core/${{ needs.prepare.outputs.version }}",)
119254
```
120255
121256
Link in CMake:

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ endif()
202202
if(PJ_INSTALL_SDK)
203203
include(CMakePackageConfigHelpers)
204204

205-
set(PJ_PACKAGE_VERSION "0.5.0")
205+
set(PJ_PACKAGE_VERSION "0.5.1")
206206
set(PJ_PACKAGE_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/plotjuggler_core)
207207

208208
install(EXPORT plotjuggler_coreTargets

conanfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
plugin_sdk — umbrella for plugin authors (base + dialog SDK + parser SDK)
88
plugin_host — umbrella for host loaders (data_source/parser/toolbox/dialog)
99
10-
A consuming Conan recipe declares e.g. `plotjuggler_core/0.5.0` and then:
10+
A consuming Conan recipe declares e.g. `plotjuggler_core/0.5.1` and then:
1111
1212
find_package(plotjuggler_core REQUIRED COMPONENTS plugin_sdk)
1313
target_link_libraries(my_plugin PRIVATE plotjuggler_core::plugin_sdk)
@@ -27,7 +27,7 @@
2727

2828
class PlotjugglerCoreConan(ConanFile):
2929
name = "plotjuggler_core"
30-
version = "0.5.0"
30+
version = "0.5.1"
3131
# Apache-2.0 covers pj_base + pj_plugins (the plugin-facing SDK);
3232
# MPL-2.0 covers pj_datastore (the storage engine). See LICENSE.
3333
license = "Apache-2.0 AND MPL-2.0"

docs/dialog-sdk-reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ For the full tutorial, see [dialog-plugin-guide.md](../pj_plugins/docs/dialog-pl
9090
| `setPlainText(name, text)` | Set plain text content |
9191
| `setCodeContent(name, code)` | Set editable code content |
9292
| `setCodeLanguage(name, lang)` | Set syntax highlighting language such as `"lua"` or `"python"` |
93+
| `setCodeCursor(name, cursor)` | Move the caret to byte offset `cursor` (e.g. after inserting a completion) |
94+
| `setCodeCaretTracking(name, enabled=true)` | Opt into caret tracking: report the caret on cursor moves too, not just edits |
9395

9496
### QTabWidget
9597

@@ -139,6 +141,7 @@ Override these in your `DialogPluginTyped` subclass. Return `true` when state ch
139141
| `onSelectionChanged(name, items)` | QListWidget, QTableWidget | Vector of selected item texts |
140142
| `onItemDoubleClicked(name, index)` | QListWidget, QTableWidget | Row index of double-clicked item |
141143
| `onCodeChanged(name, code)` | QPlainTextEdit code editor | Edited code |
144+
| `onCodeChangedWithCursor(name, code, cursor)` | QPlainTextEdit code editor | Edited code + caret offset (`cursor < 0` when no opt-in / not reported); defaults to `onCodeChanged` |
142145
| `onItemsDropped(name, items)` | Any widget with `setDropTarget` | Dropped item labels |
143146
| `onChartViewChanged(name, x_min, x_max, y_min, y_max)` | QFrame chart container | Visible chart range |
144147
| `onTabChanged(name, index)` | QTabWidget | New tab index |

pj_plugins/dialog_protocol/include/pj_plugins/host/widget_data_view.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,16 @@ class WidgetDataView {
209209
[[nodiscard]] std::optional<std::string> codeLanguage(std::string_view name) const {
210210
return getString(name, "code_language");
211211
}
212+
/// Requested caret offset (bytes) for a code editor; the host moves the caret
213+
/// here after applying new code content. Absent ⇒ leave the caret as-is.
214+
[[nodiscard]] std::optional<int> codeCursor(std::string_view name) const {
215+
return getInt(name, "code_cursor");
216+
}
217+
/// Whether this code editor opted into caret tracking (setCodeCaretTracking).
218+
/// Absent ⇒ not requested; the host wires cursor-move events only when true.
219+
[[nodiscard]] std::optional<bool> codeCaretTracking(std::string_view name) const {
220+
return getBool(name, "code_caret_tracking");
221+
}
212222

213223
// --- QLabel ---
214224
[[nodiscard]] std::optional<std::string> label(std::string_view name) const {

pj_plugins/dialog_protocol/include/pj_plugins/host/widget_event_builder.hpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,15 @@ struct WidgetEventBuilder {
106106
return j.dump();
107107
}
108108

109-
/// Code editor: code changed
110-
[[nodiscard]] static std::string codeChanged(std::string_view code) {
109+
/// Code editor: code changed. `cursor` is the caret offset (bytes) in the new
110+
/// text, or negative when unknown; it is serialized only when >= 0, so callers
111+
/// that omit it stay wire-compatible with readers that ignore the field.
112+
[[nodiscard]] static std::string codeChanged(std::string_view code, int cursor = -1) {
111113
nlohmann::json j;
112114
j["code_changed"] = code;
115+
if (cursor >= 0) {
116+
j["code_cursor"] = cursor;
117+
}
113118
return j.dump();
114119
}
115120

pj_plugins/dialog_protocol/include/pj_plugins/sdk/dialog_plugin_typed.hpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ class DialogPluginTyped : public DialogPluginBase {
7272
return false;
7373
}
7474

75+
/// Cursor-aware code change: `cursor` is the caret offset (bytes) in `code`,
76+
/// or negative when the host didn't report one. The dispatch always calls
77+
/// this; it defaults to onCodeChanged(name, code), so existing plugins keep
78+
/// working. Override this (instead of onCodeChanged) to drive caret-aware
79+
/// completion. A distinct name (rather than an overload) avoids the
80+
/// overloaded-virtual hiding hazard.
81+
///
82+
/// The caret is only reported (and cursor-only moves only fire this at all)
83+
/// for editors that opted in via WidgetData::setCodeCaretTracking. Without
84+
/// opt-in this fires on text changes only, with cursor < 0 — so an editor
85+
/// that merely validates code is not re-run on every cursor move.
86+
virtual bool onCodeChangedWithCursor(std::string_view widget_name, std::string_view code, int /*cursor*/) {
87+
return onCodeChanged(widget_name, code);
88+
}
89+
7590
virtual bool onItemsDropped(std::string_view /*widget_name*/, const std::vector<std::string>& /*items*/) {
7691
return false;
7792
}
@@ -113,7 +128,7 @@ class DialogPluginTyped : public DialogPluginBase {
113128
return onItemsDropped(widget_name, *v);
114129
}
115130
if (auto v = event.codeChanged()) {
116-
return onCodeChanged(widget_name, *v);
131+
return onCodeChangedWithCursor(widget_name, *v, event.codeCursor().value_or(-1));
117132
}
118133
if (auto v = event.text()) {
119134
return onTextChanged(widget_name, *v);

pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_data.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,24 @@ class WidgetData {
213213
return *this;
214214
}
215215

216+
/// Move the caret of a code editor to `cursor` (byte offset). Used after the
217+
/// plugin programmatically rewrites the code (e.g. inserting a completion) so
218+
/// the caret lands where the user expects rather than jumping to the start.
219+
WidgetData& setCodeCursor(std::string_view name, int cursor) {
220+
entry(name)["code_cursor"] = cursor;
221+
return *this;
222+
}
223+
224+
/// Opt this code editor into caret tracking. When enabled, the host reports
225+
/// the caret offset on cursor moves as well as text edits (via
226+
/// onCodeChangedWithCursor), so the plugin can drive caret-aware completion.
227+
/// Editors that don't opt in only fire on text changes — the default — so an
228+
/// editor that merely validates code isn't re-run on every cursor move.
229+
WidgetData& setCodeCaretTracking(std::string_view name, bool enabled = true) {
230+
entry(name)["code_caret_tracking"] = enabled;
231+
return *this;
232+
}
233+
216234
// --- QLabel ---
217235
WidgetData& setLabel(std::string_view name, std::string_view text) {
218236
entry(name)["label"] = text;

pj_plugins/dialog_protocol/include/pj_plugins/sdk/widget_event.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ class WidgetEvent {
106106
return getString("code_changed");
107107
}
108108

109+
/// Caret offset (bytes) accompanying a codeChanged event, when the host
110+
/// reported one. Absent for hosts/events that don't carry the cursor.
111+
std::optional<int> codeCursor() const {
112+
auto it = data_.find("code_cursor");
113+
if (it == data_.end() || !it->is_number_integer()) {
114+
return std::nullopt;
115+
}
116+
return it->get<int>();
117+
}
118+
109119
/// Drag-and-drop: items dropped on a widget (curves, files, or any draggable payload).
110120
std::optional<std::vector<std::string>> itemsDropped() const {
111121
auto it = data_.find("items_dropped");

0 commit comments

Comments
 (0)