Skip to content

feat(component-browser): insert components into the include list - #346

Merged
eFAILution merged 2 commits into
eFAILution:betafrom
X-Guardian:feature/insert-component-into-include-list
Oct 13, 2026
Merged

eFAILution merged 2 commits into
eFAILution:betafrom
X-Guardian:feature/insert-component-into-include-list

Conversation

@X-Guardian

Copy link
Copy Markdown
Contributor

Summary

  • Inserting a component from the Component Browser (or the detached details panel) dropped the - component: snippet at the cursor, wherever that happened to be — often inside a job's script: or between two unrelated keys, leaving invalid YAML for the user to cut and paste.
  • The component now goes where it belongs: at the end of the top-level include: list, or in a new include: block at the top of the file when there isn't one.

Change Type

  • feat
  • fix
  • refactor
  • docs
  • test
  • chore

Context

User-facing impact

  • Existing list → appended after the last entry, including a multi-line last entry (inputs:, rules:), and indented to match the list (column 0, 2 or 4 spaces). Comments and blank lines trailing the list stay below the new entry.

  • Single include: entry → converted into a list of the existing entry followed by the new one. Entries are kept verbatim:

    Before After
    include: a.yml include:
    - a.yml
    - component: …
    include: { local: a.yml } include:
    - { local: a.yml }
    - component: …
    include: [a.yml, { local: b.yml }] include:
    - a.yml
    - { local: b.yml }
    - component: …
    include: [a.yml,
    b.yml]
    include:
    - a.yml
    - b.yml
    - component: …
    include: a.yml # shared include: # shared
    - a.yml
    - component: …
    include:
    local: a.yml
    rules: …
    include:
    - local: a.yml
    rules: …
    - component: …
  • No include: → a new include: block above the first line of YAML, so a file's header comments stay on top, followed by a blank line. In a component template, it goes after the spec: header and its --- so the include lands in the pipeline document rather than the header.

  • The editor scrolls to the inserted entry if it's off screen.

When it still inserts at the cursor

Some files can't be rewritten without changing behaviour elsewhere or reflowing the user's text. These keep the old cursor insertion:

Form Why it's left alone
include: *shared (alias) Appending would mean expanding the shared list and breaking its link to the anchor; wrapping it as - *shared nests a list in a list, which GitLab rejects.
include: &shared [a.yml] (anchor) Keeping the anchor on the new list adds the component everywhere *shared is used; dropping it breaks those uses.
include: !reference [.defaults, include] (tag) It points at another key's value, not a list of files; converting it would produce two bogus includes.
include: | (block scalar) A text block, not a list — very rare for include:.
A single entry that itself spans lines ([{ local: a.yml, ↵ rules: [] }], or a wrapped plain scalar) Moving it under a - would need its continuation lines re-indented, i.e. reflowing the user's text. A flow sequence spanning lines is fine as long as each element sits on one line.
YAML that doesn't parse Positions from a broken parse can't be trusted, and the file is likely mid-edit.
A spec: header with no --- There's no pipeline document to put the include in.

GitLab scope

  • gitlab.com
  • self-managed GitLab
  • both (placement is instance-independent)

Affected areas

  • Component Browser (and the detached details panel, which shares insertComponent)
  • Hover provider
  • Completion provider
  • Validation provider
  • Cache and refresh behavior
  • GitLab API calls/auth/token storage
  • Docs only

What changed by bucket

