Skip to content

feat: prefill function/arrow/def snippets on typing (issue #618) - #3095

Merged
abose merged 4 commits into
mainfrom
feat/618-prefill-function-snippets
Aug 15, 2026
Merged

feat: prefill function/arrow/def snippets on typing (issue #618)#3095
abose merged 4 commits into
mainfrom
feat/618-prefill-function-snippets

Conversation

@abose

@abose abose commented Aug 15, 2026

Copy link
Copy Markdown
Member

Adds built-in Custom Snippets that pop up as you type "function"/"arrow" (JS/TS/JSX/TSX), "function" (PHP), or "def" (Python), inserting a bare function/arrow-function/def skeleton with tab-stops for the name and params, without requiring the full word to be typed first (fires on a 2+ char prefix for these built-ins only).

Screencast.From.2026-08-15.22-22-13.mp4

Architecture:

  • Built-in snippets (src/extensionsIntegrated/CustomSnippets/defaultSnippets.js) are merged into the matching engine's optimized structures at runtime only - never written to the user's customSnippets.json and never part of Global.SnippetHintsList, so they're invisible to the Custom Snippets panel and can't be edited/deleted there. Nothing to keep in sync: they're re-derived from this file fresh on every boot.
  • Fixed a pre-existing bug in helper.js: snippetsByAbbreviation mapped one abbreviation to a single snippet, so two snippets sharing an abbreviation across languages (e.g. "function" for JS and PHP) would silently shadow each other. Now maps to a candidate array, checked per-language.
  • Hint insertion now resolves the exact snippet via a stable insertionKey carried on the hint element (helper.js getSnippetByInsertionKey), rather than re-deriving language context from the cursor at accept time - avoids redundant work and any risk of resolving a different snippet than what was actually shown.
  • Migrated Custom Snippets' tab-stop engine (snippetCursorManager.js) onto editor/TabstopManager.js (already used by DocCommentHints/LSP completions) to get real select-on-tab ${N:default} placeholders, which the old bespoke engine couldn't support. Added escapeBareDollarSigns so every pre-existing saved snippet keeps behaving byte-for-byte identically under the new grammar.
  • Fixed two bugs surfaced in TabstopManager.js by that migration: goToNextStop/goToPreviousStop crashed when called with no active session, and ending a session no longer happened when the cursor moved outside the snippet's lines (or a multi-cursor selection was made) - both fixed at the shared-engine level, so LSP completions and DocCommentHints get the fix too.

Also updates the i18n rule in CLAUDE.md: only genuinely translatable strings belong in strings.js. Content that must render identically in every locale (code syntax, keyword examples) should be a local constant instead, since the automated AI translation pass has no way to tell code tokens apart from prose.

Adds built-in Custom Snippets that pop up as you type "function"/"arrow"
(JS/TS/JSX/TSX), "function" (PHP), or "def" (Python), inserting a bare
function/arrow-function/def skeleton with tab-stops for the name and
params, without requiring the full word to be typed first (fires on a
2+ char prefix for these built-ins only).

Architecture:
- Built-in snippets (src/extensionsIntegrated/CustomSnippets/defaultSnippets.js)
  are merged into the matching engine's optimized structures at runtime
  only - never written to the user's customSnippets.json and never part
  of Global.SnippetHintsList, so they're invisible to the Custom
  Snippets panel and can't be edited/deleted there. Nothing to keep in
  sync: they're re-derived from this file fresh on every boot.
- Fixed a pre-existing bug in helper.js: snippetsByAbbreviation mapped
  one abbreviation to a single snippet, so two snippets sharing an
  abbreviation across languages (e.g. "function" for JS and PHP) would
  silently shadow each other. Now maps to a candidate array, checked
  per-language.
- Hint insertion now resolves the exact snippet via a stable
  insertionKey carried on the hint element (helper.js
  getSnippetByInsertionKey), rather than re-deriving language context
  from the cursor at accept time - avoids redundant work and any risk
  of resolving a different snippet than what was actually shown.
- Migrated Custom Snippets' tab-stop engine
  (snippetCursorManager.js) onto editor/TabstopManager.js (already used
  by DocCommentHints/LSP completions) to get real
  select-on-tab `${N:default}` placeholders, which the old bespoke
  engine couldn't support. Added escapeBareDollarSigns so every
  pre-existing saved snippet keeps behaving byte-for-byte identically
  under the new grammar.
- Fixed two bugs surfaced in TabstopManager.js by that migration:
  goToNextStop/goToPreviousStop crashed when called with no active
  session, and ending a session no longer happened when the cursor
  moved outside the snippet's lines (or a multi-cursor selection was
  made) - both fixed at the shared-engine level, so LSP completions and
  DocCommentHints get the fix too.

Also updates the i18n rule in CLAUDE.md: only genuinely translatable
strings belong in strings.js. Content that must render identically in
every locale (code syntax, keyword examples) should be a local
constant instead, since the automated AI translation pass has no way
to tell code tokens apart from prose.
@abose
abose force-pushed the feat/618-prefill-function-snippets branch from 01e3ba7 to 45e3ff1 Compare August 15, 2026 16:54
abose added 3 commits August 15, 2026 22:43
The function/arrow/def default snippet bodies hardcoded 4 literal
spaces, which looked wrong the moment a file used a different width
or tabs.

Adds an INDENT_TOKEN ("@@indent@@") that templateText can use for "one
indent level, whatever this file is actually configured/detected to
use". It's resolved by snippetCursorManager.js's own preprocessing
(getOneIndentUnit/resolveIndentToken), using the same
Editor.getUseTabChar/getSpaceUnits APIs (auto-detection + project/
language preference cascade) Phoenix's own Tab-key handling relies on,
scoped per-file via the editor being inserted into.

Deliberately not new tab-stop syntax taught to editor/TabstopManager
(the shared engine LSP completions and DocCommentHints also use) -
the token is fully substituted with literal characters before the
text ever reaches that parser, so real LSP-served snippet text (which
never passes through this module) can't be affected, and there's
nothing left for TabstopManager to interpret even in principle.

Also exposed to user-authored custom snippets via the panel's
template-field tooltip, since the mechanism isn't built-in-specific.

Fixes a real bug found while writing this: `require("editor/Editor")`
needs `.Editor` to get the actual class with its static methods -
used the bare module by mistake, which would have thrown at runtime.
TabstopManager now draws a subdued box around every remaining tab-stop
in an active session, with a bolder box layered on top of whichever
stop is currently selected - matching the visual language
RenameIdentifier.js already uses for its rename boxes. Benefits every
consumer of the shared tab-stop engine (LSP completions, DocComment
hints, Custom Snippets), not just one feature.

Uses a new @bc-editor-decoration-neutral variable (no light/dark
split) for the subdued box, since it decorates the code editor canvas
whose background depends on the user's code theme, not the app's
light/dark UI mode - same reasoning as .cm-matchhighlight. The active
box reuses the existing @bc-primary-btn-border/@dark-bc-primary-btn-border
pair split by .dark& for consistency with rename's own accent color.

Adds unit coverage for marker class assignment, active-marker swap on
navigation, zero-width stops correctly getting no box, and both
markers clearing on session end.
…k line

.custom-snippet-code-hint (the "Snippet" badge on default-snippet hint
rows) was only position: absolute while its row was highlighted. When
unhighlighted it fell back to static positioning with just
visibility: hidden - which still reserves layout space - so the
invisible badge text wrapped onto its own line inside the row,
leaving a blank line and shifting every hint below it down as soon as
selection moved off that row (e.g. pressing the down arrow).

Make the badge position: absolute unconditionally so it's out of flow
in both states; only visibility toggles with .highlight now.
@sonarqubecloud

Copy link
Copy Markdown

@abose
abose merged commit 18c8303 into main Aug 15, 2026
13 of 21 checks passed
@abose
abose deleted the feat/618-prefill-function-snippets branch August 15, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant