Skip to content

feat: allow block type transformation via palette on non-empty blocks #186

@oobagi

Description

@oobagi

Problem

There's no way to change a block's type once it has content. The / palette only opens on empty blocks (ta.Value() == ""), so users who want to turn a paragraph into a heading — or a bullet into a quote — must clear the block, change its type, then re-type the content.

This is a common editing operation (realize a paragraph should be a heading, promote/demote headings, convert to a list after drafting). The infrastructure already supports it — applyPaletteSelection preserves content for all non-Divider types — but the opening gate blocks it.

Context

  • Palette open condition (internal/editor/editor.go:1184-1188): requires ta.Value() == "", ta.Line() == 0, ta.LineInfo().ColumnOffset == 0
  • applyPaletteSelection (internal/editor/editor.go:729-743): already changes type and preserves content; only clears content for Divider
  • Backspace at pos 0 already converts lists → paragraph, showing the pattern exists for non-empty type changes
  • Palette filtering, navigation, and selection all work independently of content state

Possible approaches

Approach A: Extend / trigger to position-0 on non-empty blocks

  • How: Remove the ta.Value() == "" gate. Keep the position-0 requirement so / mid-sentence still types a slash.
  • Pros: Single mental model ("go to start of block, type /, pick a type"). Mirrors Notion. Minimal code change.
  • Cons: Need to ensure / isn't inserted into the block text when triggering the palette.
  • Files: internal/editor/editor.go (open condition), internal/editor/palette.go (maybe hide Divider when content exists)

Approach B: Separate shortcut (e.g., Ctrl+/) for non-empty blocks

  • How: Add a dedicated keybind that opens the palette regardless of content or cursor position.
  • Pros: No ambiguity about when / is a character vs. a command.
  • Cons: Two ways to do the same thing. More to document. Conflicts with macOS terminal remapping concerns.
  • Files: internal/editor/editor.go

Recommended: Approach A — simpler, consistent, less to learn.

Tasks

  • Remove ta.Value() == "" from palette open condition (keep pos-0 check)
  • Ensure the / character is not inserted into block content when palette opens
  • Hide Divider from palette options when the current block has content (avoids silent data loss)
  • Visually indicate the current block type in the palette (dim or checkmark)
  • Verify undo reverts a type transformation as a single operation
  • Add tests: palette opens on non-empty block at pos 0, content preserved after type change, / still types normally mid-text

Test plan

  • Place cursor at position 0 of a block with text, press / — palette opens
  • Select a different type — block type changes, all content preserved
  • Place cursor mid-text, press / — literal slash inserted, no palette
  • Open palette on non-empty block — Divider option is hidden
  • Select the block's current type — no-op, palette closes
  • Undo after type change — type and content revert
  • Multi-line block converted to heading — content preserved

Scope

Type: enhancement
Size: small–medium

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions