super-productivity/docs/wiki/3.04-Short-Syntax.md
Kamil 808d093932
feat(add-task-bar): inline highlighting of detected short syntax (#9135)
* feat(short-syntax): parse recurrence phrases like @every friday in the add task bar

Adds a recurrence layer in front of the chrono-node date parsing: @every
<weekday|Nth|day|weekday|week|month|year> (with optional interval and time)
and the bare frequency words @daily/@weekly/@monthly/@yearly map onto the
existing repeat quick-setting presets of the add task bar, so the created
task gets a TaskRepeatCfg through the same code path as the repeat menu.

- @every friday [3pm] anchors the due date to the next matching weekday
- @every 15th anchors to the next matching day of month
- @every 2 weeks carries the interval into the created repeat cfg
- recurrence is only parsed in the add task bar (task creation); title
  edits of existing tasks keep treating the phrase as a plain date
- the repeat action button shows the parsed setting and clearing it also
  strips the phrase from the input

Closes #4961

* fix(short-syntax): allow trailing punctuation after recurrence phrases

'water plants @every friday.' fell through to plain-date parsing (chrono
tolerates the dot, the recurrence boundary check did not), silently
downgrading the repeat to a one-off Friday task. The phrase boundary now
accepts trailing punctuation as well as whitespace/end-of-input.

* fix(short-syntax): address review findings on recurrence parsing

- drop interval support ("@every 2 weeks"): the quick-setting presets all
  mean an interval of 1 - DAILY + repeatEvery>1 wrongly inherited
  skipOverdue=true (a missed occurrence would silently vanish), the repeat
  dialog silently reset the interval to 1 on any save, and
  MONDAY_TO_FRIDAY intervals produced 7-day-block schedules the app cannot
  express or display. Interval phrases now fall through to the plain-date
  path, and shortSyntax() returns a plain repeatQuickSetting instead of a
  repeatCfg object.
- only absorb a chrono match directly adjacent to the recurrence phrase, so
  "@every friday call mom tomorrow" no longer swallows everything between
  the phrase and a downstream date expression (up to and including the
  whole title)
- join tolerated trailing punctuation to the preceding word
  ("Water plants @every friday." -> "Water plants.", not "Water plants .")
  and avoid a double space when absorbing an adjacent date mid-title
- advance the anchor one period when an explicit time is already past
  ("@every friday 3pm" on a Friday at 17:00 -> next Friday 15:00, matching
  chrono's forwardDate behavior for the plain "@friday 3pm" form)

* feat(add-task-bar): highlight detected short syntax inline in the input

Renders a mirror overlay behind the add-task-bar textarea that paints a
background under every substring the short-syntax parser consumed (#tag,
+project, 30m/1h, @friday, @every friday, !deadline, extracted URLs), so a
mis-capture is visible while typing instead of after the title is saved
with text silently stripped (#8974, #6493).

- shortSyntax() now reports the exact consumed substrings as typed tokens;
  a pure mapper resolves them to positions in the raw input, skipping
  anything it cannot locate unambiguously (missing highlight over wrong
  highlight)
- ranges are pinned to the input string they were parsed from, so the
  async parse can never paint stale highlights on newer text
- overlay shares the textarea's typography via a common placeholder and
  mirrors its scroll position; textarea text stays the visible layer
- highlight color derives from --c-accent via color-mix, matching the
  --task-c-current-bg pattern

* fix(add-task-bar): keep highlight overlay text aligned with the textarea

Bare text nodes in the overlay picked up the template's indentation as a
literal collapsed space, shifting the mirrored text (and every highlight)
right by one space width. Render all segments as whitespace-tight spans;
whitespace-only nodes between elements are dropped entirely.

* fix(add-task-bar): mirror the textarea's computed typography onto the highlight overlay

Custom themes are arbitrary user CSS and can style the textarea and the
overlay div differently — glass.css for instance sets a font-family on
every element — so static CSS cannot keep the two layers metric-identical.
Copy all text-layout-affecting computed styles (font, spacing, kerning,
ligatures, tab-size) from the textarea onto the overlay on every segments
render and on theme switches, with a delayed re-sync to cover the async
stylesheet load of built-in themes.

* fix(add-task-bar): pin highlight segment spans to inherited text metrics

Theme CSS can match the overlay's segment spans directly — glass.css sets
font-family on '*' — and a direct rule beats inheritance, so the spans
rendered in the theme font while the textarea kept the component font,
desyncing every highlight position. Force the spans to inherit all
text-metric properties at component specificity, which outranks any
non-important theme rule on the spans.

* fix(short-syntax): track consumed spans through an offset map instead of recovering them by search

mapShortSyntaxTokensToRanges located tokens with rawText.indexOf, which guesses wrong whenever a token's text also occurs inside a tag, URL or word ('#1h retro 1h' highlighted the 1h inside the tag; a URL containing 30m absorbed the estimate highlight and evicted the correct URL range). Every strip site in the parser now routes its edit through TrackedTitle, a per-character map from working-title index to raw-title index, so shortSyntax() returns exact parsedRanges directly and the search plus its overlap heuristic are gone. A token whose consumed text is split by an earlier removal ('Call Bob @tomorrow 1h evening') now yields one range per contiguous run instead of silently dropping the highlight.

The 38 spec cases that asserted parsedTokens with jasmine.anything() now assert real ranges, plus regression tests for exactly the ambiguous inputs where searching guessed wrong.

* fix(add-task-bar): sync overlay typography per theme change, overlay scroll per render

The typography-sync effect read highlightSegments(), forcing a getComputedStyle style recalc plus 15 inline style writes on every keystroke with the task list rendered behind the bar; computed typography only changes on view init and theme switches, so the effect now tracks only those. The scroll sync moves the other way: (scroll) alone fires before the overlay re-renders once the textarea's caret-scroll kicks in past 4 rows, clamping scrollTop against still-short content, so it now also re-syncs after each segments render. Also mirrors direction, text-align and word-break onto the overlay.

* fix(short-syntax): derive the clear-repeat removal from the parser grammar

The clear-repeat button hand-wrote a second, broader grammar than the parser
uses, so it deleted text that was never read as a recurrence: "@every 2 weeks"
and "@every 3 days" (documented as unsupported, parsed as plain dates) lost
their unit word, "@every quarter" vanished entirely, and a trailing "." the
parser deliberately keeps was eaten. Both regexes now come from one grammar
source, re-anchored for removal.

Also fixes two anchoring bugs found in the same review:

- "@weekly"/"@monthly"/"@yearly" plus an already-passed time shifted the
  recurring day. The presets carry no anchor, so chrono's forwardDate slid the
  first occurrence to tomorrow and the cycle took its weekday/day-of-month from
  there — "@weekly 6am" typed Wednesday 10:00 recurred on Thursdays. They now
  pin to today and roll a whole period, like the explicit "@every wednesday 6am"
  form already did.
- getNextDayOfMonthDate's comment claimed months lacking the anchor day are
  skipped "matching how the repeat engine clamps" — opposite behaviors. The
  skip is correct (the engine reads the recurring day off the start date, so
  clamping "@every 31st" in February would make it recur on the 28th forever);
  the comment now says why.

* fix(short-syntax): keep highlights stable across keystrokes and single-source the removals

The highlight ranges are pinned to the text they were parsed from, but the
parse is async, so every keystroke painted once with the previous text's
ranges and dropped all of them — the overlay blanked for a frame per
keystroke. Keep the ranges that end inside the unchanged common prefix
instead: a highlight can then never be mispositioned, only at most one
keystroke stale.

Replace the runtime typography sync with the full property set declared
statically on %main-input-typography. A textarea does not inherit typography
(the UA `font:` shorthand resets weight/style/variant/stretch/line-height)
while the mirror div does, but that is a static problem: declaring the whole
set on both layers removes the drift with no JS, no theme-change trigger and
no 300ms guess, and also covers the box properties (padding/border/
box-sizing) the sync never touched.

Also:
- splitTextByRanges skips a range that overlaps the previous one rather than
  re-emitting its text, which would lengthen the mirror and shift every later
  highlight.
- Markdown-link removal was expressed twice (a .replace() deriving the
  plain-URL search text, plus tracked.remove() calls doing the real edit).
  Collapse both into collapseMarkdownLinks(), with the plain-URL scan reading
  the tracker's text afterwards, and emit ranges for the stripped '[' and
  '](url)' so markdown links highlight like any other consumed token.

* fix(short-syntax): keep URL-less markdown links and pin the last shared text metrics

Two follow-ups from the review:

Collapse only markdown links that carry a URL. The collapse ran for every
`[text](url)` match, but whether it ran at all was decided by the title holding
*some* URL — so `Add [Website]() https://a.com` lost the brackets while
`Add [Website]()` alone kept them. A URL-less link yields no attachment, so
extracting it only deletes the user's characters; skipping it makes that rule
unconditional and stops unrelated text elsewhere in the title from deciding it.

Add `direction`, `text-align` and `word-break` to the shared typography, and
the wrapping/bidi properties to the overlay spans. All were copied by the
runtime sync that the static block replaced; without them a theme rule aimed at
the textarea alone (or at the spans via `*`) moves the glyphs out from under the
highlight backgrounds. `inherit` rather than fixed values, so the app's own RTL
from `[dir]` on a shared ancestor still reaches both layers.
2026-07-22 13:24:15 +02:00

9.1 KiB
Executable file
Raw Blame History

Short Syntax

Super Productivity supports five main short syntax forms for quickly adding metadata to tasks: tags (#), projects (+), time estimates (m/h), due dates (@), and deadlines (!). The due date form also accepts recurrence phrases (@every friday, @daily) in the Add Task Bar. The project, tag, and due/deadline/time forms can be enabled or disabled in 3.02-Settings-and-Preferences#global-settings.Short-Syntax.

Supported Syntax Forms

Tag Syntax (#)

  • Symbol: #
  • Purpose: Add tags to the task.
  • Grammar: #<tag-name>. A space is required before #.
  • Parameters: Tag name (case-insensitive).
  • Valid: "Task #urgent", "Meeting #work #important". Multiple tags allowed.
  • Invalid: "Task#urgent" (no space before #) — the whole short-syntax run returns no changes. If other tokens are valid (e.g. "Task#urgent #other"), only the valid tag is applied and the invalid token is left in the title.

Project Syntax (+)

  • Symbol: +
  • Purpose: Assign the task to a project.
  • Grammar: +<project-name>. A space is required before +.
  • Parameters: Project title or partial title; minimum 3 characters. Matching is prefix-based, case-insensitive, with spaces removed for comparison. The shortest matching project title is preferred when several match.
  • Valid: "Task +Important Project", "Task +Project" (partial match). Project names containing +, #, @, or similar are supported; the parser uses lookahead so these characters inside the name do not end the token.
  • Invalid: "Task+Project" (no space before +) — no project is applied.

Time Estimate Syntax

  • Symbols: m (minutes), h (hours). Optional prefix t. Optional / to separate time spent from time estimate.
  • Purpose: Set time spent and/or time estimate.
  • Grammar: [t]<number><unit>[ <number><unit>...][/ <number><unit>...]. Units: m, h. Decimal numbers allowed (e.g. 1.5h). Before /: time spent on the current day. After /: time estimate. Clusters like 1h 30m are supported.
  • Parameters: Time spent (before /): e.g. 30m, 1.5h. Time estimate (after /): e.g. 1h, 90m. One side can be omitted: 30m/ sets only time spent; 1h sets only estimate.
  • Valid: "Task 10m", "Task 1h/2h", "Task 30m/", "Task t1.5h", "Task 1h 30m / 2h".
  • Defaults: h = 60 minutes.

Due Date Syntax (@)

  • Symbol: @
  • Purpose: Set due date/time for the task.
  • Grammar: @<date-time-expression>. No space is allowed between @ and the expression.
  • Parameters: Date/time expressions supported by the Chrono-based parser (e.g. 4pm, friday, tomorrow 19:00, 2030-10-12T13:37).
  • Valid: "Task @4pm", "Task @friday", "Task @tomorrow 19:00", "Task @2030-10-12T13:37".
  • Invalid: "Task @ tomorrow" (space after @) — due date is not applied and the expression is stripped from the title. Two-digit years 5099 are corrected to the current or next calendar year to avoid Chrono interpreting them as years (e.g. 90m in a date context).

Recurrence Syntax (@every ..., @daily, ...)

  • Symbol: @, shared with the due date form. A recurrence phrase must come directly after the @.
  • Purpose: Create the task as a repeating task, equivalent to picking a repeat preset in the Add Task Bar's repeat menu.
  • Grammar: @every <unit> or a bare frequency word: @daily, @weekly, @monthly, @yearly, @annually. Units for every: day(s), week(s), weekday(s)/workday(s), month(s), year(s), a weekday name (friday, fri, fridays, ...), or a day of the month (15th). An optional time can follow: @every friday 3pm. Intervals (@every 2 weeks) are not supported and are parsed as a plain due date instead.
  • Mapping: @every friday → weekly on Friday (due date set to the next Friday); @every weekday → Monday through Friday; @every 15th → monthly on the 15th (due date set to the next 15th); @daily 6am → daily with a start time. The unanchored words recur on the day you add the task: @weekly on today's weekday, @monthly on today's day of the month, @yearly on today's date. If a time you add has already passed today, the first occurrence moves a whole period ahead so the recurring day stays the same — @weekly 6am added on a Wednesday at 10:00 is due next Wednesday, not tomorrow.
  • Valid: "Water plants @every friday", "Journal @daily 6am", "Pay rent @every 15th".
  • Scope: Only in the Add Task Bar (task creation). In title edits of existing tasks the phrase is parsed as a plain due date, since an existing task cannot be converted to a repeating task this way. The parsed repeat shows on the repeat button and can be cleared there, which also removes the phrase from the input.

Deadline Syntax (!)

  • Symbol: !
  • Purpose: Set a deadline date/time for the task.
  • Grammar: !<date-time-expression>. A space is required before ! when it is not at the start of the title.
  • Parameters: Date/time expressions supported by the Chrono-based parser (e.g. 14:30, friday, next month).
  • Valid: "Task !14:30", "Task !friday", "Task @monday !friday".
  • Invalid: "Done!" (no space before !) — the exclamation mark is treated as punctuation and no deadline is applied.

Formal Grammar and Regex Patterns

The parser uses regular expressions for each syntax form. Time: optional t prefix, digits and decimal with m/h, optional / for spent/estimate. Project: + with lookahead so delimiters (#, @, time-like tokens) inside the project name do not terminate the match. Tag: # followed by characters until a special character or whitespace. Due: @ followed by characters until a special character. Deadline: ! followed by characters until a special character. Exact patterns are defined in short-syntax.ts (e.g. SHORT_SYNTAX_TIME_REG_EX, SHORT_SYNTAX_PROJECT_REG_EX, SHORT_SYNTAX_TAGS_REG_EX, SHORT_SYNTAX_DUE_REG_EX, SHORT_SYNTAX_DEADLINE_REG_EX).

Default Values and Behavior

  • Time units: m = minutes, h = 60 minutes.
  • Time spent only: e.g. 30m/ sets time spent on the current day and no estimate.
  • Time estimate only: e.g. 1h sets estimate without time spent.
  • Date parsing: Uses a custom Chrono-based parser with year correction for two-digit years 5099 (current/next year).
  • Project matching: Prefix-based, case-insensitive, spaces removed; shortest matching project title wins.
  • Tag mode: combine (default) or replace; configurable in settings.

Where Short Syntax Is Supported

Supported:

  • Add Task Bar (primary place for creating tasks with short syntax).
  • Task creation when adding new tasks (e.g. via the shared add-task action).
  • Task title updates when the only change is the title (syntax is re-parsed).

Unsupported or disabled:

Parsing and Evaluation Rules

  1. Order of processing: Due/deadline/time → Project → Tag.
  2. Title cleanup: Parsed syntax is removed from the task title after processing.
  3. Tag creation: Tags that do not exist yet are collected in a newTagTitles array for creation.
  4. Return value: The shortSyntax() function returns an object with task changes (and newTagTitles, remindAt, projectId, repeatQuickSetting) or undefined if nothing was parsed or syntax is disabled.

Error Handling

Malformed syntax:

  • Invalid syntax is ignored; the function may return undefined or only apply valid parts.
  • No exception is thrown; invalid tokens are skipped or left in the title as appropriate.
  • If a syntax type is disabled in settings, that form is not processed.

Edge cases:

  • Project names containing +, #, @, or time-like patterns are handled so that the project token is parsed correctly.
  • Two-digit years 5099 in date expressions are corrected to the current or next year.
  • Time estimate tokens (e.g. 90m) in the same title as date expressions are parsed separately so Chrono date parsing does not consume the time estimate.

Add Task Bar Behavior

  • Toggle new task position: A toggle in the Add Task Bar controls whether the new task is added at the top or the bottom of the list.
  • Autocomplete: The Add Task Bar can suggest tags, projects, and date expressions while typing.
  • Inline highlighting: Detected short syntax (tags, project, estimate, due/deadline dates, recurrence, extracted URLs) is highlighted directly in the input while typing, showing which characters will be stripped from the title before the task is created.

Notes

  • Short syntax is processed by the shortSyntax() function; it returns task changes or undefined when no changes are needed.
  • The five syntax forms are controlled by the global Short Syntax settings for project, tag, and due/deadline/time in 3.02-Settings-and-Preferences.