Skip to content

feat: add hanging indent support for wrapped list items#481

Open
jy-tan wants to merge 6 commits intocharmbracelet:v2-expfrom
jy-tan:list-indentation
Open

feat: add hanging indent support for wrapped list items#481
jy-tan wants to merge 6 commits intocharmbracelet:v2-expfrom
jy-tan:list-indentation

Conversation

@jy-tan
Copy link

@jy-tan jy-tan commented Oct 11, 2025

Fixes issue #56 where list items that wrap to multiple lines did not have proper hanging indentation, causing continuation lines to align with the bullet/number instead of the text content.

Just taking a stab at this since I'd like to see this fixed, let me know if there's a better approach.

Before:

• Long item text that wraps across
multiple lines with no indent

After:

• Long item text that wraps across
  multiple lines with no indent

The fix works by:

  1. Detecting list item prefixes in the plain text representation
  2. Calculating reduced wrap width accounting for hanging indent space
  3. Splitting prefix from content using ANSI-aware parsing
  4. Wrapping content at the reduced width and prepending hanging indent to continuation lines
  5. Passing pre-formatted text to MarginWriter, which preserves the spacing

@meowgorithm
Copy link
Member

This is really nice; thanks for the contribution. I haven't personally reviewed this one yet but it's something I'd very much like to see merged; noting down internally.

@aymanbagabas
Copy link
Member

@jy-tan Could you please change the base to make it against v2-exp? We're finalizing our v2 releases and this would be a great feature to have

@jy-tan jy-tan changed the base branch from master to v2-exp November 11, 2025 06:11
Copy link
Member

@aymanbagabas aymanbagabas left a comment

Choose a reason for hiding this comment

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

We already provide ANSI aware wrapping logic for 7-bit and 8-bit sequences and UTF-8. We should probably use them instead of implementing a separate wrapping and parsing logic. Could you please explain the use case in details for consumeEscapeSequence and other wrapping functions?

https://github.com/charmbracelet/x/blob/main/ansi/wrap.go
https://github.com/charmbracelet/lipgloss/blob/v2-exp/wrap.go#L12

}

// wrapListContent wraps list content with proper hanging indentation.
func wrapListContent(content string, wrapWidth int) string {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you use ANSI aware wrapping logic from charmbracelet/x/ansi and charmbracelet/lipgloss/v2?

ansi/list.go Outdated

// consumeEscapeSequence consumes an ANSI escape sequence starting at position i.
// Returns the position after the escape sequence.
func consumeEscapeSequence(ansiString string, i int) int {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this?

@jy-tan
Copy link
Author

jy-tan commented Nov 14, 2025

Ah wasn't aware of those existing utils. Modified to use them now, let me know if this works.

@jy-tan jy-tan requested a review from aymanbagabas November 17, 2025 10:45
Copy link
Member

@aymanbagabas aymanbagabas left a comment

Choose a reason for hiding this comment

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

Hey @jy-tan, thank you again for taking the time to work on this. You still haven't answered my questions above 🙂

Honestly, I don't understand why we need to do multiple ansi.Strip and split logic around the list items. I believe you can achieve that easily without the complicated logic via lipgloss.Wrap(...) and ansi.StringWidth(...). Check out this example to do hanging indent via Lip Gloss main.go

image

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.

4 participants