Compare commits

...

2 Commits

Author SHA1 Message Date
yoyoyohamapi
43a7285d16 feat(core): block diff ui 2025-06-26 16:40:41 +08:00
yoyoyohamapi
a6fd3deb84 feat(core): markdown-diff & patch apply 2025-06-26 15:07:22 +08:00
94 changed files with 2096 additions and 178 deletions

View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"emoji-mart": "^5.6.0",
"lit": "^3.2.0",

View File

@ -27,7 +27,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -28,7 +28,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"date-fns": "^4.0.0",
"lit": "^3.2.0",

View File

@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",

View File

@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -26,7 +26,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"file-type": "^21.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/katex": "^0.16.7",
"@types/mdast": "^4.0.4",
"katex": "^0.16.11",

View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -23,6 +23,7 @@ import { effect } from '@preact/signals-core';
import { html, nothing, type TemplateResult } from 'lit';
import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { correctNumberedListsOrderToPrev } from './commands/utils.js';
@ -138,6 +139,11 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
override renderBlock(): TemplateResult<1> {
const { model, _onClickIcon } = this;
const widgets = html`${repeat(
Object.entries(this.widgets),
([id]) => id,
([_, widget]) => widget
)}`;
const collapsed = this.store.readonly
? this._readonlyCollapsed
: model.props.collapsed;
@ -199,7 +205,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
></rich-text>
</div>
${children}
${children} ${widgets}
</div>
`;
}

View File

@ -27,7 +27,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",

View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -26,6 +26,7 @@ import { computed, effect, signal } from '@preact/signals-core';
import { html, nothing, type TemplateResult } from 'lit';
import { query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
@ -227,6 +228,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
}
override renderBlock(): TemplateResult<1> {
const widgets = html`${repeat(
Object.entries(this.widgets),
([id]) => id,
([_, widget]) => widget
)}`;
const { type$ } = this.model.props;
const collapsed = this.store.readonly
? this._readonlyCollapsed
@ -340,7 +347,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
`}
</div>
${children}
${children} ${widgets}
</div>
`;
}

View File

@ -44,7 +44,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"dompurify": "^3.2.4",
"html2canvas": "^1.4.1",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fractional-indexing": "^3.2.0",
"lit": "^3.2.0",

View File

@ -20,7 +20,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fractional-indexing": "^3.2.0",
"html2canvas": "^1.4.1",

View File

@ -21,7 +21,7 @@
"@lit/context": "^1.1.2",
"@lottiefiles/dotlottie-wc": "^0.5.0",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/hast": "^3.0.4",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",

View File

@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"clsx": "^2.1.1",
"date-fns": "^4.0.0",

View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1"
},

View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",

View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -24,7 +24,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@ -23,7 +23,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -26,7 +26,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -30,7 +30,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -26,7 +26,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -23,7 +23,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -23,7 +23,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"collapse-white-space": "^2.1.0",
"date-fns": "^4.0.0",

View File

@ -23,7 +23,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/hast": "^3.0.4",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",

View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"collapse-white-space": "^2.1.0",
"date-fns": "^4.0.0",

View File

@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"collapse-white-space": "^2.1.0",
"date-fns": "^4.0.0",

View File

@ -27,7 +27,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/hast": "^3.0.4",
"@types/katex": "^0.16.7",
"@types/lodash-es": "^4.17.12",

View File

@ -21,7 +21,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"collapse-white-space": "^2.1.0",
"date-fns": "^4.0.0",

View File

@ -13,7 +13,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fractional-indexing": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -20,7 +20,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"collapse-white-space": "^2.1.0",
"date-fns": "^4.0.0",

View File

@ -10,6 +10,8 @@
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
@ -18,7 +20,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/bytes": "^3.1.5",
"@types/hast": "^3.0.4",
"@types/lodash-es": "^4.17.12",
@ -63,7 +65,8 @@
"./theme": "./src/theme/index.ts",
"./styles": "./src/styles/index.ts",
"./services": "./src/services/index.ts",
"./adapters": "./src/adapters/index.ts"
"./adapters": "./src/adapters/index.ts",
"./test-utils": "./src/test-utils/index.ts"
},
"files": [
"src",

View File

@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import { getFirstBlockCommand } from '../../../commands/block-crud/get-first-content-block';
import { affine } from '../../helpers/affine-template';
import { affine } from '../../../test-utils';
describe('commands/block-crud', () => {
describe('getFirstBlockCommand', () => {

View File

@ -4,7 +4,7 @@
import { describe, expect, it } from 'vitest';
import { getLastBlockCommand } from '../../../commands/block-crud/get-last-content-block';
import { affine } from '../../helpers/affine-template';
import { affine } from '../../../test-utils';
describe('commands/block-crud', () => {
describe('getLastBlockCommand', () => {

View File

@ -1,13 +1,13 @@
/**
* @vitest-environment happy-dom
*/
import '../../helpers/affine-test-utils';
import '../../../test-utils/affine-test-utils';
import type { TextSelection } from '@blocksuite/std';
import { describe, expect, it } from 'vitest';
import { replaceSelectedTextWithBlocksCommand } from '../../../commands/model-crud/replace-selected-text-with-blocks';
import { affine, block } from '../../helpers/affine-template';
import { affine, block } from '../../../test-utils';
describe('commands/model-crud', () => {
describe('replaceSelectedTextWithBlocksCommand', () => {

View File

@ -6,7 +6,7 @@ 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';
import { affine } from '../../../test-utils';
describe('commands/selection', () => {
describe('isNothingSelectedCommand', () => {

View File

@ -1,7 +1,7 @@
import { TextSelection } from '@blocksuite/std';
import { describe, expect, it } from 'vitest';
import { affine } from './affine-template';
import { affine } from '../../test-utils';
describe('helpers/affine-template', () => {
it('should create a basic document structure from template', () => {

View File

@ -1,29 +1,32 @@
import {
CodeBlockSchemaExtension,
DatabaseBlockSchemaExtension,
ImageBlockSchemaExtension,
ListBlockSchemaExtension,
NoteBlockSchemaExtension,
ParagraphBlockSchemaExtension,
RootBlockSchemaExtension,
} from '@blocksuite/affine-model';
import { getInternalStoreExtensions } from '@blocksuite/affine/extensions/store';
import { StoreExtensionManager } from '@blocksuite/affine-ext-loader';
import { Container } from '@blocksuite/global/di';
import { TextSelection } from '@blocksuite/std';
import { type Block, type Store } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import { type Block, type Store, 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,
DatabaseBlockSchemaExtension,
CodeBlockSchemaExtension,
];
const manager = new StoreExtensionManager(getInternalStoreExtensions());
const extensions = manager.get('store');
// // Extensions array
// const extensions = [
// RootBlockSchemaExtension,
// NoteBlockSchemaExtension,
// ParagraphBlockSchemaExtension,
// ListBlockSchemaExtension,
// ImageBlockSchemaExtension,
// DatabaseBlockSchemaExtension,
// CodeBlockSchemaExtension,
// RootStoreExtension,
// NoteStoreExtension,
// ParagraphStoreExtension,
// ListStoreExtension,
// ImageStoreExtension,
// DatabaseStoreExtension,
// CodeStoreExtension
// ];
// Mapping from tag names to flavours
const tagToFlavour: Record<string, string> = {
@ -75,8 +78,11 @@ export function affine(strings: TemplateStringsArray, ...values: any[]) {
const workspace = new TestWorkspace({});
workspace.meta.initialize();
const doc = workspace.createDoc('test-doc');
const store = doc.getStore({ extensions });
const container = new Container();
extensions.forEach(extension => {
extension.setup(container);
});
const store = doc.getStore({ extensions, provider: container.provider() });
let selectionInfo: SelectionInfo = {};
// Use DOMParser to parse HTML string

View File

@ -63,10 +63,8 @@ function compareBlocks(
if (JSON.stringify(actualProps) !== JSON.stringify(expectedProps))
return false;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < actual.children.length; i++) {
if (!compareBlocks(actual.children[i], expected.children[i], compareId))
return false;
for (const [i, child] of actual.children.entries()) {
if (!compareBlocks(child, expected.children[i], compareId)) return false;
}
return true;

View File

@ -240,7 +240,7 @@ export function createTestHost(doc: Store): EditorHost {
std.selection = new MockSelectionStore();
std.command = new CommandManager(std as any);
// @ts-expect-error
// @ts-expect-error dev-only
host.command = std.command;
host.selection = std.selection;

View File

@ -0,0 +1,3 @@
export * from './affine-template';
export * from './affine-test-utils';
export * from './create-test-host';

View File

@ -27,7 +27,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -20,7 +20,7 @@
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1"
},

View File

@ -21,7 +21,7 @@
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1",
"yjs": "^13.6.21"

View File

@ -25,7 +25,7 @@
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1",
"yjs": "^13.6.21"

View File

@ -22,7 +22,7 @@
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -20,7 +20,7 @@
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -20,7 +20,7 @@
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1"
},

View File

@ -37,7 +37,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fflate": "^0.8.2",
"lit": "^3.2.0",

View File

@ -23,7 +23,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fflate": "^0.8.2",
"lit": "^3.2.0",

View File

@ -22,7 +22,7 @@
"@blocksuite/std": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1",
"yjs": "^13.6.21"

View File

@ -20,7 +20,7 @@
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"fflate": "^0.8.2",
"lit": "^3.2.0",

View File

@ -19,7 +19,7 @@
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -16,7 +16,7 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"lit": "^3.2.0",
"rxjs": "^7.8.1"
},

View File

@ -20,7 +20,7 @@
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -22,7 +22,7 @@
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -19,7 +19,7 @@
"@blocksuite/std": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",

View File

@ -28,6 +28,21 @@ import { ShadowlessElement } from './shadowless-element.js';
export const storeContext = createContext<Store>('store');
export const stdContext = createContext<BlockStdScope>('std');
function isMatchFlavour(widgetFlavour: string, block: BlockModel) {
if (widgetFlavour.endsWith('/*')) {
const path = widgetFlavour.slice(0, -2).split('/');
let current: BlockModel | null = block.parent;
for (let i = path.length - 1; i >= 0; i--) {
if (!current || current.flavour !== path[i]) {
return false;
}
current = current.parent;
}
return true;
}
return block.flavour === widgetFlavour;
}
@requiredProperties({
store: PropTypes.instanceOf(Store),
std: PropTypes.object,
@ -61,7 +76,7 @@ export class EditorHost extends SignalWatcher(
const widgets = Array.from(widgetViews.entries()).reduce(
(mapping, [key, tag]) => {
const [widgetFlavour, id] = key.split('|');
if (widgetFlavour === flavour) {
if (isMatchFlavour(widgetFlavour, model)) {
const template = html`<${tag} ${unsafeStatic(WIDGET_ID_ATTR)}=${id}></${tag}>`;
mapping[id] = template;
}

View File

@ -19,7 +19,7 @@
"@lit/context": "^1.1.3",
"@lottiefiles/dotlottie-wc": "^0.5.0",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@vanilla-extract/css": "^1.17.0",
"lit": "^3.2.0",
"rxjs": "^7.8.1",

View File

@ -39,7 +39,7 @@
"@sentry/react": "^9.2.0",
"@tanstack/react-table": "^8.20.5",
"@toeverything/infra": "workspace:*",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"cmdk": "^1.0.4",
"embla-carousel-react": "^8.5.1",
"input-otp": "^1.4.1",

View File

@ -19,7 +19,7 @@
"@emotion/react": "^11.14.0",
"@sentry/react": "^9.2.0",
"@toeverything/infra": "workspace:*",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@vanilla-extract/css": "^1.17.0",
"async-call-rpc": "^6.4.2",
"next-themes": "^0.4.4",

View File

@ -46,7 +46,7 @@
"@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@vanilla-extract/dynamic": "^2.1.2",
"bytes": "^3.1.2",
"check-password-strength": "^3.0.0",

View File

@ -19,6 +19,9 @@
"@affine/templates": "workspace:*",
"@affine/track": "workspace:*",
"@blocksuite/affine": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.13",
"@blocksuite/std": "workspace:*",
"@dotlottie/player-component": "^2.7.12",
@ -39,7 +42,7 @@
"@sentry/react": "^9.2.0",
"@toeverything/infra": "workspace:*",
"@toeverything/pdf-viewer": "^0.1.1",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"@vanilla-extract/dynamic": "^2.1.2",
"@webcontainer/api": "^1.6.1",
"animejs": "^4.0.0",

View File

@ -0,0 +1,109 @@
/**
* @vitest-environment happy-dom
*/
import { affine } from '@blocksuite/affine-shared/test-utils';
import { describe, expect, it } from 'vitest';
import { applyPatchToDoc } from '../../../../blocksuite/ai/utils/apply-model/apply-patch-to-doc';
import type { PatchOp } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff';
describe('applyPatchToDoc', () => {
it('should delete a block', async () => {
const host = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
<affine-paragraph id="paragraph-2">World</affine-paragraph>
</affine-note>
</affine-page>
`;
const patch: PatchOp[] = [{ op: 'delete', id: 'paragraph-1' }];
await applyPatchToDoc(host.store, patch);
const expected = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-2">World</affine-paragraph>
</affine-note>
</affine-page>
`;
expect(host.store).toEqualDoc(expected.store, {
compareId: true,
});
});
it('should replace a block', async () => {
const host = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
<affine-paragraph id="paragraph-2">World</affine-paragraph>
</affine-note>
</affine-page>
`;
const patch: PatchOp[] = [
{
op: 'replace',
id: 'paragraph-1',
content: 'New content',
},
];
await applyPatchToDoc(host.store, patch);
const expected = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-1">New content</affine-paragraph>
<affine-paragraph id="paragraph-2">World</affine-paragraph>
</affine-note>
</affine-page>
`;
expect(host.store).toEqualDoc(expected.store, {
compareId: true,
});
});
it('should insert a block at index', async () => {
const host = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
<affine-paragraph id="paragraph-2">World</affine-paragraph>
</affine-note>
</affine-page>
`;
const patch: PatchOp[] = [
{
op: 'insert',
index: 2,
block: {
id: 'paragraph-3',
type: 'affine:paragraph',
content: 'Inserted',
},
},
];
await applyPatchToDoc(host.store, patch);
const expected = affine`
<affine-page id="page">
<affine-note id="note">
<affine-paragraph id="paragraph-1">Hello</affine-paragraph>
<affine-paragraph id="paragraph-2">World</affine-paragraph>
<affine-paragraph id="paragraph-3">Inserted</affine-paragraph>
</affine-note>
</affine-page>
`;
expect(host.store).toEqualDoc(expected.store, {
compareId: true,
});
});
});

