Skip to content

Add Cmd/Ctrl + D shortcut for duplicating document blocks #8578 #8627

Open
acarolinacc wants to merge 4 commits intoAppFlowy-IO:mainfrom
leticia-003:fr/duplicate-block-shortcut
Open

Add Cmd/Ctrl + D shortcut for duplicating document blocks #8578 #8627
acarolinacc wants to merge 4 commits intoAppFlowy-IO:mainfrom
leticia-003:fr/duplicate-block-shortcut

Conversation

@acarolinacc
Copy link
Copy Markdown

@acarolinacc acarolinacc commented Apr 5, 2026

Feature Preview

A short demo video is attached below showing the new shortcut behavior after the fix:

  • duplicating a single block with Ctrl + D / Cmd + D
  • duplicating multiple selected blocks
  • displaying the shortcut in the block action menu
CES_After.mp4

Summary

This PR adds a keyboard shortcut for duplicating document blocks in AppFlowy.

The new shortcut works as follows:

  • Cmd + D on macOS
  • Ctrl + D on Windows/Linux

The implementation reuses the existing duplicate block behavior instead of creating a separate duplication path. It also supports both single-block and multi-block duplication, and shows the shortcut label in the block action menu to make the feature easier to discover.

Related issue

Closes #8578

Changes made

  • added a new shortcut command for block duplication
  • implemented support for duplicating both a single block and multiple selected blocks
  • displayed the shortcut label in the block action menu
  • added integration tests for the new behavior

Testing

Tested manually by:

  • duplicating a single block with the shortcut
  • duplicating multiple selected blocks with the shortcut
  • confirming that the shortcut label appears in the block action menu

Also validated with integration tests for:

  • single-block duplication
  • multi-block duplication

PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing.

Summary by Sourcery

Add a keyboard shortcut to duplicate document blocks and expose it in the editor UI.

New Features:

  • Introduce Cmd+D (macOS) / Ctrl+D (Windows/Linux) shortcut for duplicating selected document blocks, supporting both single-block and multi-block selections.
  • Display the duplicate shortcut hint next to the duplicate action in the block options menu.

Tests:

  • Add integration tests covering single-block duplication and multi-block duplication via the new keyboard shortcut.

Added tests for custom duplicate block commands in the editor, including single and multi-block selections.
Add right icon for duplicate option with platform-specific shortcut.
Implement custom command to duplicate selected blocks in the editor.
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Apr 5, 2026

Reviewer's Guide

Adds a Cmd/Ctrl + D keyboard shortcut that reuses the existing duplicate-block behavior, supports single and multi-block selections, and surfaces the shortcut in the block action menu, with integration tests covering the new shortcut behavior.

Sequence diagram for Cmd_or_Ctrl_D duplicate block shortcut handling

sequenceDiagram
  actor User
  participant EditorWidget
  participant CommandShortcutRegistry
  participant customDuplicateBlockCommand
  participant EditorState
  participant Transaction
  participant EditorNotification

  User->>EditorWidget: Press_Cmd_or_Ctrl_D
  EditorWidget->>CommandShortcutRegistry: onKeyEvent(ctrl_or_cmd_plus_d)
  CommandShortcutRegistry->>customDuplicateBlockCommand: invokeHandler(editorState)
  customDuplicateBlockCommand->>EditorState: read_selection()

  alt no_selection
    customDuplicateBlockCommand-->>CommandShortcutRegistry: KeyEventResult_ignored
  else block_or_multi_block_selection
    customDuplicateBlockCommand->>EditorState: transaction = transaction
    customDuplicateBlockCommand->>EditorState: getNodesInSelection(normalized_selection)
    customDuplicateBlockCommand->>Transaction: insertNodes(end_path.next, deep_copied_nodes)
  else caret_inside_single_block
    customDuplicateBlockCommand->>EditorState: getNodeAtPath(end_path)
    customDuplicateBlockCommand->>Transaction: insertNode(node.path.next, node.deepCopy())
  end

  customDuplicateBlockCommand->>EditorState: apply(transaction)
  EditorState-->>customDuplicateBlockCommand: Future_complete
  customDuplicateBlockCommand->>EditorNotification: paste().post()
