|
17 | 17 | */ |
18 | 18 |
|
19 | 19 | import { EditorView } from "@codemirror/view"; |
| 20 | +import { KeyBinding } from "@codemirror/view"; |
20 | 21 |
|
21 | 22 | /** |
22 | 23 | * Inserts or removes markdown formatting around selected text (toggles) |
@@ -445,3 +446,145 @@ export const insertMarkdownTaskList = (view: EditorView | null) => { |
445 | 446 |
|
446 | 447 | view.focus(); |
447 | 448 | }; |
| 449 | + |
| 450 | +export const handleEnterForListContinuation = (view: EditorView): boolean => { |
| 451 | + const state = view.state; |
| 452 | + const selection = state.selection.main; |
| 453 | + |
| 454 | + // Only handle if there's no selection (just a cursor) |
| 455 | + if (selection.from !== selection.to) { |
| 456 | + return false; |
| 457 | + } |
| 458 | + |
| 459 | + const cursorPosition = selection.from; |
| 460 | + const line = state.doc.lineAt(cursorPosition); |
| 461 | + const lineText = line.text; |
| 462 | + |
| 463 | + // Check if cursor is at the end of the line or in the middle |
| 464 | + const cursorInLine = cursorPosition - line.from; |
| 465 | + |
| 466 | + // Check for task list (- [ ] or - [x]) |
| 467 | + const taskMatch = lineText.match(/^(\s*)([-*])\s+\[([ x])\]\s+(.*)$/); |
| 468 | + if (taskMatch) { |
| 469 | + const [, indent, marker, , content] = taskMatch; |
| 470 | + |
| 471 | + // If the task item is empty, remove it and exit list mode |
| 472 | + if (!content.trim()) { |
| 473 | + view.dispatch({ |
| 474 | + changes: { |
| 475 | + from: line.from, |
| 476 | + to: line.to, |
| 477 | + insert: '' |
| 478 | + }, |
| 479 | + selection: { |
| 480 | + anchor: line.from |
| 481 | + } |
| 482 | + }); |
| 483 | + return true; |
| 484 | + } |
| 485 | + |
| 486 | + // Continue the task list with unchecked box |
| 487 | + const textBeforeCursor = lineText.substring(0, cursorInLine); |
| 488 | + const textAfterCursor = lineText.substring(cursorInLine); |
| 489 | + const newListItem = indent + marker + ' [ ] '; |
| 490 | + |
| 491 | + view.dispatch({ |
| 492 | + changes: { |
| 493 | + from: line.from, |
| 494 | + to: line.to, |
| 495 | + insert: textBeforeCursor + '\n' + newListItem + textAfterCursor |
| 496 | + }, |
| 497 | + selection: { |
| 498 | + anchor: line.from + textBeforeCursor.length + 1 + newListItem.length |
| 499 | + } |
| 500 | + }); |
| 501 | + return true; |
| 502 | + } |
| 503 | + |
| 504 | + // Check for unordered list (- or *) |
| 505 | + const unorderedMatch = lineText.match(/^(\s*)([-*])\s+(.*)$/); |
| 506 | + if (unorderedMatch) { |
| 507 | + const [, indent, marker, content] = unorderedMatch; |
| 508 | + |
| 509 | + // If the list item is empty (just the marker), remove it and exit list mode |
| 510 | + if (!content.trim()) { |
| 511 | + view.dispatch({ |
| 512 | + changes: { |
| 513 | + from: line.from, |
| 514 | + to: line.to, |
| 515 | + insert: '' |
| 516 | + }, |
| 517 | + selection: { |
| 518 | + anchor: line.from |
| 519 | + } |
| 520 | + }); |
| 521 | + return true; |
| 522 | + } |
| 523 | + |
| 524 | + // Continue the list |
| 525 | + const textBeforeCursor = lineText.substring(0, cursorInLine); |
| 526 | + const textAfterCursor = lineText.substring(cursorInLine); |
| 527 | + const newListItem = indent + marker + ' '; |
| 528 | + |
| 529 | + view.dispatch({ |
| 530 | + changes: { |
| 531 | + from: line.from, |
| 532 | + to: line.to, |
| 533 | + insert: textBeforeCursor + '\n' + newListItem + textAfterCursor |
| 534 | + }, |
| 535 | + selection: { |
| 536 | + anchor: line.from + textBeforeCursor.length + 1 + newListItem.length |
| 537 | + } |
| 538 | + }); |
| 539 | + return true; |
| 540 | + } |
| 541 | + |
| 542 | + // Check for ordered list (1., 2., etc.) |
| 543 | + const orderedMatch = lineText.match(/^(\s*)(\d+)\.\s+(.*)$/); |
| 544 | + if (orderedMatch) { |
| 545 | + const [, indent, number, content] = orderedMatch; |
| 546 | + |
| 547 | + // If the list item is empty (just the number), remove it and exit list mode |
| 548 | + if (!content.trim()) { |
| 549 | + view.dispatch({ |
| 550 | + changes: { |
| 551 | + from: line.from, |
| 552 | + to: line.to, |
| 553 | + insert: '' |
| 554 | + }, |
| 555 | + selection: { |
| 556 | + anchor: line.from |
| 557 | + } |
| 558 | + }); |
| 559 | + return true; |
| 560 | + } |
| 561 | + |
| 562 | + // Continue the list with incremented number |
| 563 | + const textBeforeCursor = lineText.substring(0, cursorInLine); |
| 564 | + const textAfterCursor = lineText.substring(cursorInLine); |
| 565 | + const nextNumber = parseInt(number, 10) + 1; |
| 566 | + const newListItem = indent + nextNumber + '. '; |
| 567 | + |
| 568 | + view.dispatch({ |
| 569 | + changes: { |
| 570 | + from: line.from, |
| 571 | + to: line.to, |
| 572 | + insert: textBeforeCursor + '\n' + newListItem + textAfterCursor |
| 573 | + }, |
| 574 | + selection: { |
| 575 | + anchor: line.from + textBeforeCursor.length + 1 + newListItem.length |
| 576 | + } |
| 577 | + }); |
| 578 | + return true; |
| 579 | + } |
| 580 | + |
| 581 | + // Not a list, use default Enter behavior |
| 582 | + return false; |
| 583 | +}; |
| 584 | + |
| 585 | +export const listContinuationKeymap: KeyBinding[] = [ |
| 586 | + { |
| 587 | + key: "Enter", |
| 588 | + run: handleEnterForListContinuation |
| 589 | + } |
| 590 | +]; |
0 commit comments