diff --git a/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-first-content-block.unit.spec.ts b/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-first-content-block.unit.spec.ts new file mode 100644 index 000000000..34652502b --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-first-content-block.unit.spec.ts @@ -0,0 +1,89 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, it } from 'vitest'; + +import { getFirstContentBlockCommand } from '../../../commands/block-crud/get-first-content-block'; +import { affine } from '../../helpers/affine-template'; + +describe('commands/block-crud', () => { + describe('getFirstContentBlockCommand', () => { + it('should return null when root is not provided and no note block exists', () => { + const host = affine``; + + const [_, { firstBlock }] = host.command.exec( + getFirstContentBlockCommand, + { + root: undefined, + std: { + host, + } as any, + } + ); + + expect(firstBlock).toBeNull(); + }); + + it('should return first content block when found', () => { + const host = affine` + + + First Paragraph + Second Paragraph + + + `; + + const [_, { firstBlock }] = host.command.exec( + getFirstContentBlockCommand, + { + root: undefined, + } + ); + + expect(firstBlock?.id).toBe('paragraph-1'); + }); + + it('should return null when no content blocks are found in children', () => { + const host = affine` + + + + + `; + + const [_, { firstBlock }] = host.command.exec( + getFirstContentBlockCommand, + {} + ); + + expect(firstBlock).toBeNull(); + }); + + it('should return first content block within specified root subtree', () => { + const host = affine` + + + 1-1 Paragraph + 1-2 Paragraph + + + 2-1 Paragraph + 2-2 Paragraph + + + `; + + const noteBlock = host.doc.getBlock('note-2')?.model; + + const [_, { firstBlock }] = host.command.exec( + getFirstContentBlockCommand, + { + root: noteBlock, + } + ); + + expect(firstBlock?.id).toBe('paragraph-2-1'); + }); + }); +}); diff --git a/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-last-content-block.unit.spec.ts b/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-last-content-block.unit.spec.ts new file mode 100644 index 000000000..8b1487b8c --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/commands/block-crud/get-last-content-block.unit.spec.ts @@ -0,0 +1,80 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, it } from 'vitest'; + +import { getLastContentBlockCommand } from '../../../commands/block-crud/get-last-content-block'; +import { affine } from '../../helpers/affine-template'; + +describe('commands/block-crud', () => { + describe('getLastContentBlockCommand', () => { + it('should return null when root is not provided and no note block exists', () => { + const host = affine``; + + const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, { + root: undefined, + std: { + host, + } as any, + }); + + expect(lastBlock).toBeNull(); + }); + + it('should return last content block when found', () => { + const host = affine` + + + First Paragraph + Second Paragraph + + + `; + + const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, { + root: undefined, + }); + + expect(lastBlock?.id).toBe('paragraph-2'); + }); + + it('should return null when no content blocks are found in children', () => { + const host = affine` + + + + + `; + + const [_, { lastBlock }] = host.command.exec( + getLastContentBlockCommand, + {} + ); + + expect(lastBlock).toBeNull(); + }); + + it('should return last content block within specified root subtree', () => { + const host = affine` + + + 1-1 Paragraph + 1-2 Paragraph + + + 2-1 Paragraph + 2-2 Paragraph + + + `; + + const noteBlock = host.doc.getBlock('note-2')?.model; + + const [_, { lastBlock }] = host.command.exec(getLastContentBlockCommand, { + root: noteBlock, + }); + + expect(lastBlock?.id).toBe('paragraph-2-2'); + }); + }); +}); diff --git a/blocksuite/affine/shared/src/__tests__/commands/selection/is-nothing-selected.unit.spec.ts b/blocksuite/affine/shared/src/__tests__/commands/selection/is-nothing-selected.unit.spec.ts new file mode 100644 index 000000000..550adfd57 --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/commands/selection/is-nothing-selected.unit.spec.ts @@ -0,0 +1,147 @@ +/** + * @vitest-environment happy-dom + */ +import { BlockSelection, TextSelection } from '@blocksuite/block-std'; +import { describe, expect, it, vi } from 'vitest'; + +import { isNothingSelectedCommand } from '../../../commands/selection/is-nothing-selected'; +import { ImageSelection } from '../../../selection'; +import { affine } from '../../helpers/affine-template'; + +describe('commands/selection', () => { + describe('isNothingSelectedCommand', () => { + it('should return true when nothing is selected', () => { + const host = affine``; + + const [_, { isNothingSelected }] = host.command.exec( + isNothingSelectedCommand, + {} + ); + + expect(isNothingSelected).toBe(true); + }); + + it('should return false when text selection exists', () => { + const host = affine` + + + Test paragraph + + + `; + + // Mock text selection + const textSelection = new TextSelection({ + from: { + blockId: 'paragraph-1', + index: 0, + length: 0, + }, + to: { + blockId: 'paragraph-1', + index: 4, + length: 0, + }, + }); + + const [_, { isNothingSelected }] = host.command.exec( + isNothingSelectedCommand, + { + currentTextSelection: textSelection, + } + ); + + expect(isNothingSelected).toBe(false); + }); + + it('should return false when block selection exists', () => { + const host = affine` + + + Test paragraph + + + `; + + // Mock block selection + const blockSelection = new BlockSelection({ + blockId: 'paragraph-1', + }); + + const [_, { isNothingSelected }] = host.command.exec( + isNothingSelectedCommand, + { + currentBlockSelections: [blockSelection], + } + ); + + expect(isNothingSelected).toBe(false); + }); + + it('should return false when image selection exists', () => { + const host = affine` + + + Test paragraph + + + `; + + // Mock image selection + const imageSelection = new ImageSelection({ + blockId: 'image-1', + }); + + const [_, { isNothingSelected }] = host.command.exec( + isNothingSelectedCommand, + { + currentImageSelections: [imageSelection], + } + ); + + expect(isNothingSelected).toBe(false); + }); + + it('should return false when no selection is provided but selection is found in context', () => { + const host = affine` + + + Test paragraph + + + `; + + // Mock selection behavior via vi.spyOn before executing the command + const mockTextSelection = new TextSelection({ + from: { + blockId: 'paragraph-1', + index: 0, + length: 0, + }, + to: null, + }); + + const mockContext = { + // No explicit `currentTextSelection provided + std: { + selection: { + find: vi.fn().mockImplementation(type => { + if (type === TextSelection) { + return mockTextSelection; + } + return null; + }), + filter: vi.fn().mockReturnValue([]), + }, + }, + }; + + const [_, { isNothingSelected }] = host.command.exec( + isNothingSelectedCommand, + mockContext as any + ); + + expect(isNothingSelected).toBe(false); + }); + }); +}); diff --git a/blocksuite/affine/shared/src/__tests__/helpers/README.md b/blocksuite/affine/shared/src/__tests__/helpers/README.md new file mode 100644 index 000000000..933058a0a --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/helpers/README.md @@ -0,0 +1,79 @@ +# AFFiNE Test Tools + +## Structured Document Creation + +`affine-template.ts` provides a concise way to create test documents, using a html-like syntax. + +### Basic Usage + +```typescript +import { affine } from '../__tests__/utils/affine-template'; + +// Create a simple document +const doc = affine` + + + Hello, World! + + +`; +``` + +### Complex Structure Example + +```typescript +// Create a document with multiple notes and paragraphs +const doc = affine` + + + First paragraph + Second paragraph + + + Another note + + +`; +``` + +### Application in Tests + +This tool is particularly suitable for creating documents with specific structures in test cases: + +```typescript +import { describe, expect, it } from 'vitest'; +import { affine } from '../__tests__/utils/affine-template'; + +describe('My Test', () => { + it('should correctly handle document structure', () => { + const doc = affine` + + + Test content + + + `; + + // Get blocks + const pages = doc.getBlocksByFlavour('affine:page'); + const notes = doc.getBlocksByFlavour('affine:note'); + const paragraphs = doc.getBlocksByFlavour('affine:paragraph'); + + expect(pages.length).toBe(1); + expect(notes.length).toBe(1); + expect(paragraphs.length).toBe(1); + + // Perform more tests here... + }); +}); +``` + +### Supported Block Types + +Currently supports the following block types: + +- `affine-page` → `affine:page` +- `affine-note` → `affine:note` +- `affine-paragraph` → `affine:paragraph` +- `affine-list` → `affine:list` +- `affine-image` → `affine:image` diff --git a/blocksuite/affine/shared/src/__tests__/helpers/affine-template.ts b/blocksuite/affine/shared/src/__tests__/helpers/affine-template.ts new file mode 100644 index 000000000..93615dd4f --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/helpers/affine-template.ts @@ -0,0 +1,179 @@ +import { + ImageBlockSchemaExtension, + ListBlockSchemaExtension, + NoteBlockSchemaExtension, + ParagraphBlockSchemaExtension, + RootBlockSchemaExtension, +} from '@blocksuite/affine-model'; +import { type Block, type Store } from '@blocksuite/store'; +import { Text } from '@blocksuite/store'; +import { TestWorkspace } from '@blocksuite/store/test'; + +import { createTestHost } from './create-test-host'; + +// Extensions array +const extensions = [ + RootBlockSchemaExtension, + NoteBlockSchemaExtension, + ParagraphBlockSchemaExtension, + ListBlockSchemaExtension, + ImageBlockSchemaExtension, +]; + +// Mapping from tag names to flavours +const tagToFlavour: Record = { + 'affine-page': 'affine:page', + 'affine-note': 'affine:note', + 'affine-paragraph': 'affine:paragraph', + 'affine-list': 'affine:list', + 'affine-image': 'affine:image', +}; + +/** + * Parse template strings and build BlockSuite document structure, + * then create a host object with the document + * + * Example: + * ``` + * const host = affine` + * + * + * Hello, world + * + * + * `; + * ``` + */ +export function affine(strings: TemplateStringsArray, ...values: any[]) { + // Merge template strings and values + let htmlString = ''; + strings.forEach((str, i) => { + htmlString += str; + if (i < values.length) { + htmlString += values[i]; + } + }); + + // Create a new doc + const workspace = new TestWorkspace({}); + workspace.meta.initialize(); + const doc = workspace.createDoc({ id: 'test-doc', extensions }); + + // Use DOMParser to parse HTML string + doc.load(() => { + const parser = new DOMParser(); + const dom = parser.parseFromString(htmlString.trim(), 'text/html'); + const root = dom.body.firstElementChild; + + if (!root) { + throw new Error('Template must contain a root element'); + } + + buildDocFromElement(doc, root, null); + }); + + // Create and return a host object with the document + return createTestHost(doc); +} + +/** + * Create a single block from template string + * + * Example: + * ``` + * const block = block`` + * ``` + */ +export function block( + strings: TemplateStringsArray, + ...values: any[] +): Block | null { + // Merge template strings and values + let htmlString = ''; + strings.forEach((str, i) => { + htmlString += str; + if (i < values.length) { + htmlString += values[i]; + } + }); + + // Create a temporary doc to hold the block + const workspace = new TestWorkspace({}); + workspace.meta.initialize(); + const doc = workspace.createDoc({ id: 'temp-doc', extensions }); + + let blockId: string | null = null; + + // Use DOMParser to parse HTML string + doc.load(() => { + const parser = new DOMParser(); + const dom = parser.parseFromString(htmlString.trim(), 'text/html'); + const root = dom.body.firstElementChild; + + if (!root) { + throw new Error('Template must contain a root element'); + } + + blockId = buildDocFromElement(doc, root, null); + }); + + // Return the created block + return blockId ? (doc.getBlock(blockId) ?? null) : null; +} + +/** + * Recursively build document structure + * @param doc + * @param element + * @param parentId + * @returns + */ +function buildDocFromElement( + doc: Store, + element: Element, + parentId: string | null +): string { + const tagName = element.tagName.toLowerCase(); + const flavour = tagToFlavour[tagName]; + + if (!flavour) { + throw new Error(`Unknown tag name: ${tagName}`); + } + + const props: Record = {}; + + const customId = element.getAttribute('id'); + + // If ID is specified, add it to props + if (customId) { + props.id = customId; + } + + // Process element attributes + Array.from(element.attributes).forEach(attr => { + if (attr.name !== 'id') { + // Skip id attribute, we already handled it + props[attr.name] = attr.value; + } + }); + + // Special handling for different block types based on their flavours + switch (flavour) { + case 'affine:paragraph': + case 'affine:list': + if (element.textContent) { + props.text = new Text(element.textContent); + } + break; + } + + // Create block + const blockId = doc.addBlock(flavour, props, parentId); + + // Recursively process child elements + Array.from(element.children).forEach(child => { + buildDocFromElement(doc, child, blockId); + }); + + return blockId; +} diff --git a/blocksuite/affine/shared/src/__tests__/helpers/affine-template.unit.spec.ts b/blocksuite/affine/shared/src/__tests__/helpers/affine-template.unit.spec.ts new file mode 100644 index 000000000..389fe9c68 --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/helpers/affine-template.unit.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; + +import { affine } from './affine-template'; + +describe('helpers/affine-template', () => { + it('should create a basic document structure from template', () => { + const host = affine` + + + Hello, world + + + `; + + expect(host.doc).toBeDefined(); + + const pageBlock = host.doc.getBlock('page'); + expect(pageBlock).toBeDefined(); + expect(pageBlock?.flavour).toBe('affine:page'); + + const noteBlock = host.doc.getBlock('note'); + expect(noteBlock).toBeDefined(); + expect(noteBlock?.flavour).toBe('affine:note'); + + const paragraphBlock = host.doc.getBlock('paragraph-1'); + expect(paragraphBlock).toBeDefined(); + expect(paragraphBlock?.flavour).toBe('affine:paragraph'); + }); + + it('should handle nested blocks correctly', () => { + const host = affine` + + + First paragraph + List item + Second paragraph + + + `; + + const noteBlocks = host.doc.getBlocksByFlavour('affine:note'); + const paragraphBlocks = host.doc.getBlocksByFlavour('affine:paragraph'); + const listBlocks = host.doc.getBlocksByFlavour('affine:list'); + + expect(noteBlocks.length).toBe(1); + expect(paragraphBlocks.length).toBe(2); + expect(listBlocks.length).toBe(1); + + const noteBlock = noteBlocks[0]; + const noteChildren = host.doc.getBlock(noteBlock.id)?.model.children || []; + expect(noteChildren.length).toBe(3); + + expect(noteChildren[0].flavour).toBe('affine:paragraph'); + expect(noteChildren[1].flavour).toBe('affine:list'); + expect(noteChildren[2].flavour).toBe('affine:paragraph'); + }); + + it('should handle empty blocks correctly', () => { + const host = affine` + + + + + + `; + + const paragraphBlocks = host.doc.getBlocksByFlavour('affine:paragraph'); + expect(paragraphBlocks.length).toBe(1); + + const paragraphBlock = host.doc.getBlock(paragraphBlocks[0].id); + const paragraphText = paragraphBlock?.model.text?.toString() || ''; + expect(paragraphText).toBe(''); + }); + + it('should throw error on invalid template', () => { + expect(() => { + affine` + + `; + }).toThrow(); + }); +}); diff --git a/blocksuite/affine/shared/src/__tests__/helpers/create-test-doc.ts b/blocksuite/affine/shared/src/__tests__/helpers/create-test-doc.ts new file mode 100644 index 000000000..a5c5a9c4c --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/helpers/create-test-doc.ts @@ -0,0 +1,135 @@ +import { + BlockSchemaExtension, + defineBlockSchema, + type Store, + Text, +} from '@blocksuite/store'; +import { TestWorkspace } from '@blocksuite/store/test'; +import { type Element as HappyDOMElement, Window } from 'happy-dom'; + +// Define schema +const PageBlockSchema = defineBlockSchema({ + flavour: 'affine:page', + props: () => ({}), + metadata: { + version: 1, + role: 'root', + children: ['affine:note'], + }, +}); + +const NoteBlockSchema = defineBlockSchema({ + flavour: 'affine:note', + props: () => ({}), + metadata: { + version: 1, + role: 'hub', + parent: ['affine:page'], + children: ['affine:paragraph'], + }, +}); + +const ParagraphBlockSchema = defineBlockSchema({ + flavour: 'affine:paragraph', + props: internal => ({ + text: internal.Text(), + }), + metadata: { + version: 1, + role: 'content', + parent: ['affine:note'], + }, +}); + +// Create schema extensions +const PageBlockSchemaExtension = BlockSchemaExtension(PageBlockSchema); +const NoteBlockSchemaExtension = BlockSchemaExtension(NoteBlockSchema); +const ParagraphBlockSchemaExtension = + BlockSchemaExtension(ParagraphBlockSchema); + +// Extensions array +const extensions = [ + PageBlockSchemaExtension, + NoteBlockSchemaExtension, + ParagraphBlockSchemaExtension, +]; + +/** + * Parse HTML string and create document block structure + * @param node Current DOM node + * @param doc Document object + * @param parentId Parent block ID + * @returns Created block ID + */ +function processNode( + node: HappyDOMElement, + doc: Store, + parentId?: string +): string | undefined { + // Skip text nodes and comments + if (node.nodeType !== 1) { + return undefined; + } + + const tagName = node.tagName.toLowerCase(); + let blockId: string | undefined = undefined; + + // Create appropriate block based on tag name + if (tagName === 'affine-page') { + blockId = doc.addBlock('affine:page', {}, parentId); + } else if (tagName === 'affine-note') { + blockId = doc.addBlock('affine:note', {}, parentId); + } else if (tagName === 'affine-paragraph') { + // Get paragraph text content + const textContent = node.textContent || ''; + // Get attributes + const props: Record = { text: new Text(textContent) }; + + // Process custom attributes + for (const attr of Array.from(node.attributes)) { + if (attr.name === 'type') { + props.type = attr.value; + } else if (attr.name === 'checked' && attr.value === 'true') { + props.checked = true; + } + } + + blockId = doc.addBlock('affine:paragraph', props, parentId); + } else { + console.warn(`Unknown tag name: ${tagName}`); + return undefined; + } + + // Process child nodes + for (const childNode of Array.from(node.children) as HappyDOMElement[]) { + processNode(childNode, doc, blockId); + } + + return blockId; +} + +/** + * Create document from HTML string + * @param template HTML template string + * @returns Created document object + */ +export function createDocFromHTML(template: string) { + const workspace = new TestWorkspace({}); + workspace.meta.initialize(); + + const doc = workspace.createDoc({ id: 'test-doc', extensions }); + + doc.load(() => { + const window = new Window(); + const document = window.document; + const container = document.createElement('div'); + container.innerHTML = template; + + // Process each child node of the root + for (const childNode of Array.from(container.children)) { + processNode(childNode, doc); + } + }); + + return doc; +} diff --git a/blocksuite/affine/shared/src/__tests__/helpers/create-test-host.ts b/blocksuite/affine/shared/src/__tests__/helpers/create-test-host.ts new file mode 100644 index 000000000..bfd999e83 --- /dev/null +++ b/blocksuite/affine/shared/src/__tests__/helpers/create-test-host.ts @@ -0,0 +1,246 @@ +import { CommandManager, type EditorHost } from '@blocksuite/block-std'; +import type { Block, Store } from '@blocksuite/store'; +import { Subject } from 'rxjs'; + +interface MockBlockComponent { + id: string; + model: Block; + flavour: string; + role: string; + parentElement: MockBlockComponent | null; + closest: (selector: string) => MockBlockComponent | null; + querySelector: (selector: string) => MockBlockComponent | null; + querySelectorAll: (selector: string) => MockBlockComponent[]; + children: MockBlockComponent[]; +} + +type ViewUpdateMethod = 'delete' | 'add'; +type ViewUpdatePayload = { + id: string; + method: ViewUpdateMethod; + type: 'block'; + view: MockBlockComponent; +}; + +/** + * Mock selection class for testing + */ +class MockSelectionStore { + private _selections: any[] = []; + + constructor() {} + + get value() { + return this._selections; + } + + create(selectionClass: any, ...args: any[]) { + return new selectionClass(...args); + } + + setGroup(group: string, selections: any[]) { + this._selections = this._selections.filter(s => s.group !== group); + this._selections.push(...selections); + return this; + } + + set(selections: any[]) { + this._selections = selections; + return this; + } + + find(type: any) { + return this._selections.find(s => s instanceof type); + } + + filter(type: any) { + return this._selections.filter(s => s instanceof type); + } + + clear() { + this._selections = []; + return this; + } + + slots = { + changed: { + emit: () => {}, + }, + remoteChanged: { + emit: () => {}, + }, + }; + + dispose() { + this._selections = []; + } +} + +class MockViewStore { + private readonly _blockMap = new Map(); + viewUpdated = new Subject(); + + constructor(private readonly doc: Store) {} + + get views() { + return Array.from(this._blockMap.values()); + } + + deleteBlock(node: MockBlockComponent) { + this._blockMap.delete(node.model.id); + this.viewUpdated.next({ + id: node.model.id, + method: 'delete', + type: 'block', + view: node, + }); + } + + getBlock(id: string): MockBlockComponent | null { + if (this._blockMap.has(id)) { + return this._blockMap.get(id) || null; + } + + const block = this.doc.getBlock(id); + if (!block) return null; + + const mockComponent = this._createMockBlockComponent(block); + this._blockMap.set(id, mockComponent); + + return mockComponent; + } + + setBlock(node: MockBlockComponent) { + if (this._blockMap.has(node.model.id)) { + this.deleteBlock(node); + } + this._blockMap.set(node.model.id, node); + this.viewUpdated.next({ + id: node.model.id, + method: 'add', + type: 'block', + view: node, + }); + } + + private _createMockBlockComponent(block: Block): MockBlockComponent { + const role = this._determineBlockRole(block); + + const mockComponent: MockBlockComponent = { + id: block.id, + model: block, + flavour: block.flavour, + role, + parentElement: null, + children: [], + closest: () => null, + querySelector: () => null, + querySelectorAll: () => [], + }; + + this._setupParentChildRelationships(mockComponent); + + return mockComponent; + } + + private _determineBlockRole(block: Block): string { + if ( + block.flavour.includes('paragraph') || + block.flavour.includes('list') || + block.flavour.includes('list-item') || + block.flavour.includes('text') + ) { + return 'content'; + } + return 'root'; + } + + private _setupParentChildRelationships(component: MockBlockComponent) { + const parentId = (component.model as any).parentId; + if (parentId) { + const parentComponent = this.getBlock(parentId); + if (parentComponent) { + component.parentElement = parentComponent; + + if ( + !parentComponent.children.find(child => child.id === component.id) + ) { + parentComponent.children.push(component); + } + } + } + + try { + const childIds = + (component.model as any).children?.map((child: any) => + typeof child === 'string' ? child : child.id + ) || []; + + for (const childId of childIds) { + const childBlock = this.doc.getBlock(childId); + if (childBlock) { + const childComponent = + this.getBlock(childId) || + this._createMockBlockComponent(childBlock); + if ( + !component.children.find(child => child.id === childComponent.id) + ) { + component.children.push(childComponent); + childComponent.parentElement = component; + } + } + } + } catch { + // ignore + } + } + + dispose() { + this._blockMap.clear(); + } +} + +/** + * Create a test host object + * + * This function creates a mock host object that includes doc and command properties, + * which can be used for testing command execution. + * + * Usage: + * ```typescript + * const doc = affine``; + * const host = createTestHost(doc); + * + * // Use host.command.exec to execute commands + * const [_, result] = host.command.exec(someCommand, { + * // command params + * }); + * ``` + * + * @param doc Document object + * @returns Host object containing doc and command + */ +export function createTestHost(doc: Store): EditorHost { + const std = { + host: undefined as any, + view: new MockViewStore(doc), + command: undefined as any, + selection: undefined as any, + }; + + const host = { + doc: doc, + std: std as any, + }; + host.doc = doc; + host.std = std as any; + + std.host = host; + std.selection = new MockSelectionStore(); + + std.command = new CommandManager(std as any); + // @ts-expect-error + host.command = std.command; + + return host as EditorHost; +} diff --git a/blocksuite/affine/shared/src/commands/index.ts b/blocksuite/affine/shared/src/commands/index.ts index 2ca07d21b..b2d8eb6f5 100644 --- a/blocksuite/affine/shared/src/commands/index.ts +++ b/blocksuite/affine/shared/src/commands/index.ts @@ -1,5 +1,7 @@ export { getBlockIndexCommand, + getFirstContentBlockCommand, + getLastContentBlockCommand, getNextBlockCommand, getPrevBlockCommand, getSelectedBlocksCommand, @@ -21,5 +23,6 @@ export { getRangeRects, getSelectionRectsCommand, getTextSelectionCommand, + isNothingSelectedCommand, type SelectionRect, } from './selection/index.js';