diff --git a/src/components/completion.test.ts b/src/components/completion.test.ts index 900e3ef..92d45f7 100644 --- a/src/components/completion.test.ts +++ b/src/components/completion.test.ts @@ -81,6 +81,50 @@ describe("completion context", () => { }) }) +describe("date completion", () => { + const TODAY = "2026-06-19" + const datesAt = (marked: string) => { + const [source, caret] = atCaret(marked) + return getCompletions(source, caret, TODAY) + } + + it("suggests today then the current-year prefix for an empty date value", () => { + const ctx = datesAt("[[feature]]\nstart = |")! + expect(ctx.items.map((i) => i.insert)).toEqual(["2026-06-19", "2026-"]) + expect(ctx.items.map((i) => i.detail)).toEqual(["today", "this year"]) + }) + + it("offers the same for plan dates and milestone weeks", () => { + expect(datesAt("end = |")!.items.map((i) => i.insert)).toEqual(["2026-06-19", "2026-"]) + expect(datesAt("[[milestone]]\nweek = |")!.items.map((i) => i.insert)).toEqual([ + "2026-06-19", + "2026-", + ]) + }) + + it("inserts at the caret, replacing nothing", () => { + const [source, caret] = atCaret("[[feature]]\ndelivered = |") + const ctx = getCompletions(source, caret, TODAY)! + expect([ctx.from, ctx.to]).toEqual([caret, caret]) + }) + + it("offers a date for a fresh element in a reestimates array", () => { + expect(datesAt("[[feature]]\nreestimates = [|")!.items.map((i) => i.insert)).toEqual([ + "2026-06-19", + "2026-", + ]) + expect( + datesAt("[[feature]]\nreestimates = [2026-01-01, |")!.items.map((i) => i.insert), + ).toEqual(["2026-06-19", "2026-"]) + }) + + it("stays out of the way once a date is being typed", () => { + expect(datesAt("[[feature]]\nstart = 2|")).toBeNull() + expect(datesAt("[[feature]]\nstart = 2026-|")).toBeNull() + expect(datesAt("[[feature]]\nreestimates = [2026-0|")).toBeNull() + }) +}) + describe("completion replace range", () => { it("replaces the typed prefix, not the whole line", () => { const source = "[[feature]]\nst" diff --git a/src/components/completion.ts b/src/components/completion.ts index 5310820..674042d 100644 --- a/src/components/completion.ts +++ b/src/components/completion.ts @@ -53,8 +53,13 @@ const HEADERS: Completion[] = [ const STATUSES = ["on-track", "at-risk", "off-track"] // keep in sync with parse.ts -/** What to suggest at `caret` in `source`, or null when nothing applies. */ -export function getCompletions(source: string, caret: number): CompletionContext | null { +/** What to suggest at `caret` in `source`, or null when nothing applies. + * `today` (yyyy-mm-dd) seeds the date suggestions; injectable for tests. */ +export function getCompletions( + source: string, + caret: number, + today: string = todayYmd(), +): CompletionContext | null { const lineStart = source.lastIndexOf("\n", caret - 1) + 1 const linePrefix = source.slice(lineStart, caret) @@ -68,6 +73,19 @@ export function getCompletions(source: string, caret: number): CompletionContext return result(lineStart + status[1].length, caret, items) } + // ── value: a date field — start = , week = , … ──────────────── + // Only on an empty value, so it never fights a date already being typed + // (the chained refresh after picking the key surfaces these immediately). + if (/^\s*(?:start|end|original|delivered|week)\s*=\s*$/.test(linePrefix)) { + return result(caret, caret, dateItems(today)) + } + + // ── value: reestimates = [ … ] — a date for a fresh array element ── + const inReestimates = /reestimates\s*=\s*\[[^\]]*$/.test(linePrefix) + if (inReestimates && /[[,]\s*$/.test(linePrefix)) { + return result(caret, caret, dateItems(today)) + } + const block = currentBlock(source, lineStart) // ── value: requires = [ "" … ] (milestones only) ─────────────────── @@ -101,6 +119,20 @@ function result(from: number, to: number, items: Completion[]): CompletionContex return items.length ? { from, to, items } : null } +/** Today as yyyy-mm-dd (UTC, matching `toYmd`). Injectable keeps tests stable. */ +function todayYmd(): string { + return new Date().toISOString().slice(0, 10) +} + +/** Date suggestions: the full date (easy to tweak the day/month in place) and + * the current-year prefix (accept it, then type the month-day). */ +function dateItems(today: string): Completion[] { + return [ + { label: today, insert: today, detail: "today" }, + { label: today.slice(0, 5), insert: today.slice(0, 5), detail: "this year" }, + ] +} + function filter(items: Completion[], word: string): Completion[] { if (!word) return items const w = word.toLowerCase()