View File

@ -0,0 +1,228 @@
import { describe, expect, test } from 'vitest';
import { diffMarkdown } from '../../../../blocksuite/ai/utils/apply-model/markdown-diff';
describe('diffMarkdown', () => {
test('should diff block insertion', () => {
// Only a new block is inserted
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
This is a new paragraph.
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'insert',
index: 1,
block: {
id: 'block-002',
type: 'paragraph',
content: 'This is a new paragraph.',
},
},
]);
});
test('should diff block deletion', () => {
// A block is deleted
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
This paragraph will be deleted.
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'delete',
id: 'block-002',
},
]);
});
test('should diff block replacement', () => {
// Only content of a block is changed
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Old Title
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# New Title
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'replace',
id: 'block-001',
content: '# New Title',
},
]);
});
test('should diff mixed changes', () => {
// Mixed: delete, insert, replace
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
Old paragraph.
<!-- block_id=block-003 flavour=paragraph -->
To be deleted.
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
Updated paragraph.
<!-- block_id=block-004 flavour=paragraph -->
New paragraph.
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'replace',
id: 'block-002',
content: 'Updated paragraph.',
},
{
op: 'insert',
index: 2,
block: {
id: 'block-004',
type: 'paragraph',
content: 'New paragraph.',
},
},
{
op: 'delete',
id: 'block-003',
},
]);
});
test('should diff consecutive block insertions', () => {
// Two new blocks are inserted consecutively
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
First inserted paragraph.
<!-- block_id=block-003 flavour=paragraph -->
Second inserted paragraph.
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'insert',
index: 1,
block: {
id: 'block-002',
type: 'paragraph',
content: 'First inserted paragraph.',
},
},
{
op: 'insert',
index: 2,
block: {
id: 'block-003',
type: 'paragraph',
content: 'Second inserted paragraph.',
},
},
]);
});
test('should diff consecutive block deletions', () => {
// Two blocks are deleted consecutively
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
First paragraph to be deleted.
<!-- block_id=block-003 flavour=paragraph -->
Second paragraph to be deleted.
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'delete',
id: 'block-002',
},
{
op: 'delete',
id: 'block-003',
},
]);
});
test('should diff deletion followed by insertion at the same position', () => {
// A block is deleted and a new block is inserted at the end
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-002 flavour=paragraph -->
This paragraph will be deleted
<!-- block_id=block-003 flavour=paragraph -->
HelloWorld
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# Title
<!-- block_id=block-003 flavour=paragraph -->
HelloWorld
<!-- block_id=block-004 flavour=paragraph -->
This is a new paragraph inserted after deletion.
`;
const patch = diffMarkdown(oldMd, newMd);
expect(patch).toEqual([
{
op: 'insert',
index: 2,
block: {
id: 'block-004',
type: 'paragraph',
content: 'This is a new paragraph inserted after deletion.',
},
},
{
op: 'delete',
id: 'block-002',
},
]);
});
});

View File

@ -0,0 +1,32 @@
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
} from '@blocksuite/affine-shared/adapters';
export const blockTagMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
flavour: 'affine:page/affine:note/*',
toMatch: () => false,
fromMatch: o => {
const block = o.node;
const parent = o.parent;
if (block.type === 'block' && parent?.node.flavour === 'affine:note') {
return true;
}
return false;
},
toBlockSnapshot: {},
fromBlockSnapshot: {
async enter(block, adapterContext) {
adapterContext.walkerContext
.openNode({
type: 'html',
value: `<!-- block_id=${block.node.id} flavour=${block.node.flavour} -->`,
})
.closeNode();
},
},
};
export const BlockTagMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
blockTagMarkdownAdapterMatcher
);

View File

@ -76,6 +76,21 @@ import {
} from './widgets/ai-panel/components';
import { AIFinishTip } from './widgets/ai-panel/components/finish-tip';
import { GeneratingPlaceholder } from './widgets/ai-panel/components/generating-placeholder';
import {
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
AffineBlockDiffWidgetForBlock,
} from './widgets/block-diff/block';
import { BlockDiffOptions } from './widgets/block-diff/options';
import {
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
AffineBlockDiffWidgetForPage,
} from './widgets/block-diff/page';
import {
AFFINE_BLOCK_DIFF_PLAYGROUND,
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
BlockDiffPlayground,
BlockDiffPlaygroundModal,
} from './widgets/block-diff/playground';
import {
AFFINE_EDGELESS_COPILOT_WIDGET,
EdgelessCopilotWidget,
@ -164,6 +179,12 @@ export function registerAIEffects() {
customElements.define('chat-message-action', ChatMessageAction);
customElements.define('chat-message-assistant', ChatMessageAssistant);
customElements.define('chat-message-user', ChatMessageUser);
customElements.define('ai-block-diff-options', BlockDiffOptions);
customElements.define(AFFINE_BLOCK_DIFF_PLAYGROUND, BlockDiffPlayground);
customElements.define(
AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL,
BlockDiffPlaygroundModal
);
customElements.define('tool-call-card', ToolCallCard);
customElements.define('tool-result-card', ToolResultCard);
@ -172,6 +193,14 @@ export function registerAIEffects() {
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
customElements.define(
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
AffineBlockDiffWidgetForBlock
);
customElements.define(
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
AffineBlockDiffWidgetForPage
);
customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel);
customElements.define(

View File

@ -0,0 +1,533 @@
import { LifeCycleWatcher } from '@blocksuite/affine/std';
import { Extension, type Store } from '@blocksuite/affine/store';
import {
BlockMarkdownAdapterMatcherIdentifier,
MarkdownAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
type Container,
createIdentifier,
type ServiceProvider,
} from '@blocksuite/global/di';
import { LiveData } from '@toeverything/infra';
import type { Subscription } from 'rxjs';
import { blockTagMarkdownAdapterMatcher } from '../adapters/block-tag';
import { applyPatchToDoc } from '../utils/apply-model/apply-patch-to-doc';
import { type Block, diffMarkdown } from '../utils/apply-model/markdown-diff';
interface DiffMap {
// removed blocks
deletes: string[];
// inserted blocks
// key is the start block id, value is the blocks(markdowns) inserted
inserts: Record<number, Block[]>;
// updated blocks
// key is the block id, value is the block(markdown)
updates: Record<string, string>;
}
interface RejectMap {
deletes: string[];
inserts: string[];
updates: string[];
}
type AcceptDelete = {
type: 'delete';
payload: {
id: string;
};
};
type AcceptUpdate = {
type: 'update';
payload: {
id: string;
content: string;
};
};
type AcceptInsert = {
type: 'insert';
payload: {
from: number;
offset: number;
content: string;
};
};
type Accept = AcceptDelete | AcceptUpdate | AcceptInsert;
type RejectDelete = {
type: 'delete';
payload: {
id: string;
};
};
type RejectUpdate = {
type: 'update';
payload: {
id: string;
};
};
type RejectInsert = {
type: 'insert';
payload: {
from: number;
offset: number;
};
};
type Reject = RejectDelete | RejectUpdate | RejectInsert;
export interface BlockDiffProvider {
diffMap$: LiveData<DiffMap>;
rejects$: LiveData<{ rejects: RejectMap; isAllRejected: boolean }>;
isBatchingApply: boolean;
/**
* Set the original markdown
* @param originalMarkdown - The original markdown
*/
setOriginalMarkdown(originalMarkdown: string): void;
/**
* Set the changed markdown
* @param changedMarkdown - The changed markdown
*/
setChangedMarkdown(changedMarkdown: string): void;
/**
* Clear the diff map
*/
clearDiff(): void;
/**
* Get the diff map
*/
getDiff(): DiffMap;
/**
* Check if there is any diff
*/
hasDiff(): boolean;
/**
* Accept all the diffs
*/
acceptAll(doc: Store): Promise<void>;
/**
* Accept a diff
*/
accept(accept: Accept, doc: Store): Promise<void>;
/**
* Reject all the diffs
*/
rejectAll(): void;
/**
* Reject a diff
*/
reject(reject: Reject): void;
/**
* Check if a diff is rejected
*/
isRejected(type: 'delete' | 'update' | 'insert', index: string): boolean;
/**
* Get the total number of diffs
*/
getTotalDiffs(): number;
/**
* Get the markdown from the doc
* @param doc - The doc
* @param provider - The provider
*/
getMarkdownFromDoc(doc: Store, provider: ServiceProvider): Promise<string>;
}
export const BlockDiffProvider = createIdentifier<BlockDiffProvider>(
'AffineBlockDiffService'
);
export class BlockDiffService extends Extension implements BlockDiffProvider {
rejects$ = new LiveData<{ rejects: RejectMap; isAllRejected: boolean }>({
rejects: {
deletes: [],
inserts: [],
updates: [],
},
isAllRejected: false,
});
diffMap$ = new LiveData<DiffMap>({
deletes: [],
inserts: {},
updates: {},
});
private originalMarkdown: string | null = null;
private changedMarkdown: string | null = null;
isBatchingApply = false;
static override setup(di: Container) {
di.addImpl(BlockDiffProvider, BlockDiffService);
}
_removeDelete(removed: string): void {
this.diffMap$.next({
...this.diffMap$.value,
deletes: this.diffMap$.value.deletes.filter(id => id !== removed),
});
}
_removeUpdate(id: string): void {
this.diffMap$.next({
...this.diffMap$.value,
updates: Object.fromEntries(
Object.entries(this.diffMap$.value.updates).filter(
([key]) => key !== id
)
),
});
}
_removeInsert(from: number, offset: number): void {
const inserts = { ...this.diffMap$.value.inserts };
if (inserts[from]) {
inserts[from] = inserts[from].slice();
inserts[from].splice(offset, 1);
if (inserts[from].length === 0) {
delete inserts[from];
}
}
this.diffMap$.next({
...this.diffMap$.value,
inserts,
});
}
hasDiff(): boolean {
const { deletes, updates, inserts } = this.diffMap$.value;
if (
deletes.length > 0 ||
Object.keys(updates).length > 0 ||
Object.keys(inserts).length > 0
) {
return true;
}
return false;
}
setOriginalMarkdown(originalMarkdown: string) {
this.originalMarkdown = originalMarkdown;
this._refreshDiff();
}
setChangedMarkdown(changedMarkdown: string) {
this.changedMarkdown = changedMarkdown;
this.clearRejects();
this._refreshDiff();
}
private _refreshDiff(): void {
const diffMap: DiffMap = {
deletes: [],
inserts: {},
updates: {},
};
if (!this.originalMarkdown || !this.changedMarkdown) {
this.clearDiff();
return;
}
const patches = diffMarkdown(this.originalMarkdown, this.changedMarkdown);
/*
* The logic to group consecutive inserts.
*
* For a continuous sequence of insert patches, we group them into a single entry in `diffMap.inserts`.
* The key of the entry is the starting index of the insertion.
*
* e.g. patches = [
* { op: 'insert', index: 1, block: b1 },
* { op: 'insert', index: 2, block: b2 },
* { op: 'delete', ... },
* { op: 'insert', index: 4, block: b3 }
* ]
*
* lastInsertKey
* |
* v
* patch index condition action inserts map
* -------------------------------------------------------------------------------------------------
* insert b1 1 is new new group, lastInsertKey=1 { 1: [b1] }
* insert b2 2 2 === 1 + 1 append to group 1 { 1: [b1, b2] }
* delete - - reset lastInsertKey=-1 { 1: [b1, b2] }
* insert b3 4 is new new group, lastInsertKey=4 { 1: [b1, b2], 4: [b3] }
*
* The condition `patch.index === lastInsertKey + diffMap.inserts[lastInsertKey].length` checks for continuity.
*/
let lastInsertKey = -1;
for (const patch of patches) {
switch (patch.op) {
case 'delete':
diffMap.deletes.push(patch.id);
lastInsertKey = -1;
break;
case 'insert': {
if (
lastInsertKey !== -1 &&
diffMap.inserts[lastInsertKey] &&
patch.index ===
lastInsertKey + diffMap.inserts[lastInsertKey].length
) {
diffMap.inserts[lastInsertKey].push(patch.block);
} else {
lastInsertKey = patch.index;
diffMap.inserts[lastInsertKey] = [patch.block];
}
break;
}
case 'replace':
diffMap.updates[patch.id] = patch.content;
lastInsertKey = -1;
break;
}
}
this.diffMap$.next(diffMap);
}
getDiff(): DiffMap {
return this.diffMap$.value;
}
clearDiff(): void {
this.diffMap$.next({
deletes: [],
inserts: {},
updates: {},
});
}
clearRejects(): void {
this.rejects$.next({
rejects: {
deletes: [],
inserts: [],
updates: [],
},
isAllRejected: false,
});
}
async acceptAll(doc: Store): Promise<void> {
this.isBatchingApply = true;
const { deletes, updates, inserts } = this.diffMap$.value;
for (const id of deletes) {
await applyPatchToDoc(doc, [{ op: 'delete', id }]);
}
for (const [id, content] of Object.entries(updates)) {
await applyPatchToDoc(doc, [{ op: 'replace', id, content }]);
}
for (const [from, blocks] of Object.entries(inserts)) {
for (const [offset, block] of blocks.entries()) {
await applyPatchToDoc(doc, [
{ op: 'insert', index: Number(from) + offset, block: block },
]);
}
}
this.diffMap$.next({
deletes: [],
inserts: {},
updates: {},
});
this.isBatchingApply = false;
}
async accept(accept: Accept, doc: Store) {
const { type, payload } = accept;
switch (type) {
case 'delete': {
await applyPatchToDoc(doc, [{ op: 'delete', id: payload.id }]);
break;
}
case 'update': {
await applyPatchToDoc(doc, [
{ op: 'replace', id: payload.id, content: payload.content },
]);
break;
}
case 'insert': {
const block = this.diffMap$.value.inserts[payload.from][payload.offset];
await applyPatchToDoc(doc, [
{ op: 'insert', index: payload.from + payload.offset, block },
]);
break;
}
}
}
rejectAll(): void {
this.rejects$.next({
rejects: this.rejects$.value.rejects,
isAllRejected: true,
});
}
reject(reject: Reject): void {
const { rejects, isAllRejected } = this.rejects$.value;
switch (reject.type) {
case 'delete':
this.rejects$.next({
rejects: {
...rejects,
deletes: [...rejects.deletes, reject.payload.id],
},
isAllRejected,
});
break;
case 'update':
this.rejects$.next({
rejects: {
...rejects,
updates: [...rejects.updates, reject.payload.id],
},
isAllRejected,
});
break;
case 'insert':
this.rejects$.next({
rejects: {
...rejects,
inserts: [
...rejects.inserts,
(reject.payload.from + reject.payload.offset).toString(),
],
},
isAllRejected,
});
break;
}
}
isRejected(type: 'delete' | 'update' | 'insert', index: string): boolean {
const { isAllRejected, rejects } = this.rejects$.value;
if (isAllRejected) {
return true;
}
if (type === 'delete') {
return rejects.deletes.includes(index);
}
if (type === 'update') {
return rejects.updates.includes(index);
}
if (type === 'insert') {
return rejects.inserts.includes(index);
}
return false;
}
getTotalDiffs(): number {
const { isAllRejected, rejects } = this.rejects$.value;
if (isAllRejected) {
return 0;
}
const { deletes, updates, inserts } = this.diffMap$.value;
const insertCount = Object.values(inserts).reduce(
(sum, arr) => sum + arr.length,
0
);
const rejectDeleteCount = rejects.deletes.length;
const rejectUpdateCount = rejects.updates.length;
const rejectInsertCount = rejects.inserts.length;
return (
deletes.length +
Object.keys(updates).length +
insertCount -
rejectDeleteCount -
rejectUpdateCount -
rejectInsertCount
);
}
getMarkdownFromDoc = async (doc: Store, provider: ServiceProvider) => {
const job = doc.getTransformer();
const snapshot = job.docToSnapshot(doc);
const adapter = new MarkdownAdapter(job, provider);
if (!snapshot) {
return 'Failed to get markdown from doc';
}
// FIXME: reverse the block matchers to make the block tag adapter the first one
adapter.blockMatchers.reverse();
const markdown = await adapter.fromDocSnapshot({
snapshot,
assets: job.assetsManager,
});
return markdown.file;
};
}
export class BlockDiffWatcher extends LifeCycleWatcher {
static override key = 'block-diff-watcher';
private _blockUpdatedSubscription: Subscription | null = null;
private _provider: ServiceProvider | null = null;
override created() {
super.created();
const cloned = this.std.store.provider.container.clone();
cloned.addImpl(
BlockMarkdownAdapterMatcherIdentifier,
blockTagMarkdownAdapterMatcher
);
this._provider = cloned.provider();
}
private readonly _refreshOriginalMarkdown = async () => {
const diffService = this.std.get(BlockDiffProvider);
if (
!diffService.hasDiff() ||
!this._provider ||
diffService.isBatchingApply
) {
return;
}
const markdown = await diffService.getMarkdownFromDoc(
this.std.store,
this._provider
);
if (markdown) {
diffService.setOriginalMarkdown(markdown);
}
};
override mounted() {
super.mounted();
this._blockUpdatedSubscription =
this.std.store.slots.blockUpdated.subscribe(() => {
this._refreshOriginalMarkdown().catch(err => {
console.error('Failed to refresh original markdown', err);
});
});
}
override unmounted() {
super.unmounted();
this._blockUpdatedSubscription?.unsubscribe();
}
}

View File

@ -0,0 +1,62 @@
import type { Store } from '@blocksuite/store';
import { insertFromMarkdown, replaceFromMarkdown } from '../../../utils';
import type { PatchOp } from './markdown-diff';
/**
* Apply a list of PatchOp to the page doc (children of the first note block)
* @param doc The page document Store
* @param patch Array of PatchOp
*/
export async function applyPatchToDoc(
doc: Store,
patch: PatchOp[]
): Promise<void> {
// Get all note blocks
const notes = doc.getBlocksByFlavour('affine:note');
if (notes.length === 0) return;
// Only handle the first note block
const note = notes[0].model;
// Build a map from block_id to BlockModel for quick lookup
const blockIdMap = new Map<string, any>();
note.children.forEach(child => {
blockIdMap.set(child.id, child);
});
for (const op of patch) {
if (op.op === 'delete') {
// Delete block
doc.deleteBlock(op.id);
} else if (op.op === 'replace') {
// Replace block: delete then insert
const oldBlock = blockIdMap.get(op.id);
if (!oldBlock) continue;
const parentId = note.id;
const index = note.children.findIndex(child => child.id === op.id);
if (index === -1) continue;
// Insert new content
await replaceFromMarkdown(
undefined,
op.content,
doc,
parentId,
index,
op.id
);
} else if (op.op === 'insert') {
// Insert new block
const parentId = note.id;
const index = op.index;
await insertFromMarkdown(
undefined,
op.block.content,
doc,
parentId,
index,
op.block.id
);
}
}
}

View File

@ -0,0 +1,110 @@
export type Block = {
id: string;
type: string;
content: string;
};
export type PatchOp =
| { op: 'replace'; id: string; content: string }
| { op: 'delete'; id: string }
| { op: 'insert'; index: number; block: Block };
export function parseMarkdownToBlocks(markdown: string): Block[] {
const lines = markdown.split(/\r?\n/);
const blocks: Block[] = [];
let currentBlockId: string | null = null;
let currentType: string | null = null;
let currentContent: string[] = [];
for (const line of lines) {
const match = line.match(/^<!--\s*block_id=(.*?)\s+flavour=(.*?)\s*-->/);
if (match) {
// If there is a block being collected, push it into blocks first
if (currentBlockId && currentType) {
blocks.push({
id: currentBlockId,
type: currentType,
content: currentContent.join('\n').trim(),
});
}
// Start a new block
currentBlockId = match[1];
currentType = match[2];
currentContent = [];
} else {
// Collect content
if (currentBlockId && currentType) {
currentContent.push(line);
}
}
}
// Collect the last block
if (currentBlockId && currentType) {
blocks.push({
id: currentBlockId,
type: currentType,
content: currentContent.join('\n').trim(),
});
}
return blocks;
}
function diffBlockLists(oldBlocks: Block[], newBlocks: Block[]): PatchOp[] {
const patch: PatchOp[] = [];
const oldMap = new Map<string, { block: Block; index: number }>();
oldBlocks.forEach((b, i) => oldMap.set(b.id, { block: b, index: i }));
const newMap = new Map<string, { block: Block; index: number }>();
newBlocks.forEach((b, i) => newMap.set(b.id, { block: b, index: i }));
// Mark old blocks that have been handled
const handledOld = new Set<string>();
// First process newBlocks in order
newBlocks.forEach((newBlock, newIdx) => {
const old = oldMap.get(newBlock.id);
if (old) {
handledOld.add(newBlock.id);
if (old.block.content !== newBlock.content) {
patch.push({
op: 'replace',
id: newBlock.id,
content: newBlock.content,
});
}
} else {
patch.push({
op: 'insert',
index: newIdx,
block: {
id: newBlock.id,
type: newBlock.type,
content: newBlock.content,
},
});
}
});
// Then process deleted oldBlocks
oldBlocks.forEach(oldBlock => {
if (!newMap.has(oldBlock.id)) {
patch.push({
op: 'delete',
id: oldBlock.id,
});
}
});
return patch;
}
export function diffMarkdown(
oldMarkdown: string,
newMarkdown: string
): PatchOp[] {
const oldBlocks = parseMarkdownToBlocks(oldMarkdown);
const newBlocks = parseMarkdownToBlocks(newMarkdown);
const patch: PatchOp[] = diffBlockLists(oldBlocks, newBlocks);
return patch;
}

View File

@ -0,0 +1,231 @@
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css, html } from 'lit';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { BlockDiffProvider } from '../../services/block-diff';
import type { Block } from '../../utils/apply-model/markdown-diff';
import { blockDiffWidgetForPage } from './page';
export const AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK =
'affine-block-diff-widget-for-block';
export class AffineBlockDiffWidgetForBlock extends WidgetComponent {
static override styles = css`
.ai-block-diff {
position: relative;
margin-top: 8px;
margin-bottom: 8px;
pointer-events: none;
background-color: ${unsafeCSSVarV2('aI/applyTextHighlightBackground')};
padding: 8px 0px;
border-radius: 4px;
}
.ai-block-diff.delete {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
}
.ai-block-diff.delete .mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${unsafeCSSVarV2('aI/applyDeleteHighlight')};
z-index: -1;
margin-top: -10px;
border-radius: 4px;
}
.ai-block-diff.delete ai-block-diff-options {
top: -10px;
}
`;
private _renderDelete(blockId: string) {
if (this.diffService.isRejected('delete', blockId)) {
return null;
}
const mask = html`<div
class="ai-block-diff delete"
data-testid="ai-block-diff-delete"
data-diff-id=${`delete-${blockId}`}
>
<div class="mask"></div>
<ai-block-diff-options
.onAccept=${() =>
this.diffService.accept(
{ type: 'delete', payload: { id: blockId } },
this.std.store
)}
.onReject=${() =>
this.diffService.reject({ type: 'delete', payload: { id: blockId } })}
></ai-block-diff-options>
</div>`;
return mask;
}
private _renderInsert(from: number, blocks: Block[]) {
return blocks.map((block, offset) => {
return this.diffService.isRejected('insert', `${from + offset}`)
? null
: html`<div
class="ai-block-diff insert"
data-diff-id=${`insert-${block.id}-${offset}`}
>
<chat-content-rich-text
.text=${block.content}
.host=${this.host}
.state="finished"
.extensions=${this.userExtensions}
></chat-content-rich-text>
<ai-block-diff-options
.onAccept=${() =>
this.diffService.accept(
{
type: 'insert',
payload: { from, offset, content: block.content },
},
this.std.store
)}
.onReject=${() =>
this.diffService.reject({
type: 'insert',
payload: { from, offset },
})}
></ai-block-diff-options>
</div>`;
});
}
private _renderUpdate(blockId: string, content: string) {
if (this.diffService.isRejected('update', blockId)) {
return null;
}
return html`
<div class="ai-block-diff update" data-diff-id=${`update-${blockId}`}>
<chat-content-rich-text
.text=${content}
.host=${this.host}
.state="finished"
.extensions=${this.userExtensions}
></chat-content-rich-text>
<ai-block-diff-options
.onAccept=${() =>
this.diffService.accept(
{
type: 'update',
payload: { id: blockId, content },
},
this.std.store
)}
.onReject=${() =>
this.diffService.reject({
type: 'update',
payload: { id: blockId },
})}
></ai-block-diff-options>
</div>
`;
}
get diffService() {
return this.std.get(BlockDiffProvider);
}
get userExtensions() {
return this.std.userExtensions.filter(
extension => extension !== blockDiffWidgetForPage
);
}
get blockIndex(): number | null {
const attached = this.block?.blockId;
if (!attached) {
return null;
}
const block = this.std.store.getBlock(attached);
if (!block) {
return null;
}
const doc = this.std.store;
const pageRoot = doc.root;
const note = pageRoot?.children.find(
child => child.flavour === 'affine:note'
);
if (!note) {
return null;
}
return note.children.findIndex(child => child.id === block.id);
}
override render() {
const attached = this.block?.blockId;
const service = this.std.get(BlockDiffProvider);
if (!attached) {
return null;
}
const blockIndex = this.blockIndex;
if (blockIndex === null) {
return null;
}
if (!service.hasDiff()) {
return null;
}
const { deletes, inserts, updates } = service.getDiff();
if (deletes.includes(attached)) {
return this._renderDelete(attached);
}
if (updates[attached]) {
return this._renderUpdate(attached, updates[attached]);
}
if (inserts[blockIndex + 1]) {
const from = blockIndex + 1;
const blocks = inserts[from];
return this._renderInsert(from, blocks);
}
return null;
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(
this.diffService.diffMap$.subscribe(() => {
this.requestUpdate();
})
);
this.disposables.add(
this.diffService.rejects$.subscribe(() => {
this.requestUpdate();
})
);
}
}
export const blockDiffWidgetForBlock = WidgetViewExtension(
'affine:note/*',
AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK,
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_WIDGET_FOR_BLOCK)}`
);

