From 6e50736c576b93c09345dc63488bbb95e9203c13 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sat, 26 Sep 2026 11:01:21 +0100 Subject: Let a cursor stay before text inserted at it (a mark) Text inserted at a cursor's position pushed every cursor there behind it. That suits a caret, not a remembered position: a mark, a selection's start or a bookmark should stay before the new text. A cursor state's reserved word in the module now carries a flag: 1 makes it a mark, which stays where text is inserted at it and otherwise follows edits as any cursor does. The flag is refused on other states and other values; existing modules, with 0 there, keep their meaning. --- docs/design/008-module-format.md | 4 +-- prototypes/connections/module.c | 8 ++++-- prototypes/connections/module.h | 4 +++ prototypes/connections/test-module.c | 49 +++++++++++++++++++++++++++++++++--- 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 6d1b32e..5474541 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -25,7 +25,7 @@ v5는 값을 가진 채 합류하는 분기(조건식)와, 리스트 항목마 | 16 | u32 명령 수, u32 문자열 바이트 수 | | 24 | u16 필드, u16 컬렉션, u16 상수, u16 레코드 | | 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 0 | -| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태, 리스트는 u32 레코드, 인덱스는 u32 대상 리스트 상태 | +| 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드, 인덱스는 u32 대상 리스트 상태 | | | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) | | | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text), u8 키별 여부, u16 0, u32 키 이름(키 입력) 또는 0, u16 첫 필드, u16 필드 수 | | | 필드 × 8 B: u32 이름, u32 타입(1 u32le, 2 바이트 꼬리) | @@ -127,7 +127,7 @@ v5는 값을 가진 채 합류하는 분기(조건식)와, 리스트 항목마 `PureApp`은 모듈 참조, 상태 값, text 상태 draft, 리스트, 열린 이벤트 하나를 가진다(3,128 B, `PureModule`은 상수 블롭을 포함해 28,600 B). text 상태는 저장소 파일이 없는 draft라 1 MiB 텍스트도 조각 트리로 편집한다. 커서는 바이트 -위치이며 항상 글자 경계에 있다. 넣기는 그 위치 이후의 커서를 뒤로(삽입 위치의 커서 포함) 밀고, 지우기는 범위 +위치이며 항상 글자 경계에 있다. 넣기는 그 위치 이후의 커서를 뒤로(삽입 위치의 커서 포함, 마크는 제자리) 밀고, 지우기는 범위 안의 커서를 범위 처음으로 모은다. 모든 이벤트에서 text draft를 잡아 두고, 실패하면 이벤트 전 버전으로 돌린다. 리스트는 런타임이 소유하는 항목 배열이고, 항목은 필드마다 수 또는 text 바이트를 가진다(최대 65,536 항목). 인덱스는 diff --git a/prototypes/connections/module.c b/prototypes/connections/module.c index 5e2b98f..7af1d5c 100644 --- a/prototypes/connections/module.c +++ b/prototypes/connections/module.c @@ -356,8 +356,11 @@ PURE_COLD PureResult pure_module_load(const void *bytes, size_t size, PureModule out->initial[i] = read_double(at + 8); if (!isfinite(out->initial[i])) REFUSE("non-finite initial value", i); } else { - if (read32(at + 12) || read32(at + 8) > UINT16_MAX) REFUSE("state reference", i); + uint32_t flags = read32(at + 12); + if ((flags && (out->state_type[i] != PURE_STATE_CURSOR || flags != PURE_CURSOR_STAYS)) || + read32(at + 8) > UINT16_MAX) REFUSE("state reference", i); out->state_ref[i] = (uint16_t)read32(at + 8); + out->state_flags[i] = (uint8_t)flags; } } for (uint32_t i = 0; i < out->collections; ++i, at += COLLECTION_BYTES) { @@ -648,7 +651,8 @@ static void move_cursors(const PureModule *m, double *values, uint32_t text, siz if (removed) { if (position >= at + removed) position -= (double)removed; else if (position > at) position = (double)at; - } else if (position >= at) position += (double)inserted; + } else if (position > at || (position == at && !(m->state_flags[c] & PURE_CURSOR_STAYS))) + position += (double)inserted; values[c] = position; } } diff --git a/prototypes/connections/module.h b/prototypes/connections/module.h index 16b755c..c27f836 100644 --- a/prototypes/connections/module.h +++ b/prototypes/connections/module.h @@ -46,6 +46,9 @@ typedef enum { PURE_FIELD_U32 = 1, PURE_FIELD_BYTES = 2 } PureFieldType; typedef enum { PURE_STATE_NUM = 0, PURE_STATE_TEXT = 1, PURE_STATE_CURSOR = 2, PURE_STATE_LIST = 3, PURE_STATE_INDEX = 4 } PureStateType; +/* A cursor that stays before text inserted at it (a mark); other cursors go + * after it (a caret). */ +enum { PURE_CURSOR_STAYS = 1 }; /* A table view has one row per item of a list; its columns are number or * text views evaluated for one row at a time. */ typedef enum { PURE_VIEW_NUM = 0, PURE_VIEW_TEXT = 1, PURE_VIEW_TABLE = 2 } PureViewType; @@ -80,6 +83,7 @@ typedef struct { char input_keys[PURE_MODULE_INPUTS][PURE_MODULE_NAME]; uint8_t state_type[PURE_MODULE_STATES]; uint16_t state_ref[PURE_MODULE_STATES]; /* text: constant; cursor: text; list: record; index: list */ + uint8_t state_flags[PURE_MODULE_STATES]; /* cursor: PURE_CURSOR_STAYS */ PureInputShape input[PURE_MODULE_INPUTS]; PureField field[PURE_MODULE_FIELDS]; double initial[PURE_MODULE_STATES]; diff --git a/prototypes/connections/test-module.c b/prototypes/connections/test-module.c index b0666fe..950fa88 100644 --- a/prototypes/connections/test-module.c +++ b/prototypes/connections/test-module.c @@ -33,7 +33,7 @@ typedef struct { uint8_t op; uint32_t arg; double value; } O; /* Text (type 1, ref = constant), cursor (type 2, ref = text state), list * (type 3, ref = record) and index (type 4, ref = list state) states follow * the number states; text views follow the number views. */ -typedef struct { const char *name; uint32_t type, ref; } T; +typedef struct { const char *name; uint32_t type, ref; } T; /* type: the state type, its flags from bit 8 */ /* A record field: a number and its default, or text (text = 1) whose * default is the constant numbered initial. */ typedef struct { const char *name; uint32_t text; double initial; } RF; @@ -108,7 +108,12 @@ static size_t build(unsigned char *out, Spec spec) { p += 40; size_t n = 0; for (size_t i = 0; i < spec.ns; ++i) { p = put32(p, offset[n++]); p = put32(p, 0); p = put_double(p, spec.s[i].initial); } - for (size_t i = 0; i < spec.nt; ++i) { p = put32(p, offset[n++]); p = put32(p, spec.t[i].type); p = put32(p, spec.t[i].ref); p = put32(p, 0); } + for (size_t i = 0; i < spec.nt; ++i) { + p = put32(p, offset[n++]); + p = put32(p, spec.t[i].type & 0xFF); + p = put32(p, spec.t[i].ref); + p = put32(p, spec.t[i].type >> 8); + } for (size_t i = 0; i < spec.nc; ++i) { p = put32(p, offset[n++]); p = put32(p, 1); p = put32(p, 1); } uint32_t field_offsets[64], field_start = 0; size_t nf = 0; @@ -1714,6 +1719,43 @@ static void deferred(void) { deferred_messages(); } +/* Text inserted at a cursor goes before it; at a mark (a cursor that stays), + * after it. Everywhere else, and when text is removed, both move alike. */ +static void marks(void) { + enum { MARK = 2 | PURE_CURSOR_STAYS << 8 }; + static const T textual[] = {{"note", 1, 0}, {"at", 2, 0}, {"mark", MARK, 0}}; + static const char *const constants[] = {"ab"}; + static const I inputs[] = {{"typed", NULL, PURE_KIND_TEXT, false, NULL, 0}, KEY("keep", "KeyK"), KEY("back", "Backspace")}; + static const O code[] = { + {CUL, 1, 0}, {TI, 0, 0}, {TINS, 0, 0}, /* 0 typed: at the cursor */ + {CUL, 1, 0}, {CS, 2, 0}, /* 3 keep: mark = at */ + {CUL, 1, 0}, {C, 0, -1}, {CM, 0, 0}, {CUL, 1, 0}, {TREM, 0, 0}, /* 5 back */ + }; + static const H handlers[] = {{1, 0, 3}, {3, 3, 2}, {5, 5, 5}}; + Spec spec = {NULL, 0, inputs, LEN(inputs), NULL, 0, handlers, LEN(handlers), code, LEN(code), + NULL, 0, textual, 3, constants, 1, NULL, 0, NULL, 0, NULL, 0}; + if (load(spec, &module_a) != PURE_OK) { fprintf(stderr, "%s\n", error); abort(); } + CHECK(module_a.state_flags[2] == PURE_CURSOR_STAYS && !module_a.state_flags[1]); + PureApp app; + CHECK(pure_app_start(&app, &module_a, (PureData){0}) == PURE_OK); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"x", 1}) == PURE_OK); + CHECK(!strcmp(state_text(&app, 0), "xab") && app.values[1] == 1 && app.values[2] == 0); /* the mark stayed */ + CHECK(pure_app_input(&app, 1, PURE_EDGE_DOWN) == PURE_OK && app.values[2] == 1); + CHECK(pure_app_text(&app, 0, (PureSpan){(const unsigned char *)"yz", 2}) == PURE_OK); + CHECK(!strcmp(state_text(&app, 0), "xyzab") && app.values[1] == 3 && app.values[2] == 1); + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 2 && app.values[2] == 1); + CHECK(pure_app_input(&app, 2, PURE_EDGE_DOWN) == PURE_OK && app.values[1] == 1 && app.values[2] == 1); /* gathered */ + pure_app_stop(&app); + /* A flag only on a cursor, and only this one. */ + static const T wrong_flag[] = {{"note", 1, 0}, {"at", 2 | 2 << 8, 0}, {"mark", MARK, 0}}; + static const T flagged_text[] = {{"note", 1 | PURE_CURSOR_STAYS << 8, 0}, {"at", 2, 0}, {"mark", 2, 0}}; + Spec bad = spec; + bad.t = wrong_flag; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); + bad.t = flagged_text; + CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "state reference")); +} + /* A host points at an item: the index a view shows as its position moves * there, as an event with no handler, saved like a key event; deferred, it * joins the burst and a failed save undoes it with the rest. */ @@ -1751,6 +1793,7 @@ int main(void) { messages(); message_mutations(); texts(); + marks(); text_mutations(); lists(); pointing(); @@ -1760,6 +1803,6 @@ int main(void) { stored_states(); deferred(); printf("{\"sizeof_module\":%zu,\"sizeof_app\":%zu}\n", sizeof(PureModule), sizeof(PureApp)); - puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing\"}"); + puts("{\"module_tests\":\"verified-load-atomic-events-faults-signed-zero-if-else-replace-by-name-rejections-mutations-messages-documents-typed-verification-text-cursors-utf8-views-rollback-replace-lists-indices-undo-migration-joins-tables-columns-pick-slide-stored-equivalence-crashes-frames-messages-migration-deferred-bursts-pointing-marks\"}"); return 0; } -- cgit v1.2.3