feat(completion): suggest dates in date value positions
Some checks failed
Deploy to GitHub Pages / build (push) Failing after 22s
Deploy to GitHub Pages / deploy (push) Has been skipped

The TOML editor offered nothing when typing a date, leaving the
yyyy-mm-dd format undiscoverable until a parse error. Suggest today
(a full, easy-to-edit date) and the current-year prefix on empty
date values and fresh reestimates elements, backing off once a date
is being typed.
This commit is contained in:
Julien Calixte
2026-06-19 18:43:41 +02:00
parent 289dde523f
commit e42987a070
2 changed files with 78 additions and 2 deletions

View File

@@ -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", () => { describe("completion replace range", () => {
it("replaces the typed prefix, not the whole line", () => { it("replaces the typed prefix, not the whole line", () => {
const source = "[[feature]]\nst" const source = "[[feature]]\nst"

View File

@@ -53,8 +53,13 @@ const HEADERS: Completion[] = [
const STATUSES = ["on-track", "at-risk", "off-track"] // keep in sync with parse.ts 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. */ /** What to suggest at `caret` in `source`, or null when nothing applies.
export function getCompletions(source: string, caret: number): CompletionContext | null { * `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 lineStart = source.lastIndexOf("\n", caret - 1) + 1
const linePrefix = source.slice(lineStart, caret) 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) return result(lineStart + status[1].length, caret, items)
} }
// ── value: a date field — start = <here>, week = <here>, … ────────────────
// 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 = [ … <here> ] — 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) const block = currentBlock(source, lineStart)
// ── value: requires = [ "<here>" … ] (milestones only) ─────────────────── // ── value: requires = [ "<here>" … ] (milestones only) ───────────────────
@@ -101,6 +119,20 @@ function result(from: number, to: number, items: Completion[]): CompletionContex
return items.length ? { from, to, items } : null 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[] { function filter(items: Completion[], word: string): Completion[] {
if (!word) return items if (!word) return items
const w = word.toLowerCase() const w = word.toLowerCase()