Loading

Updated class diagram for duplicate block shortcut and option action menu

classDiagram

  class CommandShortcutEvent {
    String key
    String command
    String macOSCommand
    CommandShortcutEventHandler handler
    String getDescription()
  }

  class CommandShortcutEventHandler {
  }

  class customDuplicateBlockCommand {
    <<constant>>
    +CommandShortcutEvent value
  }

  class EditorState {
    Selection selection
    SelectionType selectionType
    Transaction transaction
    Selection get normalized
    List~Node~ getNodesInSelection(Selection selection)
    Node getNodeAtPath(Path path)
    Future apply(Transaction transaction)
  }

  class Transaction {
    void insertNodes(Path path, List~Node~ nodes)
    void insertNode(Path path, Node node)
  }

  class OptionActionWrapper {
    OptionAction inner
    Widget leftIcon(Color iconColor)
    Widget rightIcon(Color iconColor)
    String get name
  }

  class OptionAction {
    <<enum>>
    OptionAction duplicate
    String description
    String svg
  }

  class EditorNotification {
    static EditorNotification paste()
    void post()
  }

  class FlowyText {
    static Widget regular(String text, Color color, double fontSize)
  }

  class UniversalPlatform {
    static bool isMacOS
  }

  CommandShortcutEvent "1" --> "1" CommandShortcutEventHandler : uses
  customDuplicateBlockCommand "1" --> "1" CommandShortcutEvent : wraps
  customDuplicateBlockCommand "1" --> "1" EditorState : handler_uses
  EditorState "1" --> "1" Transaction : creates_and_uses
  customDuplicateBlockCommand "1" --> "1" EditorNotification : posts_paste

  OptionActionWrapper "1" --> "1" OptionAction : wraps
  OptionActionWrapper "1" --> "1" FlowyText : displays_shortcut_label
  OptionActionWrapper "1" --> "1" UniversalPlatform : checks_platform
Loading

File-Level Changes

Change Details Files
Add a dedicated shortcut command for duplicating the current block or the blocks in the current selection.
  • Define customDuplicateBlockCommand with ctrl+d / cmd+d bindings and localized description.
  • Implement handler that detects selection type, gathers affected nodes, and inserts deep copies immediately after the selection end.
  • Apply the transaction asynchronously and post an EditorNotification.paste() after duplication completes.
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_duplicate_block_command.dart
Register the duplicate-block shortcut with the editor’s command shortcut system.
  • Import the custom duplicate block command into the shortcuts registry.
  • Append customDuplicateBlockCommand to the commandShortcutEvents list so it’s active alongside other custom commands.
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart
Expose the new duplicate shortcut in the block option menu UI.
  • Add rightIcon implementation to OptionActionWrapper that renders a platform-specific shortcut label only for the duplicate option.
  • Use FlowyText and UniversalPlatform to render Cmd+D on macOS and Ctrl+D on other platforms with subtle styling.
frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart
Add integration tests to validate duplicate shortcut behavior for single and multi-block selections.
  • Create a test that sets up two paragraph blocks, places a collapsed caret in the first block, sends the platform-appropriate D key event with Ctrl/Cmd, and asserts that the first block is duplicated before the second.
  • Create a test that sets up three paragraph blocks, selects across the first two, triggers the shortcut, and asserts that only the first two blocks are duplicated in order after the selection while preserving the third block.
  • Use UniversalPlatform to send the correct modifier (Control vs Meta) when simulating the shortcut in tests.
