Skip to content

Commit 58b259a

Browse files
authored
Merge pull request #107 from PlotJuggler/gor/dialog-caret-0.5
feat(dialog): code-editor caret protocol for cursor-aware completion (0.5.1)
2 parents aa1dcbe + d2745e0 commit 58b259a

13 files changed

Lines changed: 148 additions & 7 deletions

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");

pj_plugins/dialog_protocol/tests/dialog_plugin_typed_test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ class RecordingPlugin : public PJ::DialogPluginTyped {
9595
return true;
9696
}
9797

98+
bool onCodeChangedWithCursor(std::string_view widget_name, std::string_view code, int cursor) override {
99+
last_handler = "code_changed";
100+
last_widget = std::string(widget_name);
101+
last_text = std::string(code);
102+
last_int = cursor;
103+
return true;
104+
}
105+
98106
// Recorded state
99107
std::string last_handler;
100108
std::string last_widget;
@@ -241,3 +249,16 @@ TEST_F(TypedDispatchTest, CurrentIndexTakesPriorityOverValue) {
241249
EXPECT_TRUE(dispatch(plugin, "w", R"({"current_index": 1, "value": 5})"));
242250
EXPECT_EQ(plugin.last_handler, "index_changed");
243251
}
252+
253+
TEST_F(TypedDispatchTest, CodeChangedCarriesCursorToTypedHandler) {
254+
EXPECT_TRUE(dispatch(plugin, "editor", R"({"code_changed": "robot ==", "code_cursor": 8})"));
255+
EXPECT_EQ(plugin.last_handler, "code_changed");
256+
EXPECT_EQ(plugin.last_text, "robot ==");
257+
EXPECT_EQ(plugin.last_int, 8);
258+
}
259+
260+
TEST_F(TypedDispatchTest, CodeChangedWithoutCursorPassesNegativeOne) {
261+
EXPECT_TRUE(dispatch(plugin, "editor", R"({"code_changed": "x"})"));
262+
EXPECT_EQ(plugin.last_handler, "code_changed");
263+
EXPECT_EQ(plugin.last_int, -1);
264+
}

pj_plugins/dialog_protocol/tests/widget_data_test.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,25 @@ TEST(WidgetDataTest, Chaining) {
249249
EXPECT_EQ(j["port"]["value"], 80);
250250
EXPECT_EQ(j["tls"]["checked"], true);
251251
}
252+
253+
TEST(WidgetDataTest, SetCodeCursor) {
254+
WidgetData wd;
255+
wd.setCodeContent("editor", "robot ==").setCodeCursor("editor", 8);
256+
auto j = parse(wd);
257+
EXPECT_EQ(j["editor"]["code_content"], "robot ==");
258+
EXPECT_EQ(j["editor"]["code_cursor"], 8);
259+
}
260+
261+
TEST(WidgetDataTest, SetCodeCaretTracking) {
262+
WidgetData wd;
263+
wd.setCodeCaretTracking("editor");
264+
auto j = parse(wd);
265+
EXPECT_EQ(j["editor"]["code_caret_tracking"], true);
266+
}
267+
268+
TEST(WidgetDataTest, SetCodeCaretTrackingExplicitFalse) {
269+
WidgetData wd;
270+
wd.setCodeCaretTracking("editor", false);
271+
auto j = parse(wd);
272+
EXPECT_EQ(j["editor"]["code_caret_tracking"], false);
273+
}

0 commit comments

Comments
 (0)