Bucket Files Approach
Placement logic src/providers/componentInsertionPoint.ts (new) planComponentInsertion(text, snippet) returns a replace range plus text, or null for the forms above. No vscode types, following componentBrowserEdit.ts, so the unit suite drives it directly. The yaml parser locates the include: value and tells its kind apart (block or flow, alias, anchor, tag, block scalar), and handles multi-document templates. It's used for source ranges only: the edit is a text splice, so the rest of the file keeps its formatting, quoting and comments. Rendering a modified AST back out would reflow the whole file.
Dependency package.json, package-lock.json, .ai/context.yaml Adds yaml ^2.9.1. js-yaml (used everywhere else) parses to plain values and exposes no source positions. In devDependencies alongside js-yaml and preact, since esbuild bundles it. The production out/extension.js grows from 290,484 to 405,121 bytes (+114,637, about 39%).
Wiring src/providers/componentBrowserProvider.ts insertComponent replaces the planned range instead of inserting at selection.active, falling back to the cursor on null. A replace rather than an insert because converting a single entry rewrites the include: line. The edit is now awaited before the success message, and the inserted lines are revealed. The edit-existing-component flow is unchanged: it still replaces the component in place.

Validation

Local checks

  • npm run compile
  • npm test (558 passing, 33 new), after a clean npm ci
  • npm run lint
  • npm run test:extension-host
  • Manual verification in VS Code Extension Host

Tests

tests/unit/componentInsertionPoint.test.ts applies each plan to the source text and asserts on the resulting document:

  • Existing list — append before the next top-level key; after a multi-line entry; re-indent for column-0 and 4-space lists; trailing comments kept below; no trailing newline; an empty include: keeps its comment; CRLF column positions; a job-level include: is ignored.
  • Single entry — plain and quoted scalars (a # inside quotes isn't a comment), a flow mapping with nested brackets, a flow sequence with a quoted comma, an empty [], a trailing comment, a flow sequence spanning lines, a block mapping with nested lines; null for alias, anchor, tag, block scalar, a multi-line flow element and a wrapped plain scalar.
  • Unparseable or unplaceable — null for YAML that doesn't parse and for a spec: header with no ---; a !reference tag elsewhere in the file doesn't block placement.
  • No include: — top of file; after leading comments; comment-only files with and without a trailing newline; empty file; after a template's spec: header; a template's empty pipeline document; appending to a list in a template's pipeline document.

Breaking Changes

  • No breaking changes
  • Breaking changes (describe below)

Users who relied on cursor placement now get the component in the include: list instead. The cursor is still used for the forms listed above.

Screenshots / Recordings (if UI behavior changed)

  • Before: Insert drops - component: … at the cursor, e.g. inside a job.
  • After: Insert appends it to the include: list, or creates include: at the top of the file.

Risk and Rollback

  • Main risks: (1) a splice misplaced against an unusual layout. Placement comes from the parser's ranges, and anything it can't rewrite verbatim returns null, so the worst case is the old cursor behaviour rather than a corrupted file; every change is a single undoable edit. (2) Bundle size, +114,637 bytes for the new parser. (3) Two YAML parsers in the codebase; yaml is only used here.
  • Rollback strategy: revert the commit.

Release Notes Draft

  • Inserting a component from the Component Browser now adds it to the end of your include: list (creating one at the top of the file if needed) instead of at the cursor.

Checklist

  • Branch is up to date with target branch
  • Commit messages follow conventional commits
  • Added/updated docs for behavior or settings changes
  • Added/updated tests for new behavior
  • No secrets or tokens in code, logs, screenshots, or test fixtures

@eFAILution eFAILution left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried a bunch of layouts against this and it held up well. One small bug inline. The bundle growth is a bit much for one feature, but I don't see a clean way to get ranges out of js-yaml, so I'm OK with it.

}

const keyLine = source.lineOf(keyRange[0]);
if (isScalar(value) && value.value === null && value.source === '') {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include: ~ and include: null come out as a list with - ~ as the first entry, which GitLab will reject. The empty check only matches value.source === ''. Could we treat any null scalar the same way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@X-Guardian
X-Guardian force-pushed the feature/insert-component-into-include-list branch from 76e5607 to a058e5a Compare October 13, 2026 08:00

@eFAILution eFAILution left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to go.

@eFAILution
eFAILution merged commit e8c8d7c into eFAILution:beta Oct 13, 2026
23 checks passed
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.

2 participants