View File

@ -0,0 +1,84 @@
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
import { CloseIcon, DoneIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import type { PatchOp } from '../../utils/apply-model/markdown-diff';
export class BlockDiffOptions extends WithDisposable(LitElement) {
static override styles = css`
:host {
position: absolute;
right: -20px;
top: 0;
display: flex;
flex-direction: column;
gap: 4px;
cursor: pointer;
pointer-events: auto;
}
.ai-block-diff-option {
padding: 2px;
border-radius: 4px;
box-shadow: ${unsafeCSSVar('shadow1')};
display: flex;
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
align-items: center;
justify-content: center;
border-radius: 4px;
}
.ai-block-diff-option.accept {
color: ${unsafeCSSVarV2('icon/activated')};
}
.ai-block-diff-option.reject {
color: ${unsafeCSSVarV2('icon/secondary')};
}
`;
@property({ attribute: false })
accessor onAccept!: (op: PatchOp) => void;
@property({ attribute: false })
accessor op!: PatchOp;
@property({ attribute: false })
accessor onReject!: (op: PatchOp) => void;
private readonly _handleAcceptClick = () => {
console.log('accept', this.op);
this.onAccept(this.op);
};
private readonly _handleRejectClick = () => {
console.log('reject', this.op);
this.onReject(this.op);
};
override render() {
return html`
<div
class="ai-block-diff-option accept"
@click=${this._handleAcceptClick}
>
${DoneIcon()}
</div>
<div
class="ai-block-diff-option reject"
@click=${this._handleRejectClick}
>
${CloseIcon()}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'ai-block-diff-options': BlockDiffOptions;
}
}

View File

@ -0,0 +1,152 @@
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
ArrowDownSmallIcon,
ArrowUpSmallIcon,
CloseIcon,
DoneIcon,
} from '@blocksuite/icons/lit';
import { css, html } from 'lit';
import { property } from 'lit/decorators.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { BlockDiffProvider } from '../../services/block-diff';
export const AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE =
'affine-block-diff-widget-for-page';
export class AffineBlockDiffWidgetForPage extends WidgetComponent {
static override styles = css`
.ai-block-diff-scroller-container {
margin: auto;
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
box-shadow: ${unsafeCSSVar('shadow1')};
border-radius: 8px;
width: 350px;
padding: 8px 4px;
cursor: pointer;
}
.ai-block-diff-scroller {
display: flex;
align-items: center;
gap: 4px;
}
.ai-block-diff-scroller span {
display: inline-flex;
}
.ai-block-diff-scroller svg {
color: ${unsafeCSSVarV2('icon/primary')};
}
.ai-block-diff-all-option {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 4px 8px;
}
`;
@property({ type: Number })
accessor currentIndex = 0;
_handleScroll(dir: 'prev' | 'next') {
const total = this.diffService.getTotalDiffs();
const diffWidgets = Array.from(
this.std.host.querySelectorAll('affine-block-diff-widget-for-block')
);
const diffs = diffWidgets.reduce<Element[]>((acc, widget) => {
const aiDiffs = widget.shadowRoot?.querySelectorAll('.ai-block-diff');
if (aiDiffs && aiDiffs.length > 0) {
acc.push(...aiDiffs);
}
return acc;
}, []);
if (dir === 'prev') {
this.currentIndex = Math.max(0, this.currentIndex - 1);
} else {
this.currentIndex = Math.min(total - 1, this.currentIndex + 1);
}
diffs[this.currentIndex].scrollIntoView({ behavior: 'smooth' });
}
get diffService() {
return this.std.get(BlockDiffProvider);
}
override render() {
if (!this.diffService.hasDiff()) {
return null;
}
const total = this.diffService.getTotalDiffs();
return total === 0
? null
: html`
<div class="ai-block-diff-scroller-container">
<div class="ai-block-diff-scroller">
<span @click=${() => this._handleScroll('next')}
>${ArrowDownSmallIcon()}</span
>
<span class="ai-block-diff-scroller-current"
>${Math.min(this.currentIndex + 1, total)}</span
>
<span>/</span>
<span class="ai-block-diff-scroller-total">${total}</span>
<span @click=${() => this._handleScroll('prev')}
>${ArrowUpSmallIcon()}</span
>
</div>
<div
class="ai-block-diff-all-option"
@click=${() => this.diffService.rejectAll()}
>
${CloseIcon({
style: `color: ${unsafeCSSVarV2('icon/secondary')}`,
})}
Reject all
</div>
<div
class="ai-block-diff-all-option"
@click=${() => this.diffService.acceptAll(this.std.store)}
>
${DoneIcon({
style: `color: ${unsafeCSSVarV2('icon/activated')}`,
})}
Accept all
</div>
</div>
`;
}
override connectedCallback() {
super.connectedCallback();
this.disposables.add(
this.diffService.diffMap$.subscribe(() => {
this.requestUpdate();
})
);
this.disposables.add(
this.diffService.rejects$.subscribe(() => {
this.requestUpdate();
})
);
}
}
export const blockDiffWidgetForPage = WidgetViewExtension(
'affine:page',
AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE,
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_WIDGET_FOR_PAGE)}`
);

View File

@ -0,0 +1,252 @@
import { WithDisposable } from '@blocksuite/affine/global/lit';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/affine/std';
import type { Store } from '@blocksuite/affine/store';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { BlockMarkdownAdapterMatcherIdentifier } from '@blocksuite/affine-shared/adapters';
import { css, html, LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { blockTagMarkdownAdapterMatcher } from '../../adapters/block-tag';
import { BlockDiffProvider } from '../../services/block-diff';
export const AFFINE_BLOCK_DIFF_PLAYGROUND = 'affine-block-diff-playground';
export const AFFINE_BLOCK_DIFF_PLAYGROUND_MODAL =
'affine-block-diff-playground-modal';
export class BlockDiffPlaygroundModal extends WithDisposable(LitElement) {
static override styles = css`
.playground-modal {
z-index: 10000;
width: 600px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18);
padding: 24px 20px 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.playground-modal {
z-index: 10000;
width: 600px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18);
padding: 24px 20px 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.playground-textarea {
width: 100%;
min-height: 300px;
resize: vertical;
font-size: 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 8px;
outline: none;
font-family: inherit;
box-sizing: border-box;
}
.playground-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
.playground-btn {
padding: 6px 18px;
border: none;
border-radius: 4px;
font-size: 15px;
cursor: pointer;
background: #f5f5f5;
color: #333;
transition: background 0.2s;
}
.playground-btn.primary {
background: #1976d2;
color: #fff;
}
.playground-btn.primary:hover {
background: #1565c0;
}
.playground-btn:hover {
background: #e0e0e0;
}
`;
@state()
private accessor markdown = '';
@property({ attribute: false })
accessor diffService!: BlockDiffProvider;
@property({ attribute: false })
accessor store!: Store;
@property({ attribute: false })
accessor onClose!: () => void;
private readonly handleInput = (e: Event) => {
this.markdown = (e.target as HTMLTextAreaElement).value;
};
private readonly handleClear = () => {
this.markdown = '';
this.diffService.setChangedMarkdown('');
};
private async getOriginalMarkdown() {
const cloned = this.store.provider.container.clone();
cloned.addImpl(
BlockMarkdownAdapterMatcherIdentifier,
blockTagMarkdownAdapterMatcher
);
const provider = cloned.provider();
const markdown = await this.diffService.getMarkdownFromDoc(
this.store,
provider
);
return markdown;
}
private readonly handleConfirm = async () => {
const originalMarkdown = await this.getOriginalMarkdown();
this.diffService.setOriginalMarkdown(originalMarkdown);
this.diffService.setChangedMarkdown(this.markdown);
this.onClose();
};
private readonly handleInsertCurrentMarkdown = async () => {
this.markdown = await this.getOriginalMarkdown();
};
private readonly stopPropagation = (e: MouseEvent) => {
e.stopPropagation();
};
override render() {
return html`
<div class="playground-modal">
<div class="playground-modal-title">Block Diff Playground</div>
<div class="playground-modal-content">
<textarea
class="playground-textarea"
placeholder="输入 markdown..."
.value=${this.markdown}
@input=${this.handleInput}
@focus=${(e: FocusEvent) => e.stopPropagation()}
@pointerdown=${this.stopPropagation}
@mousedown=${this.stopPropagation}
@mouseup=${this.stopPropagation}
@click=${this.stopPropagation}
@keydown=${this.stopPropagation}
@keyup=${this.stopPropagation}
@copy=${this.stopPropagation}
@paste=${this.stopPropagation}
@blur=${(e: FocusEvent) => e.stopPropagation()}
></textarea>
<div class="playground-actions">
<button
class="playground-btn"
@click=${this.handleInsertCurrentMarkdown}
>
Insert Current Doc MD
</button>
<button class="playground-btn" @click=${this.handleClear}>
Clear
</button>
<button class="playground-btn primary" @click=${this.handleConfirm}>
Confirm
</button>
</div>
</div>
</div>
`;
}
}
export class BlockDiffPlayground extends WidgetComponent {
static override styles = css`
.playground-fab {
position: fixed;
right: 32px;
bottom: 32px;
z-index: 9999;
width: 56px;
height: 56px;
border-radius: 50%;
background: #1976d2;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: background 0.2s;
}
.playground-fab:hover {
background: #1565c0;
}
`;
@query('.playground-fab')
accessor fab!: HTMLDivElement;
private _abortController: AbortController | null = null;
private get diffService() {
return this.std.get(BlockDiffProvider);
}
private readonly handleOpen = () => {
this._abortController?.abort();
this._abortController = new AbortController();
createLitPortal({
template: html`
<affine-block-diff-playground-modal
.diffService=${this.diffService}
.store=${this.std.store}
.onClose=${this.handleClose}
></affine-block-diff-playground-modal>
`,
container: this.host,
computePosition: {
referenceElement: this.fab,
placement: 'top-end',
},
closeOnClickAway: true,
abortController: this._abortController,
});
};
private readonly handleClose = () => {
this._abortController?.abort();
};
override render() {
return html`
<div>
<div
class="playground-fab"
@click=${this.handleOpen}
title="Block Diff Playground"
>
🧪
</div>
</div>
`;
}
}
export const blockDiffPlayground = WidgetViewExtension(
'affine:page',
AFFINE_BLOCK_DIFF_PLAYGROUND,
literal`${unsafeStatic(AFFINE_BLOCK_DIFF_PLAYGROUND)}`
);

View File

@ -131,7 +131,8 @@ export async function insertFromMarkdown(
markdown: string,
doc: Store,
parent?: string,
index?: number
index?: number,
id?: string
) {
const { snapshot, transformer } = await markdownToSnapshot(
markdown,
@ -144,6 +145,9 @@ export async function insertFromMarkdown(
const models: BlockModel[] = [];
for (let i = 0; i < snapshots.length; i++) {
const blockSnapshot = snapshots[i];
if (snapshots.length === 1 && id) {
blockSnapshot.id = id;
}
const model = await transformer.snapshotToBlock(
blockSnapshot,
doc,
@ -158,6 +162,34 @@ export async function insertFromMarkdown(
return models;
}
export async function replaceFromMarkdown(
host: EditorHost | undefined,
markdown: string,
doc: Store,
parent: string,
index: number,
id: string
) {
doc.deleteBlock(id);
const { snapshot, transformer } = await markdownToSnapshot(
markdown,
doc,
host
);
const snapshots = snapshot?.content.flatMap(x => x.children) ?? [];
const blockSnapshot = snapshots[0];
blockSnapshot.id = id;
const model = await transformer.snapshotToBlock(
blockSnapshot,
doc,
parent,
index
);
return model;
}
export async function markDownToDoc(
provider: ServiceProvider,
schema: Schema,

View File

@ -19,6 +19,13 @@ import { BlockFlavourIdentifier } from '@blocksuite/affine/std';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
import {
BlockDiffService,
BlockDiffWatcher,
} from '../../ai/services/block-diff';
import { blockDiffWidgetForBlock } from '../../ai/widgets/block-diff/block';
import { blockDiffWidgetForPage } from '../../ai/widgets/block-diff/page';
import { blockDiffPlayground } from '../../ai/widgets/block-diff/playground';
import { EdgelessClipboardAIChatConfig } from './edgeless-clipboard';
const optionsSchema = z.object({
@ -50,6 +57,7 @@ export class AIViewExtension extends ViewExtensionProvider<AIViewOptions> {
config: imageToolbarAIEntryConfig(),
})
);
if (context.scope === 'edgeless' || context.scope === 'page') {
context.register([
aiPanelWidget,
@ -73,7 +81,17 @@ export class AIViewExtension extends ViewExtensionProvider<AIViewOptions> {
]);
}
if (context.scope === 'page') {
context.register(getAIPageRootWatcher(framework));
context.register([
blockDiffWidgetForPage,
blockDiffWidgetForBlock,
getAIPageRootWatcher(framework),
BlockDiffService,
BlockDiffWatcher,
]);
if (process.env.NODE_ENV === 'development') {
context.register([blockDiffPlayground]);
}
}
}
}

View File

@ -9,7 +9,7 @@
"@blocksuite/affine": "workspace:*",
"@blocksuite/integration-test": "workspace:*",
"@playwright/test": "=1.52.0",
"@toeverything/theme": "^1.1.15",
"@toeverything/theme": "^1.1.16",
"json-stable-stringify": "^1.2.1",
"rxjs": "^7.8.1"
},

147
yarn.lock
View File

@ -87,7 +87,7 @@ __metadata:
"@blocksuite/affine": "workspace:*"
"@blocksuite/integration-test": "workspace:*"
"@playwright/test": "npm:=1.52.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
json-stable-stringify: "npm:^1.2.1"
rxjs: "npm:^7.8.1"
languageName: unknown
@ -215,7 +215,7 @@ __metadata:
"@sentry/react": "npm:^9.2.0"
"@tanstack/react-table": "npm:^8.20.5"
"@toeverything/infra": "workspace:*"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
class-variance-authority: "npm:^0.7.1"
clsx: "npm:^2.1.1"
@ -336,7 +336,7 @@ __metadata:
"@storybook/react-vite": "npm:^9.0.0"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/react": "npm:^16.1.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/bytes": "npm:^3.1.5"
"@types/react": "npm:^19.0.1"
"@types/react-dom": "npm:^19.0.2"
@ -402,6 +402,9 @@ __metadata:
"@affine/templates": "workspace:*"
"@affine/track": "workspace:*"
"@blocksuite/affine": "workspace:*"
"@blocksuite/affine-components": "workspace:*"
"@blocksuite/affine-shared": "workspace:*"
"@blocksuite/global": "workspace:*"
"@blocksuite/icons": "npm:^2.2.13"
"@blocksuite/std": "workspace:*"
"@dotlottie/player-component": "npm:^2.7.12"
@ -424,7 +427,7 @@ __metadata:
"@testing-library/react": "npm:^16.1.0"
"@toeverything/infra": "workspace:*"
"@toeverything/pdf-viewer": "npm:^0.1.1"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/animejs": "npm:^3.1.12"
"@types/bytes": "npm:^3.1.5"
"@types/image-blob-reduce": "npm:^4.1.4"
@ -526,7 +529,7 @@ __metadata:
"@emotion/react": "npm:^11.14.0"
"@sentry/react": "npm:^9.2.0"
"@toeverything/infra": "workspace:*"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/react": "npm:^19.0.1"
"@types/react-dom": "npm:^19.0.2"
"@vanilla-extract/css": "npm:^1.17.0"
@ -2428,7 +2431,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
file-type: "npm:^21.0.0"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2455,7 +2458,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
rxjs: "npm:^7.8.1"
@ -2484,7 +2487,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.10"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
emoji-mart: "npm:^5.6.0"
lit: "npm:^3.2.0"
@ -2515,7 +2518,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2543,7 +2546,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2574,7 +2577,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
date-fns: "npm:^4.0.0"
lit: "npm:^3.2.0"
@ -2600,7 +2603,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2629,7 +2632,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
rxjs: "npm:^7.8.1"
@ -2657,7 +2660,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -2689,7 +2692,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -2720,7 +2723,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2749,7 +2752,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
file-type: "npm:^21.0.0"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2777,7 +2780,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/katex": "npm:^0.16.7"
"@types/mdast": "npm:^4.0.4"
katex: "npm:^0.16.11"
@ -2807,7 +2810,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2838,7 +2841,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
"@types/mdast": "npm:^4.0.4"
"@vanilla-extract/css": "npm:^1.17.0"
@ -2867,7 +2870,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/mdast": "npm:^4.0.4"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -2914,7 +2917,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
dompurify: "npm:^3.2.4"
html2canvas: "npm:^1.4.1"
@ -2946,7 +2949,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fractional-indexing: "npm:^3.2.0"
lit: "npm:^3.2.0"
@ -2971,7 +2974,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fractional-indexing: "npm:^3.2.0"
html2canvas: "npm:^1.4.1"
@ -3028,7 +3031,7 @@ __metadata:
"@lit/context": "npm:^1.1.2"
"@lottiefiles/dotlottie-wc": "npm:^0.5.0"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/hast": "npm:^3.0.4"
"@types/katex": "npm:^0.16.7"
"@types/lodash-es": "npm:^4.17.12"
@ -3074,7 +3077,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3099,7 +3102,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
languageName: unknown
@ -3123,7 +3126,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
rxjs: "npm:^7.8.1"
@ -3149,7 +3152,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3177,7 +3180,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@vanilla-extract/css": "npm:^1.17.0"
lit: "npm:^3.2.0"
minimatch: "npm:^10.0.1"
@ -3203,7 +3206,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3232,7 +3235,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3261,7 +3264,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3292,7 +3295,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3327,7 +3330,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3359,7 +3362,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3387,7 +3390,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3416,7 +3419,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3446,7 +3449,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3474,7 +3477,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3512,7 +3515,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
collapse-white-space: "npm:^2.1.0"
date-fns: "npm:^4.0.0"
@ -3543,7 +3546,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/hast": "npm:^3.0.4"
"@types/katex": "npm:^0.16.7"
"@types/lodash-es": "npm:^4.17.12"
@ -3577,7 +3580,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
collapse-white-space: "npm:^2.1.0"
date-fns: "npm:^4.0.0"
@ -3605,7 +3608,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
collapse-white-space: "npm:^2.1.0"
date-fns: "npm:^4.0.0"
@ -3639,7 +3642,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/hast": "npm:^3.0.4"
"@types/katex": "npm:^0.16.7"
"@types/lodash-es": "npm:^4.17.12"
@ -3673,7 +3676,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
collapse-white-space: "npm:^2.1.0"
date-fns: "npm:^4.0.0"
@ -3693,7 +3696,7 @@ __metadata:
"@blocksuite/global": "workspace:*"
"@blocksuite/std": "workspace:*"
"@blocksuite/store": "workspace:*"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fractional-indexing: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3717,7 +3720,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
collapse-white-space: "npm:^2.1.0"
date-fns: "npm:^4.0.0"
@ -3734,6 +3737,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@blocksuite/affine-shared@workspace:blocksuite/affine/shared"
dependencies:
"@blocksuite/affine": "workspace:*"
"@blocksuite/affine-ext-loader": "workspace:*"
"@blocksuite/affine-model": "workspace:*"
"@blocksuite/global": "workspace:*"
"@blocksuite/icons": "npm:^2.2.12"
@ -3742,7 +3747,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/bytes": "npm:^3.1.5"
"@types/hast": "npm:^3.0.4"
"@types/lodash-es": "npm:^4.17.12"
@ -3801,7 +3806,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3825,7 +3830,7 @@ __metadata:
"@blocksuite/icons": "npm:^2.2.12"
"@blocksuite/std": "workspace:*"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
languageName: unknown
@ -3846,7 +3851,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
yjs: "npm:^13.6.21"
@ -3872,7 +3877,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
yjs: "npm:^13.6.21"
@ -3895,7 +3900,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3917,7 +3922,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@floating-ui/dom": "npm:^1.6.13"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -3939,7 +3944,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
languageName: unknown
@ -3976,7 +3981,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fflate: "npm:^0.8.2"
lit: "npm:^3.2.0"
@ -4002,7 +4007,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fflate: "npm:^0.8.2"
lit: "npm:^3.2.0"
@ -4028,7 +4033,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
yjs: "npm:^13.6.21"
@ -4049,7 +4054,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
fflate: "npm:^0.8.2"
lit: "npm:^3.2.0"
@ -4071,7 +4076,7 @@ __metadata:
"@blocksuite/icons": "npm:^2.2.12"
"@blocksuite/std": "workspace:*"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -4089,7 +4094,7 @@ __metadata:
"@blocksuite/global": "workspace:*"
"@blocksuite/std": "workspace:*"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
lit: "npm:^3.2.0"
rxjs: "npm:^7.8.1"
languageName: unknown
@ -4109,7 +4114,7 @@ __metadata:
"@blocksuite/store": "workspace:*"
"@floating-ui/dom": "npm:^1.6.13"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -4133,7 +4138,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@floating-ui/dom": "npm:^1.6.13"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -4154,7 +4159,7 @@ __metadata:
"@blocksuite/std": "workspace:*"
"@floating-ui/dom": "npm:^1.6.13"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
lit: "npm:^3.2.0"
lodash-es: "npm:^4.17.21"
@ -4266,7 +4271,7 @@ __metadata:
"@floating-ui/dom": "npm:^1.6.13"
"@lit/context": "npm:^1.1.2"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@types/lodash-es": "npm:^4.17.12"
clsx: "npm:^2.1.1"
date-fns: "npm:^4.0.0"
@ -4319,7 +4324,7 @@ __metadata:
"@lit/context": "npm:^1.1.3"
"@lottiefiles/dotlottie-wc": "npm:^0.5.0"
"@preact/signals-core": "npm:^1.8.0"
"@toeverything/theme": "npm:^1.1.15"
"@toeverything/theme": "npm:^1.1.16"
"@vanilla-extract/css": "npm:^1.17.0"
"@vanilla-extract/vite-plugin": "npm:^5.0.0"
lit: "npm:^3.2.0"
@ -14344,10 +14349,10 @@ __metadata:
languageName: node
linkType: hard
"@toeverything/theme@npm:^1.1.15":
version: 1.1.15
resolution: "@toeverything/theme@npm:1.1.15"
checksum: 10/a77c73c255856b94c2df26b431b0b6901b15f18da2ea24d78b0a88d289a9b20e05b96e14e509322492c5e37b890df4d6f94ef11e63cce5465b3ac757b6f3b519
"@toeverything/theme@npm:^1.1.16":
version: 1.1.16
resolution: "@toeverything/theme@npm:1.1.16"
checksum: 10/6fc139f5b31d1d4051ae9897ec79f4fa50cdc81b865d188e441b812e211e7e1699b78e0e6f0a15ac268a5ebdad8c319c0db5a10f2cfa1dea8673eebb06b1bc67
languageName: node
linkType: hard