Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions e2e/tests/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,55 @@ test.describe('Editor', () => {
});
});

// ─── Line wrap ───────────────────────────────────────────────────────────────

test.describe('Editor line wrap', () => {
test.beforeEach(async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(user, password);
const viewPage = new ViewPage(page);
await viewPage.expectUserLoggedIn();
});

test.afterEach(async ({ page }) => {
const viewPage = new ViewPage(page);
await viewPage.logout();
});

test('editor-line-wrap-toggle-enables-and-disables-wrapping', async ({ page }) => {
const stamp = Date.now();
const slug = `editor-line-wrap-${stamp}`;

await createPageWithMetadata(page, {
title: `Editor Line Wrap ${stamp}`,
slug,
content: '',
});

// Reset stored editor settings so the test always starts with the default (lineWrap: true)
await page.evaluate(() => localStorage.removeItem('leafwiki-editor-settings'));

const viewPage = new ViewPage(page);
await viewPage.goto(`/${slug}`);
await viewPage.clickEditPageButton();

const cmContent = page.locator('.cm-content');
await cmContent.waitFor({ state: 'visible' });

// Default: line wrap is enabled
await expect(cmContent).toHaveClass(/cm-lineWrapping/);

// Disable line wrap
await page.locator('[data-testid="toggle-line-wrap-button"]').click();
await expect(cmContent).not.toHaveClass(/cm-lineWrapping/);

// Re-enable line wrap
await page.locator('[data-testid="toggle-line-wrap-button"]').click();
await expect(cmContent).toHaveClass(/cm-lineWrapping/);
});
});

// ─── Formatting ───────────────────────────────────────────────────────────────

