From d9cfc05367c7c32daf914d866e2a903fae0cc92e Mon Sep 17 00:00:00 2001 From: Dennis Decoene Date: Sun, 12 Jul 2026 11:13:23 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(#59):=20field-bound=20@=20SAY=20GET=20?= =?UTF-8?q?=E2=80=94=20fields=20shadow=20memvars,=20rowid=20captured=20at?= =?UTF-8?q?=20GET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interpreter/Executor.ts | 35 ++++++++++- src/shared/types.ts | 9 +++ tests/FormFieldBinding.test.ts | 107 +++++++++++++++++++++++++++++++++ tests/Session.test.ts | 28 ++++----- 4 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 tests/FormFieldBinding.test.ts diff --git a/src/interpreter/Executor.ts b/src/interpreter/Executor.ts index fdac08d..f15283a 100644 --- a/src/interpreter/Executor.ts +++ b/src/interpreter/Executor.ts @@ -557,8 +557,39 @@ export class Executor implements IndexCommandsHost { const row = Number(this.evalExpr(rowE)); const col = Number(this.evalExpr(colE)); const text = String(this.evalExpr(textE)); - this.pendingForm.push({ row, col, label: text, varName }); - return { output: [] }; + const out: OutputLine[] = []; + + // Field binding: a GET whose name matches a column of the active table edits + // the current record (dBASE III behavior — fields shadow memory variables; + // this is why the m_ prefix convention exists). Capturing the rowid here means + // form-submit writes by rowid, like grid-edit — pointer motion between here + // and submit cannot retarget the write. + if (this.area.table) { + const cols = await this.db.getStructure(this.area.table); + const match = cols.find(c => c.name.toUpperCase() === varName.toUpperCase()); + if (match) { + const cur = await this.fetchCurrentRow(); + if (!cur) throw new Error(`GET ${match.name}: no current record`); + const field: FormField = { + row, col, label: text, varName: match.name, + target: { + kind: 'field', column: match.name, table: this.area.table, + db: this.area.db ?? '', rowid: Number(cur._rowid), + }, + value: String(cur[match.name] ?? ''), + }; + const info = this.columnMetaStore?.getColumnType(this.metaDb, this.area.table, match.name); + if (info?.lookup) { + const options = await resolveLookup(this.db, info.lookup); + if (options) field.options = options; + else out.push({ text: `** Warning: lookup for ${match.name} could not be resolved — free entry`, cls: 'warn' }); + } + this.pendingForm.push(field); + return { output: out }; + } + } + this.pendingForm.push({ row, col, label: text, varName, target: { kind: 'var' }, value: '' }); + return { output: out }; } private doRead(): ExecResult { diff --git a/src/shared/types.ts b/src/shared/types.ts index 9801959..ed458f0 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -13,7 +13,16 @@ export interface FormField { row: number; col: number; label: string; + /** The submit key. For a field-bound GET this is the column name. */ varName: string; + /** What form-submit writes. Absent (legacy INPUT/@SAY paths) means 'var'. */ + target?: + | { kind: 'var' } + | { kind: 'field'; column: string; table: string; db: string; rowid: number }; + /** Prefill. Field GETs carry the record's value; var GETs stay '' (unchanged UX). */ + value?: string; + /** Resolved lookup options — render a