frontend/appflowy_flutter/integration_test/desktop/document/document_shortcuts_test.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#8578 Add a keyboard shortcut so that Cmd+D on macOS and Ctrl+D on Windows/Linux duplicate the current document block, and when multiple blocks are selected the shortcut duplicates all selected blocks.
#8578 Show the Cmd/Ctrl + D shortcut next to the Duplicate action in the document block action menu.
#8578 Expose the new duplicate-block shortcut in Settings > Shortcuts so it is listed among other editor shortcuts.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The Cmd+D / Ctrl+D label in OptionActionWrapper.rightIcon is hard-coded and duplicates the shortcut definition in custom_duplicate_block_command; consider deriving the display string from the same shortcut configuration (or a shared helper) so that changing the keybinding in one place automatically updates the menu label.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Cmd+D` / `Ctrl+D` label in `OptionActionWrapper.rightIcon` is hard-coded and duplicates the shortcut definition in `custom_duplicate_block_command`; consider deriving the display string from the same shortcut configuration (or a shared helper) so that changing the keybinding in one place automatically updates the menu label.

## Individual Comments

### Comment 1
<location path="frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart" line_range="139-148" />
<code_context>
   @override
   Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);

+  @override
+  Widget? rightIcon(Color iconColor) {
+    if (inner != OptionAction.duplicate) {
+      return null;
+    }
+
+    return FlowyText.regular(
+      UniversalPlatform.isMacOS ? 'Cmd+D' : 'Ctrl+D',
+      color: iconColor.withValues(alpha: 0.65),
</code_context>
<issue_to_address>
**suggestion:** Avoid hardcoding the shortcut label to keep it in sync with the actual keybinding configuration.

The `rightIcon` hardcodes `Cmd+D` / `Ctrl+D` while the actual shortcut is defined in `customDuplicateBlockCommand`. If the binding changes or becomes configurable, the label will be wrong. Please derive this label from the same source as the shortcut (or a shared constant) so the UI shortcut hint always matches the real keybinding.

Suggested implementation:

```
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// TODO: Import the module that defines `customDuplicateBlockCommand` or exposes
// a shared shortcut label for the duplicate block action.
// import 'package:appflowy_editor/commands/custom_duplicate_block_command.dart';

```

```
  @override
  Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);

  @override
  Widget? rightIcon(Color iconColor) {
    if (inner != OptionAction.duplicate) {
      return null;
    }

    // Use the same source of truth as the duplicate block keybinding so the
    // label always matches the actual shortcut configuration.
    final shortcutLabel = customDuplicateBlockCommand.shortcutLabel;

    if (shortcutLabel == null || shortcutLabel.isEmpty) {
      // If no shortcut is configured, don't show a hint.
      return null;
    }

    return FlowyText.regular(
      shortcutLabel,
      color: iconColor.withValues(alpha: 0.65),
      fontSize: 12,
    );
  }

```

1. Ensure that `customDuplicateBlockCommand` is accessible in this file:
   - If it already exists (e.g. in a commands/shortcuts file), export it or import the defining file here.
   - The object should expose a `shortcutLabel` (or similarly named) property or getter that returns a user-facing label like `Cmd+D` / `Ctrl+D` based on the current keybinding configuration and platform.
2. If `customDuplicateBlockCommand` is currently only defining the keybinding but not a label:
   - Add a `String get shortcutLabel` to that command (or a shared helper) that builds the label from its configured keybinding(s) instead of hardcoding platform-specific strings in the UI.
3. Once the shared shortcut label API is in place, remove any now-unused imports such as `universal_platform` from this file.
</issue_to_address>

### Comment 2
<location path="frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_duplicate_block_command.dart" line_range="14-23" />
<code_context>
+final CommandShortcutEvent customDuplicateBlockCommand = CommandShortcutEvent(
</code_context>
<issue_to_address>
**suggestion:** Consider updating the selection to target the newly duplicated block(s) after the command runs.

Right now the selection stays on the original block(s), which can lead to accidental edits there and makes repeated `Cmd/Ctrl+D` less fluid. If the editor API supports it, update `editorState.selection` in the transaction to focus or select the newly created block range instead of the originals.

Suggested implementation:

```
final CommandShortcutEvent customDuplicateBlockCommand = CommandShortcutEvent(
  key: 'duplicate selected block',
  getDescription: () => LocaleKeys.document_plugins_optionAction_duplicate.tr(),
  command: 'ctrl+d',
  macOSCommand: 'cmd+d',
  handler: _duplicateBlockCommandHandler,
  // NOTE:
  // When duplicating, the selection should be moved to the newly created block(s)
  // so that repeated Cmd/Ctrl+D and subsequent edits operate on the duplicates
  // instead of the original blocks. The handler below is responsible for
  // updating the selection in the same transaction that performs the duplication.
);

```

To fully implement “selection moves to duplicated blocks”, you’ll need to update the body of `_duplicateBlockCommandHandler`. Since that part of the file isn’t visible here, below is a concrete pattern you can adapt to your actual duplication logic:

1. **Capture the original selection and the blocks being duplicated**

   Right after your existing null-check, derive which block nodes are being duplicated and their paths:

   ```dart
   CommandShortcutEventHandler _duplicateBlockCommandHandler = (editorState) {
     final selection = editorState.selection;
     if (selection == null) {
       return KeyEventResult.ignored; // or whatever you currently return
     }

     // 1. Get all blocks in the current selection (API name may differ in your codebase)
     final selectedNodes = editorState.getNodesInSelection(selection)
         .where((node) => node.isBlock) // if needed
         .toList();

     if (selectedNodes.isEmpty) {
       return KeyEventResult.ignored;
     }

     final firstOriginalPath = selectedNodes.first.path;
     final lastOriginalPath = selectedNodes.last.path;
   ```

2. **Duplicate the blocks as you already do, but track the insertion position**

   Wherever you are currently duplicating blocks, make sure you know the insertion index/path of the first duplicated block. For example, if you insert immediately after the last original block:

   ```dart
     final transaction = editorState.transaction;

     // This assumes you insert new blocks after the last original block.
     // Adjust indices to match your actual insertion logic.
     final insertionParentPath = lastOriginalPath.parent;
     final insertionIndex = lastOriginalPath.index + 1;

     final duplicatedNodes = <Node>[];

     for (final node in selectedNodes) {
       final cloned = node.copyWith(
         // If your Node has a dedicated clone method, use that instead.
         // Make sure IDs are regenerated if your model requires it.
       );

       transaction.insertNode(
         insertionParentPath,
         insertionIndex + duplicatedNodes.length,
         cloned,
       );

       duplicatedNodes.add(cloned);
     }
   ```

3. **Compute the selection for the duplicated blocks**

   After you've scheduled the insertions on the transaction, compute a new `Selection` that spans from the first duplicated block to the last duplicated block:

   ```dart
     if (duplicatedNodes.isNotEmpty) {
       final firstDuplicated = duplicatedNodes.first;
       final lastDuplicated = duplicatedNodes.last;

       // Depending on your editor model, you may want to:
       // - select the entire block(s)
       // - or place a caret at a specific offset (e.g., at the start of the first duplicated block)
       final newSelection = Selection(
         base: Position(path: firstDuplicated.path, offset: 0),
         extent: Position(path: lastDuplicated.path, offset: lastDuplicated.length),
       );

       // If the transaction supports setting an "after selection", prefer that:
       transaction.afterSelection = newSelection;

       // Otherwise, after applying the transaction:
       // editorState.selection = newSelection;
       // or
       // editorState.updateSelection(newSelection);
     }
   ```

4. **Apply the transaction and return the appropriate `KeyEventResult`**

   Ensure the selection update happens as part of the same edit cycle:

   ```dart
     editorState.apply(transaction);
     return KeyEventResult.handled;
   }
   ```

5. **Align with actual APIs in your codebase**

   - Replace `getNodesInSelection`, `insertNode`, `Selection`, `Position`, `transaction.afterSelection`, `editorState.apply`, and `node.copyWith` with the actual APIs from `appflowy_editor` and your models.
   - If your editor already exposes a helper like `editorState.duplicateSelection(selection)` that returns the paths of the new nodes, use that directly and only handle computing and setting the `Selection` for those paths.

In summary, the handler should:
1. Derive the list (and paths) of blocks being duplicated.
2. Duplicate them exactly as you currently do.
3. Compute a `Selection` that targets the newly inserted blocks.
4. Set that selection on the transaction (or on `editorState` after applying the transaction), so that subsequent `Cmd/Ctrl+D` operates on the duplicates.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 139 to +148
@override
Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);

@override
Widget? rightIcon(Color iconColor) {
if (inner != OptionAction.duplicate) {
return null;
}

return FlowyText.regular(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Avoid hardcoding the shortcut label to keep it in sync with the actual keybinding configuration.

The rightIcon hardcodes Cmd+D / Ctrl+D while the actual shortcut is defined in customDuplicateBlockCommand. If the binding changes or becomes configurable, the label will be wrong. Please derive this label from the same source as the shortcut (or a shared constant) so the UI shortcut hint always matches the real keybinding.

Suggested implementation:

import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
// TODO: Import the module that defines `customDuplicateBlockCommand` or exposes
// a shared shortcut label for the duplicate block action.
// import 'package:appflowy_editor/commands/custom_duplicate_block_command.dart';

  @override
  Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);

  @override
  Widget? rightIcon(Color iconColor) {
    if (inner != OptionAction.duplicate) {
      return null;
    }

    // Use the same source of truth as the duplicate block keybinding so the
    // label always matches the actual shortcut configuration.
    final shortcutLabel = customDuplicateBlockCommand.shortcutLabel;

    if (shortcutLabel == null || shortcutLabel.isEmpty) {
      // If no shortcut is configured, don't show a hint.
      return null;
    }

    return FlowyText.regular(
      shortcutLabel,
      color: iconColor.withValues(alpha: 0.65),
      fontSize: 12,
    );
  }

  1. Ensure that customDuplicateBlockCommand is accessible in this file:
    • If it already exists (e.g. in a commands/shortcuts file), export it or import the defining file here.
    • The object should expose a shortcutLabel (or similarly named) property or getter that returns a user-facing label like Cmd+D / Ctrl+D based on the current keybinding configuration and platform.
  2. If customDuplicateBlockCommand is currently only defining the keybinding but not a label:
    • Add a String get shortcutLabel to that command (or a shared helper) that builds the label from its configured keybinding(s) instead of hardcoding platform-specific strings in the UI.
  3. Once the shared shortcut label API is in place, remove any now-unused imports such as universal_platform from this file.

Comment on lines +14 to +23
final CommandShortcutEvent customDuplicateBlockCommand = CommandShortcutEvent(
key: 'duplicate selected block',
getDescription: () => LocaleKeys.document_plugins_optionAction_duplicate.tr(),
command: 'ctrl+d',
macOSCommand: 'cmd+d',
handler: _duplicateBlockCommandHandler,
);

CommandShortcutEventHandler _duplicateBlockCommandHandler = (editorState) {
final selection = editorState.selection;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Consider updating the selection to target the newly duplicated block(s) after the command runs.

Right now the selection stays on the original block(s), which can lead to accidental edits there and makes repeated Cmd/Ctrl+D less fluid. If the editor API supports it, update editorState.selection in the transaction to focus or select the newly created block range instead of the originals.

Suggested implementation:

final CommandShortcutEvent customDuplicateBlockCommand = CommandShortcutEvent(
  key: 'duplicate selected block',
  getDescription: () => LocaleKeys.document_plugins_optionAction_duplicate.tr(),
  command: 'ctrl+d',
  macOSCommand: 'cmd+d',
  handler: _duplicateBlockCommandHandler,
  // NOTE:
  // When duplicating, the selection should be moved to the newly created block(s)
  // so that repeated Cmd/Ctrl+D and subsequent edits operate on the duplicates
  // instead of the original blocks. The handler below is responsible for
  // updating the selection in the same transaction that performs the duplication.
);

To fully implement “selection moves to duplicated blocks”, you’ll need to update the body of _duplicateBlockCommandHandler. Since that part of the file isn’t visible here, below is a concrete pattern you can adapt to your actual duplication logic:

  1. Capture the original selection and the blocks being duplicated

    Right after your existing null-check, derive which block nodes are being duplicated and their paths:

    CommandShortcutEventHandler _duplicateBlockCommandHandler = (editorState) {
      final selection = editorState.selection;
      if (selection == null) {
        return KeyEventResult.ignored; // or whatever you currently return
      }
    
      // 1. Get all blocks in the current selection (API name may differ in your codebase)
      final selectedNodes = editorState.getNodesInSelection(selection)
          .where((node) => node.isBlock) // if needed
          .toList();
    
      if (selectedNodes.isEmpty) {
        return KeyEventResult.ignored;
      }
    
      final firstOriginalPath = selectedNodes.first.path;
      final lastOriginalPath = selectedNodes.last.path;
  2. Duplicate the blocks as you already do, but track the insertion position

    Wherever you are currently duplicating blocks, make sure you know the insertion index/path of the first duplicated block. For example, if you insert immediately after the last original block:

      final transaction = editorState.transaction;
    
      // This assumes you insert new blocks after the last original block.
      // Adjust indices to match your actual insertion logic.
      final insertionParentPath = lastOriginalPath.parent;
      final insertionIndex = lastOriginalPath.index + 1;
    
      final duplicatedNodes = <Node>[];
    
      for (final node in selectedNodes) {
        final cloned = node.copyWith(
          // If your Node has a dedicated clone method, use that instead.
          // Make sure IDs are regenerated if your model requires it.
        );
    
        transaction.insertNode(
          insertionParentPath,
          insertionIndex + duplicatedNodes.length,
          cloned,
        );
    
        duplicatedNodes.add(cloned);
      }
  3. Compute the selection for the duplicated blocks

    After you've scheduled the insertions on the transaction, compute a new Selection that spans from the first duplicated block to the last duplicated block:

      if (duplicatedNodes.isNotEmpty) {
        final firstDuplicated = duplicatedNodes.first;
        final lastDuplicated = duplicatedNodes.last;
    
        // Depending on your editor model, you may want to:
        // - select the entire block(s)
        // - or place a caret at a specific offset (e.g., at the start of the first duplicated block)
        final newSelection = Selection(
          base: Position(path: firstDuplicated.path, offset: 0),
          extent: Position(path: lastDuplicated.path, offset: lastDuplicated.length),
        );
    
        // If the transaction supports setting an "after selection", prefer that:
        transaction.afterSelection = newSelection;
    
        // Otherwise, after applying the transaction:
        // editorState.selection = newSelection;
        // or
        // editorState.updateSelection(newSelection);
      }
  4. Apply the transaction and return the appropriate KeyEventResult

    Ensure the selection update happens as part of the same edit cycle:

      editorState.apply(transaction);
      return KeyEventResult.handled;
    }
  5. Align with actual APIs in your codebase

    • Replace getNodesInSelection, insertNode, Selection, Position, transaction.afterSelection, editorState.apply, and node.copyWith with the actual APIs from appflowy_editor and your models.
    • If your editor already exposes a helper like editorState.duplicateSelection(selection) that returns the paths of the new nodes, use that directly and only handle computing and setting the Selection for those paths.

In summary, the handler should:

  1. Derive the list (and paths) of blocks being duplicated.
  2. Duplicate them exactly as you currently do.
  3. Compute a Selection that targets the newly inserted blocks.
  4. Set that selection on the transaction (or on editorState after applying the transaction), so that subsequent Cmd/Ctrl+D operates on the duplicates.

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.

[FR] Add Cmd/Ctrl + D shortcut for duplicating document blocks

2 participants