test.describe('Editor formatting', () => {
Expand Down
36 changes: 33 additions & 3 deletions ui/leafwiki-ui/src/features/editor/MarkdownCodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,31 @@ import { insertHeadingAtStart, insertWrappedText } from './editorCommands'
import type { InternalLinkCompletion } from './internalLinkCompletion'
import { internalLinkCompletionSource } from './internalLinkCompletion'

// Extensions toggled via lineWrapCompartment
const noWrapExtensions = EditorView.theme({
'.cm-content': { whiteSpace: 'pre', width: 'max-content' },
'.cm-line': { whiteSpace: 'pre' },
})

// font-size is 13px; 1.5*13 + 3 + 3 = 25.5px — matches "Enter" spacing
const wrapExtensions = [
EditorView.lineWrapping,
EditorView.theme({
'.cm-content': { lineHeight: 'calc(1.5em + 6px)' },
'.cm-line': {
lineHeight: 'calc(1.5em + 6px)',
paddingTop: '0',
paddingBottom: '0',
},
}),
]

type MarkdownCodeEditorProps = {
initialValue: string
onChange: (value: string) => void
onCursorLineChange?: (line: number) => void
editorViewRef: React.RefObject<EditorView | null>
lineWrap?: boolean
}

// CodeMirror uses 80 for the built-in detail slot, so render the path just before it.
Expand All @@ -52,6 +72,7 @@ export default function MarkdownCodeEditor({
editorViewRef,
onChange,
onCursorLineChange,
lineWrap = true,
}: MarkdownCodeEditorProps) {
const editorRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
Expand All @@ -60,6 +81,7 @@ export default function MarkdownCodeEditor({

const designMode = useDesignModeStore((state) => state.mode)
const [themeCompartment] = useState(() => new Compartment())
const [lineWrapCompartment] = useState(() => new Compartment())

// Always use the latest onChange function
useEffect(() => {
Expand Down Expand Up @@ -155,6 +177,7 @@ export default function MarkdownCodeEditor({
doc: initialValue,
extensions: [
themeCompartment.of(designMode === 'light' ? githubLight : oneDark),
lineWrapCompartment.of(lineWrap ? wrapExtensions : noWrapExtensions),
markdown(),
search({
top: true,
Expand Down Expand Up @@ -214,16 +237,13 @@ export default function MarkdownCodeEditor({
},
'.cm-content': {
lineHeight: '1.5',
whiteSpace: 'pre',
width: 'max-content',
minWidth: '100%',
},
'.cm-line': {
lineHeight: '1.5',
paddingTop: '3px',
paddingBottom: '3px',
paddingLeft: '15px',
whiteSpace: 'pre',
},
'.cm-gutters': {
lineHeight: '1.5',
Expand Down Expand Up @@ -317,5 +337,15 @@ export default function MarkdownCodeEditor({
})
}, [designMode, themeCompartment])

useEffect(() => {
const view = viewRef.current
if (!view) return
view.dispatch({
effects: lineWrapCompartment.reconfigure(
lineWrap ? wrapExtensions : noWrapExtensions,
),
})
}, [lineWrap, lineWrapCompartment])

return <div ref={editorRef} className="markdown-code-editor" />
}
15 changes: 13 additions & 2 deletions ui/leafwiki-ui/src/features/editor/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ const MarkdownEditor = (
(s) => s.maxAssetUploadSizeBytes,
)

const { previewVisible: showPreview, togglePreview } = useEditorStore()
const {
previewVisible: showPreview,
togglePreview,
lineWrap,
} = useEditorStore()

const [activeTab, setActiveTab] = useState<'editor' | 'preview'>('editor')

Expand Down Expand Up @@ -374,11 +378,18 @@ const MarkdownEditor = (
onChange={handleEditorChange}
onCursorLineChange={onCursorLineChange}
editorViewRef={editorViewRef}
lineWrap={lineWrap}
/>
</>
)
},
[handleEditorChange, initialValue, onCursorLineChange, renderToolbar],
[
handleEditorChange,
initialValue,
lineWrap,
onCursorLineChange,
renderToolbar,
],
)

const renderPreview = useCallback((): JSX.Element => {
Expand Down
27 changes: 20 additions & 7 deletions ui/leafwiki-ui/src/features/editor/MarkdownToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import {
Code,
Code2,
Eye,
EyeOff,
Image,
Italic,
Link,
Redo,
Strikethrough,
Table,
Undo,
WrapText,
} from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEditorStore } from '@/stores/editor'
import { MarkdownEditorRef } from './MarkdownEditor'

type Props = {
Expand All @@ -41,6 +42,7 @@ export default function MarkdownToolbar({
onTogglePreview,
}: Props) {
const openDialog = useDialogsStore((state) => state.openDialog)
const { lineWrap, toggleLineWrap } = useEditorStore()
const [canUndo, setCanUndo] = useState(false)
const [canRedo, setCanRedo] = useState(false)
const isRenamingRef = useRef(false)
Expand Down Expand Up @@ -305,6 +307,21 @@ export default function MarkdownToolbar({
<Redo className="markdown-toolbar__icon" />
</Button>
</TooltipWrapper>
<TooltipWrapper
label={lineWrap ? 'Disable line wrap' : 'Enable line wrap'}
side="top"
align="center"
>
<Button
variant="ghost"
size="icon"
onClick={toggleLineWrap}
className={`markdown-toolbar__button${lineWrap ? 'markdown-toolbar__button--active' : ''}`}
Comment thread
perber marked this conversation as resolved.
data-testid="toggle-line-wrap-button"
>
<WrapText className="markdown-toolbar__icon" />
</Button>
</TooltipWrapper>
{!isMobile && (
<>
<div className="markdown-toolbar__separator" />
Expand All @@ -317,13 +334,9 @@ export default function MarkdownToolbar({
variant="ghost"
size="icon"
onClick={onTogglePreview}
className="markdown-toolbar__button markdown-toolbar__button--desktop-only"
className={`markdown-toolbar__button markdown-toolbar__button--desktop-only${previewVisible ? 'markdown-toolbar__button--active' : ''}`}
Comment thread
perber marked this conversation as resolved.
>
{!previewVisible ? (
<Eye className="markdown-toolbar__icon" />
) : (
<EyeOff className="markdown-toolbar__icon" />
)}
<Eye className="markdown-toolbar__icon" />
</Button>
</TooltipWrapper>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function MarkdownCodeBlock(
}

return (
<div className="custom-scrollbar markdown-code-block">
<div className="markdown-code-block">
<div className="markdown-code-block__actions">
<TooltipWrapper label={copied ? 'Copied' : 'Copy code'}>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@
position: relative;
max-width: 100%;
min-width: 0;
overflow-x: auto;
/* Own block-level spacing (replaces the inner <pre> margins which are
now zeroed out below). */
margin-top: 1.5em;
Expand All @@ -102,7 +101,9 @@
the heading's own margin-bottom creates the gap. */
.page-viewer__content :where(h1, h2, h3, h4, h5, h6) + .markdown-code-block,
.markdown-editor__preview :where(h1, h2, h3, h4, h5, h6) + .markdown-code-block,
.page-history__preview-body :where(h1, h2, h3, h4, h5, h6) + .markdown-code-block {
.page-history__preview-body
:where(h1, h2, h3, h4, h5, h6)
+ .markdown-code-block {
margin-top: 0;
}

Expand All @@ -111,14 +112,15 @@
.page-history__preview-body .markdown-code-block pre {
margin-top: 0;
margin-bottom: 0;
overflow-x: auto;
}

.page-viewer__content .markdown-code-block__actions,
.markdown-editor__preview .markdown-code-block__actions,
.page-history__preview-body .markdown-code-block__actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
top: -1rem;
right: 0.75rem;
z-index: 1;
}

Expand Down
Loading
Loading