diff --git a/packages/core/src/extensions/list.test.ts b/packages/core/src/extensions/list.test.ts
index 0076abf2..c6f405d6 100644
--- a/packages/core/src/extensions/list.test.ts
+++ b/packages/core/src/extensions/list.test.ts
@@ -120,6 +120,55 @@ describe('commands', () => {
fixture.editor.commands.cycleCheckableList()
expect(docToMarkdown(fixture.doc)).toBe('+ [ ] outer\n - [ ] inner\n')
})
+
+ it('cycleOrderedList cycles plain content through bullet, ordered, and text', () => {
+ using fixture = setupFixture()
+ const { n } = fixture
+ fixture.set(n.doc(n.paragraph('todo')))
+
+ expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"todo"`)
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"- todo"`)
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"1. todo"`)
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"todo"`)
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc).trim()).toMatchInlineSnapshot(`"- todo"`)
+ })
+
+ it('cycleOrderedList converts a task to a plain bullet first', () => {
+ using fixture = setupFixture()
+ const { n } = fixture
+ fixture.set(n.doc(n.list({ kind: 'task', marker: '+', checked: true }, n.paragraph('done'))))
+
+ fixture.editor.commands.cycleOrderedList()
+
+ expect(fixture.doc.child(0).attrs).toMatchObject({
+ kind: 'bullet',
+ checked: false,
+ })
+ expect(docToMarkdown(fixture.doc)).toBe('- done\n')
+ })
+
+ it('cycleOrderedList changes only the closest nested list', () => {
+ using fixture = setupFixture()
+ const { n } = fixture
+ fixture.set(
+ n.doc(
+ n.list(
+ { kind: 'bullet' },
+ n.paragraph('outer'),
+ n.list({ kind: 'bullet' }, n.paragraph('inner')),
+ ),
+ ),
+ )
+
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc)).toBe('- outer\n 1. inner\n')
+ fixture.editor.commands.cycleOrderedList()
+ expect(docToMarkdown(fixture.doc)).toBe('- outer\n\n inner\n')
+ })
})
describe('keymap', () => {
diff --git a/packages/core/src/extensions/list.ts b/packages/core/src/extensions/list.ts
index 14ae3f2d..75698079 100644
--- a/packages/core/src/extensions/list.ts
+++ b/packages/core/src/extensions/list.ts
@@ -20,8 +20,8 @@ import {
type ListAttrs,
} from '@prosekit/extensions/list'
import type { ProseMirrorNode } from '@prosekit/pm/model'
-import { Plugin } from '@prosekit/pm/state'
import type { Command, EditorState } from '@prosekit/pm/state'
+import { Plugin } from '@prosekit/pm/state'
import {
createListRenderingPlugin,
createSafariInputMethodWorkaroundPlugin,
@@ -300,8 +300,11 @@ function getListAttrsAtSelection(state: EditorState): MeowdownListAttrs | null {
}
/**
- * Cycle the selected block between square and circle checkbox tasks. Non-task
- * content becomes an unchecked square task; shape changes preserve checked state.
+ * Cycle the selected block between square and circle checkbox tasks.
+ *
+ * - A square task becomes a circle task, keeping its checked state;
+ * - A circle task becomes a square task, keeping its checked state;
+ * - Other content becomes an unchecked square task.
*/
function cycleCheckableList(): Command {
return (state, dispatch, view) => {
@@ -316,11 +319,35 @@ function cycleCheckableList(): Command {
}
}
-function defineTaskCommands() {
+/**
+ * Cycle the selected block between a bullet list, an ordered list, and no list.
+ *
+ * - A bullet list becomes an ordered list;
+ * - An ordered list unwraps;
+ * - Other content, including a task, becomes a bullet list.
+ */
+function cycleOrderedList(): Command {
+ return (state, dispatch, view) => {
+ const attrs = getListAttrsAtSelection(state)
+ const next: MeowdownListAttrs =
+ attrs?.kind === 'bullet' || attrs?.kind === 'ordered'
+ ? { kind: 'ordered', marker: null, checked: false, collapsed: false }
+ : { kind: 'bullet', marker: null, checked: false, collapsed: false }
+ return toggleList(next)(state, dispatch, view)
+ }
+}
+
+function toggleListCollapsed(): Command {
+ return createToggleCollapsedCommand({ isToggleable: isCollapsibleBullet })
+}
+
+function defineMeowdownListCommands() {
return defineCommands({
cycleCheckableList,
+ cycleOrderedList,
wrapInCircleTask,
wrapInSquareTask,
+ toggleListCollapsed,
})
}
@@ -413,12 +440,6 @@ function defineMeowdownListPlugins(): PlainExtension {
])
}
-function defineCollapseCommands() {
- return defineCommands({
- toggleListCollapsed: () => createToggleCollapsedCommand({ isToggleable: isCollapsibleBullet }),
- })
-}
-
function defineMeowdownListKeymap(): PlainExtension {
return defineKeymap({
'Mod-Enter': rotateSquareTask(),
@@ -450,7 +471,6 @@ export function defineMeowdownList() {
defineListMarkerAttr(),
defineListTaskMarkerAttr(),
defineListMarkerGapAttr(),
- defineTaskCommands(),
- defineCollapseCommands(),
+ defineMeowdownListCommands(),
)
}