diff options
| author | eunhhu <[email protected]> | 2026-09-26 11:01:21 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-26 11:01:21 +0100 |
| commit | acbd39e3e838fe49b17e32fe254af9ea12b4f81b (patch) | |
| tree | 64a91b85cd4249f52e6f1ab5faae0142627c599a | |
| parent | 91332801ff2334cd887df3971f944b60bebf729c (diff) | |
| download | aup-acbd39e3e838fe49b17e32fe254af9ea12b4f81b.tar.gz aup-acbd39e3e838fe49b17e32fe254af9ea12b4f81b.zip | |
Declare marks: cursors that stay before inserted text
`state from: mark in note` is a cursor that stays before text inserted
at its position; `cursor in` goes after it, as a caret does. Everything
a cursor takes, a mark takes too. The notes example's mark is now one,
and its reference model agrees: typing at the mark leaves it in place.
| -rw-r--r-- | docs/design/002-dsl-direction.md | 4 | ||||
| -rw-r--r-- | examples/notes.aup | 5 | ||||
| -rw-r--r-- | src/compile.c | 12 | ||||
| -rw-r--r-- | tests/check.py | 3 |
4 files changed, 17 insertions, 7 deletions
diff --git a/docs/design/002-dsl-direction.md b/docs/design/002-dsl-direction.md index 7d38d55..3692866 100644 --- a/docs/design/002-dsl-direction.md +++ b/docs/design/002-dsl-direction.md @@ -30,6 +30,7 @@ aup는 범용 언어가 아니라 **무엇이 무엇에 연결되는지를 선� state title: text = "할 일" state note: text = "" # 한 줄이든 1 MiB 문서든 같은 타입 state at: cursor in note # 항상 글자 경계에 있고 편집을 따라간다 +state from: mark in note # 같지만, 이 자리에 넣은 글보다 앞에 남는다 input typed = text # 호스트가 준 확정 입력(IME 조합 완료분) input back = key Backspace @@ -49,6 +50,9 @@ view current = note.line(at) # 커서가 있는 줄. 복사하지 않� `cursor in 값` 타입을 둔다. 커서는 항상 글자 경계에 있고, 범위를 벗어나지 않으며, 같은 이벤트의 편집에 맞춰 런타임이 옮긴다. 이동은 단위로 쓴다: `at + 1 char`, `at - 1 line`, `note.start`, `note.end`. 범위는 `a .. b`, 부분은 `note[a .. b]`, 줄은 `note.line(at)`이다. 모두 복사 없는 보기다. +- **커서와 마크.** 둘 다 편집을 따라간다. 차이는 그 자리에 글을 넣을 때뿐이다. 커서(캐럿)는 넣은 글 뒤로 가고, + 마크(기억해 둔 위치, 선택의 시작점, 책갈피)는 글 앞에 남는다. 지울 때는 둘 다 지운 범위의 처음으로 모인다. + 타입은 같아서 커서에 쓰는 모든 것(이동, 비교, 범위, `insert` 위치)에 마크도 쓴다. - **글자 단위.** 목표는 사용자가 한 글자로 보는 단위(grapheme cluster)다. 첫 구현의 `char`는 Unicode scalar이고, 분절 표(UAX #29)는 크기 비용이 있어 선택 모듈로 붙인다. `word`도 그때 추가한다. - **편집.** `insert`, `remove`, `replace(a .. b, s)`는 Pure draft의 splice가 된다. 이벤트가 성공하면 커밋된다. diff --git a/examples/notes.aup b/examples/notes.aup index fdc91b8..214b617 100644 --- a/examples/notes.aup +++ b/examples/notes.aup @@ -1,10 +1,11 @@ # A small text editor. The runtime owns the text: typing inserts at the # cursor, the cursor and the mark always sit between characters and follow -# edits, and a failed event would leave everything as it was. +# edits (text typed at the mark goes after it: the mark stays), and a failed +# event would leave everything as it was. state title: text = "메모" state note: text = "" state at: cursor in note -state mark: cursor in note +state mark: mark in note input typed = text input left = key ArrowLeft diff --git a/src/compile.c b/src/compile.c index c2e5ff2..234991f 100644 --- a/src/compile.c +++ b/src/compile.c @@ -119,6 +119,7 @@ typedef struct { /* Declarations in source order. */ char state_names[STATES][NAME], state_target[STATES][NAME]; int state_types[STATES], state_line[STATES], state_column[STATES]; + bool state_stays[STATES]; /* a mark: a cursor that stays before text inserted at it */ double initial[STATES]; Expr *state_text[STATES]; unsigned states; @@ -987,15 +988,17 @@ static void state_declaration(Compiler *c) { c->state_column[index] = name.column; if (is(c, ":")) { take(c); - Token type = expect_name(c, "text, cursor, list or index"); + Token type = expect_name(c, "text, cursor, mark, list or index"); if (!strcmp(type.text, "text")) { c->state_types[index] = STATE_TEXT; if (is(c, "=")) { take(c); c->state_text[index] = plain_string(c, "a text state"); } - } else if (!strcmp(type.text, "cursor")) { + } else if (!strcmp(type.text, "cursor") || !strcmp(type.text, "mark")) { + /* Both follow edits; text inserted at a cursor goes before it, at a mark after it. */ c->state_types[index] = STATE_CURSOR; + c->state_stays[index] = type.text[0] == 'm'; expect_word(c, "in"); Token target = expect_name(c, "a text state"); memcpy(c->state_target[index], target.text, NAME); @@ -1005,7 +1008,8 @@ static void state_declaration(Compiler *c) { Token target = expect_name(c, type.text[0] == 'l' ? "a record" : "a list state"); memcpy(c->state_target[index], target.text, NAME); } else fail_at(c, type.line, type.column, - "state types are numbers, text, cursor in a text, list of a record, index of a list, and bytes by key"); + "state types are numbers, text, cursor or mark in a text, list of a record, index of a list, " + "and bytes by key"); ++c->states; return; } @@ -1861,7 +1865,7 @@ static void generate(Compiler *c, unsigned char **module, size_t *size) { p = put32(p, state_name[i]); p = put32(p, (uint32_t)c->state_types[i]); if (c->state_types[i] == STATE_NUM) p = put_double(p, c->initial[i]); - else { p = put32(p, state_ref[i]); p = put32(p, 0); } + else { p = put32(p, state_ref[i]); p = put32(p, c->state_stays[i] ? 1 : 0); } /* 1: the cursor stays */ } for (unsigned i = 0; i < c->collections; ++i) { p = put32(p, collection_name[i]); p = put32(p, 1); p = put32(p, 1); } unsigned first = 0; diff --git a/tests/check.py b/tests/check.py index c4484c4..7f94235 100644 --- a/tests/check.py +++ b/tests/check.py @@ -445,7 +445,8 @@ class NotesModel: def insert(self, text): p = self.at self.note = self.note[:p] + text + self.note[p:] - self.at, self.mark = [c + len(text) if c >= p else c for c in (self.at, self.mark)] + self.at = self.at + len(text) if self.at >= p else self.at + self.mark = self.mark + len(text) if self.mark > p else self.mark # a mark stays at the insertion point def remove(self, a, b): self.note = self.note[:a] + self.note[b:] |
