Fix list indentation with hanging indent for wrapped lines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -194,7 +194,7 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
},
|
||||
})
|
||||
|
||||
// Second pass: line-based decorations for lists and checkboxes
|
||||
// Second pass: line-based decorations for lists (bullets, checkboxes, ordered)
|
||||
for (let i = 1; i <= doc.lines; i++) {
|
||||
const line = doc.line(i)
|
||||
const text = line.text
|
||||
@@ -202,39 +202,56 @@ function buildDecorations(view: EditorView): DecorationSet {
|
||||
const indent = text.length - stripped.length
|
||||
const lineOnCursor = isOnCursorLine(view, line.from, line.to)
|
||||
|
||||
// Skip horizontal rule lines (avoid false match with bullet regex)
|
||||
if (/^([-*_]\s*){3,}$/.test(stripped)) continue
|
||||
|
||||
const level = Math.min(Math.floor(indent / 2), 5)
|
||||
|
||||
// Checkbox: "- [ ] " or "- [x] " (with optional leading whitespace)
|
||||
const checkboxMatch = text.match(/^(\s*)- \[([ x])\] /)
|
||||
if (checkboxMatch && !lineOnCursor) {
|
||||
const checked = checkboxMatch[2] === 'x'
|
||||
const markerStart = line.from + checkboxMatch[1].length
|
||||
const markerEnd = markerStart + 6 // "- [x] " is 6 chars
|
||||
const markerPos = line.from + checkboxMatch[1].length
|
||||
const fullMarkerEnd = line.from + checkboxMatch[0].length
|
||||
// Replace entire prefix (whitespace + "- [x] ") with checkbox widget
|
||||
decs.push(
|
||||
Decoration.replace({
|
||||
widget: new CheckboxWidget(checked, markerStart),
|
||||
}).range(markerStart, markerEnd)
|
||||
widget: new CheckboxWidget(checked, markerPos),
|
||||
}).range(line.from, fullMarkerEnd)
|
||||
)
|
||||
// Add nested guide line decoration
|
||||
if (indent > 0) {
|
||||
decs.push(Decoration.line({ class: 'cm-live-list-nested' }).range(line.from))
|
||||
}
|
||||
decs.push(Decoration.line({
|
||||
class: `cm-live-list-item cm-live-list-bullet cm-live-list-level-${level}`,
|
||||
}).range(line.from))
|
||||
continue
|
||||
}
|
||||
|
||||
// Unordered list bullet: "- " (with optional leading whitespace)
|
||||
const bulletMatch = text.match(/^(\s*)- /)
|
||||
if (bulletMatch && !lineOnCursor) {
|
||||
const markerStart = line.from + bulletMatch[1].length
|
||||
const markerEnd = markerStart + 2 // "- " is 2 chars
|
||||
const fullMarkerEnd = line.from + bulletMatch[0].length
|
||||
// Replace entire prefix (whitespace + "- ") with bullet widget
|
||||
decs.push(
|
||||
Decoration.replace({
|
||||
widget: new BulletWidget(),
|
||||
}).range(markerStart, markerEnd)
|
||||
}).range(line.from, fullMarkerEnd)
|
||||
)
|
||||
decs.push(Decoration.line({
|
||||
class: `cm-live-list-item cm-live-list-bullet cm-live-list-level-${level}`,
|
||||
}).range(line.from))
|
||||
continue
|
||||
}
|
||||
|
||||
// Add nested guide line for any indented list item (bullet or checkbox)
|
||||
if (indent > 0 && /^\s+(- |\* |\d+\. )/.test(text)) {
|
||||
decs.push(Decoration.line({ class: 'cm-live-list-nested' }).range(line.from))
|
||||
// Ordered list: "1. " (with optional leading whitespace)
|
||||
const orderedMatch = text.match(/^(\s*)(\d+)\. /)
|
||||
if (orderedMatch && !lineOnCursor) {
|
||||
// Hide leading whitespace — CSS controls indentation
|
||||
if (indent > 0) {
|
||||
decs.push(Decoration.replace({}).range(line.from, line.from + indent))
|
||||
}
|
||||
decs.push(Decoration.line({
|
||||
class: `cm-live-list-item cm-live-list-ordered cm-live-list-level-${level}`,
|
||||
}).range(line.from))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,32 +325,32 @@ const livePreviewTheme = EditorView.theme({
|
||||
margin: '8px 0',
|
||||
},
|
||||
|
||||
// Bullet widget
|
||||
// Bullet widget — fixed width for consistent hanging indent
|
||||
'.cm-live-bullet': {
|
||||
display: 'inline-block',
|
||||
width: '20px',
|
||||
color: '#888',
|
||||
fontSize: '0.85em',
|
||||
marginRight: '4px',
|
||||
},
|
||||
|
||||
// Checkbox widget
|
||||
// Checkbox widget — fixed width matching bullet for consistent indent
|
||||
'.cm-live-checkbox-wrap': {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '4px',
|
||||
width: '20px',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
'.cm-live-checkbox': {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
border: '1.5px solid #555',
|
||||
borderRadius: '3px',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '11px',
|
||||
fontSize: '10px',
|
||||
lineHeight: '1',
|
||||
color: 'transparent',
|
||||
userSelect: 'none',
|
||||
@@ -347,11 +364,15 @@ const livePreviewTheme = EditorView.theme({
|
||||
color: '#fff',
|
||||
},
|
||||
|
||||
// Nested list guide line
|
||||
'.cm-live-list-nested': {
|
||||
borderLeft: '1.5px solid #2a2a4a',
|
||||
marginLeft: '8px',
|
||||
},
|
||||
// List indentation — hanging indent so wrapped lines align with text, not marker
|
||||
'.cm-live-list-bullet': { textIndent: '-20px' },
|
||||
'.cm-live-list-ordered': { textIndent: '-20px' },
|
||||
'.cm-live-list-level-0': { paddingLeft: '40px' },
|
||||
'.cm-live-list-level-1': { paddingLeft: '60px' },
|
||||
'.cm-live-list-level-2': { paddingLeft: '80px' },
|
||||
'.cm-live-list-level-3': { paddingLeft: '100px' },
|
||||
'.cm-live-list-level-4': { paddingLeft: '120px' },
|
||||
'.cm-live-list-level-5': { paddingLeft: '140px' },
|
||||
})
|
||||
|
||||
export function livePreview(): Extension {
|
||||
|
||||
@@ -142,14 +142,17 @@ belongs_to:
|
||||
- [ ] Review analytics from last issue
|
||||
|
||||
### Nested Topics
|
||||
- Content strategy
|
||||
- Newsletter growth
|
||||
- Organic subscribers
|
||||
- Paid acquisition
|
||||
- Content strategy for growing the newsletter audience through organic channels, referrals, and high-quality evergreen content that people want to share with their engineering teams
|
||||
- Newsletter growth and subscriber acquisition including all the different channels we use to attract new readers to the publication
|
||||
- Organic subscribers from search, Twitter, and word of mouth — these are the highest quality subscribers with the best retention rates over time
|
||||
- Paid acquisition through Facebook ads and newsletter cross-promotions with other engineering publications in the space
|
||||
- Social media cross-posting
|
||||
- Technical writing
|
||||
- Code examples
|
||||
- Architecture diagrams
|
||||
1. First ordered item with a really long description that should definitely wrap to the next line when displayed in the editor, testing the hanging indent behavior for numbered lists
|
||||
2. Second ordered item — shorter
|
||||
1. Nested ordered item that also has quite a long description to verify that the indentation works correctly for nested numbered lists too
|
||||
`,
|
||||
'/Users/luca/Laputa/procedure/run-sponsorships.md': `---
|
||||
title: Run Sponsorships
|
||||
|
||||
Reference in New Issue
Block a user