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
84 changes: 55 additions & 29 deletions packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,42 +48,80 @@ type KnownCommandRecord = {
};

/**
* A chainable version of an editor command keyed by command name.
* Union of all command interfaces via explicit imports.
* Module augmentation doesn't survive the npm boundary, so this is the
* single source of truth for the built-in command surface. Used by
* EditorCommands, ChainableCommandObject, and CanObject.
*/
export type ChainedCommand<K extends string = string> = (...args: CommandArgs<K>) => ChainableCommandObject;
type AllCommandSignatures = CoreCommandSignatures &
CommentCommands &
FormattingCommandAugmentations &
HistoryLinkTableCommandAugmentations &
SpecializedCommandAugmentations &
ParagraphCommands &
BlockNodeCommands &
ImageCommands &
MiscellaneousCommands &
TrackChangesCommands;

type KnownChainedCommands = {
/**
* Transforms a command interface so every method returns ChainableCommandObject
* instead of boolean, preserving parameter types.
*/
type Chainified<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K];
};

/**
* Commands from module augmentation, transformed for chaining.
* Empty for npm consumers (augmentation doesn't survive the boundary),
* but consumers who augment ExtensionCommandMap get their custom commands
* on chain() for free.
*/
type AugmentedChainedCommands = {
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => ChainableCommandObject;
};

/** Same as AugmentedChainedCommands but with original return types for can(). */
type AugmentedCanCommands = {
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => CommandResult<K>;
};

/**
* A chainable version of an editor command keyed by command name.
*/
export type ChainedCommand<K extends string = string> = (...args: CommandArgs<K>) => ChainableCommandObject;

/**
* Chainable command object returned by `createChain`.
* Has dynamic keys (one per command) and a `run()` method.
* Only `run()` returns boolean — all other methods return ChainableCommandObject.
*
* Includes AugmentedChainedCommands so consumers who extend ExtensionCommandMap
* via module augmentation get their custom commands on chain() automatically.
*/
export type ChainableCommandObject = {
run: () => boolean;
} & KnownChainedCommands &
Record<string, (...args: unknown[]) => ChainableCommandObject>;
} & Chainified<AllCommandSignatures> &
AugmentedChainedCommands;

/**
* A command that can be checked for availability.
*/
export type CanCommand<K extends string = string> = (...args: CommandArgs<K>) => CommandResult<K>;

type KnownCanCommands = {
[K in keyof RegisteredCommands]: (...args: CommandArgs<K>) => CommandResult<K>;
};

/**
* Map of commands that can be checked.
*/
export type CanCommands = Record<string, CanCommand>;

/**
* Object returned by `createCan`: dynamic boolean commands + a `chain()` helper.
* Object returned by `createCan`: typed boolean commands + a `chain()` helper.
*
* Includes AugmentedCanCommands so consumers who extend ExtensionCommandMap
* via module augmentation get their custom commands on can() automatically.
*/
export type CanObject = KnownCanCommands &
Record<string, CanCommand> & {
export type CanObject = AllCommandSignatures &
AugmentedCanCommands & {
chain: () => ChainableCommandObject;
};

Expand All @@ -100,23 +138,11 @@ export type ExtensionCommands = Pick<KnownCommandRecord, keyof ExtensionCommandM
/**
* All available editor commands.
*
* Composed from explicit imports of each command interface for reliable
* cross-package typing (module augmentation doesn't survive the npm boundary).
* The Record<string, AnyCommand> fallback allows dynamic/plugin commands.
* Composed from AllCommandSignatures (explicit imports) for reliable
* cross-package typing, plus CoreCommands/ExtensionCommands (module
* augmentation) and a Record fallback for dynamic/plugin commands.
*/
export type EditorCommands = CoreCommands &
ExtensionCommands &
CoreCommandSignatures &
CommentCommands &
FormattingCommandAugmentations &
HistoryLinkTableCommandAugmentations &
SpecializedCommandAugmentations &
ParagraphCommands &
BlockNodeCommands &
ImageCommands &
MiscellaneousCommands &
TrackChangesCommands &
Record<string, AnyCommand>;
export type EditorCommands = CoreCommands & ExtensionCommands & AllCommandSignatures & Record<string, AnyCommand>;

/**
* Command props made available to every command handler.
Expand Down
9 changes: 9 additions & 0 deletions tests/consumer-typecheck/src/customer-scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,15 @@ function testEditorCommands(editor: Editor) {

// Chain API
editor.chain().toggleBold().toggleItalic().run();

// SD-2334: Chain intermediate methods must return ChainableCommandObject, not boolean.
// Reproduces IT-344 (Ontra): chain().setTextSelection(...).setMark(...).run()
const chainResult: ChainableCommandObject = editor.chain().setTextSelection({ from: 0, to: 5 });
const runResult: boolean = editor.chain().setTextSelection({ from: 0, to: 5 }).setMark('bold').run();

// SD-2334: can().chain() must return ChainableCommandObject, not boolean
const canChain: ChainableCommandObject = editor.can().chain();
const canChainRun: boolean = editor.can().chain().toggleBold().run();
}

function testPresentationEditorCommands(pe: PresentationEditor) {
Expand Down
Loading