mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
39 Commits
v0.22.2-be
...
06-18-feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc69a3bef | ||
|
|
e8d774a2ad | ||
|
|
a1abb60dec | ||
|
|
04f3d88e2c | ||
|
|
e98f035f97 | ||
|
|
1d4bc81e90 | ||
|
|
deeea3428e | ||
|
|
8b0dd3c067 | ||
|
|
8ca17864f1 | ||
|
|
d2664480f7 | ||
|
|
b986a39da3 | ||
|
|
097a63362c | ||
|
|
7284320355 | ||
|
|
b4401a8abf | ||
|
|
0351fbcb86 | ||
|
|
6eed9c686b | ||
|
|
8d2214424c | ||
|
|
d12954f8c3 | ||
|
|
83733cd828 | ||
|
|
ed56f076ed | ||
|
|
2d17c265ca | ||
|
|
2a9f7e1835 | ||
|
|
a71904e641 | ||
|
|
814364489f | ||
|
|
24448659a4 | ||
|
|
c846c57a12 | ||
|
|
e82c9d2ddc | ||
|
|
3c29f62224 | ||
|
|
b5ef361f87 | ||
|
|
4fa85416ae | ||
|
|
f69a98eb8c | ||
|
|
115496aa8e | ||
|
|
7aafbf12a5 | ||
|
|
0f9b7d4a0d | ||
|
|
2817b5aec4 | ||
|
|
72e66aca11 | ||
|
|
7d1f2adb7f | ||
|
|
512a908fd4 | ||
|
|
71be1d424a |
@@ -95,11 +95,13 @@ spec:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
||||
1
.github/helm/affine/charts/doc/values.yaml
vendored
1
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -36,6 +36,7 @@ resources:
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
timeoutSeconds: 5
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
1
.github/workflows/build-test.yml
vendored
1
.github/workflows/build-test.yml
vendored
@@ -827,7 +827,6 @@ jobs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
formatSize,
|
||||
openSingleFileWith,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
AttachmentIcon,
|
||||
ResetIcon,
|
||||
@@ -31,7 +34,7 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { nanoid, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { batch, computed, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
@@ -42,7 +45,7 @@ import { filter } from 'rxjs/operators';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
import { downloadAttachmentBlob, getFileType, refreshData } from './utils';
|
||||
|
||||
type AttachmentResolvedStateInfo = ResolvedStateInfo & {
|
||||
kind?: TemplateResult;
|
||||
@@ -129,12 +132,50 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
// Refreshes the embed component.
|
||||
reload = () => {
|
||||
if (this.model.props.embed) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
batch(() => {
|
||||
if (this.model.props.embed$.value) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
this.refreshData();
|
||||
});
|
||||
};
|
||||
|
||||
// Replaces the current attachment.
|
||||
replace = async () => {
|
||||
const state = this.resourceController.state$.peek();
|
||||
if (state.uploading) return;
|
||||
|
||||
const file = await openSingleFileWith();
|
||||
if (!file) return;
|
||||
|
||||
const sourceId = await this.std.store.blobSync.set(file);
|
||||
const type = await getFileType(file);
|
||||
const { name, size } = file;
|
||||
|
||||
let embed = this.model.props.embed$.value ?? false;
|
||||
|
||||
this.std.store.captureSync();
|
||||
this.std.store.transact(() => {
|
||||
this.std.store.updateBlock(this.blockId, {
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
sourceId,
|
||||
embed: false,
|
||||
});
|
||||
|
||||
const provider = this.std.get(AttachmentEmbedProvider);
|
||||
embed &&= provider.embedded(this.model);
|
||||
|
||||
if (embed) {
|
||||
provider.convertTo(this.model);
|
||||
}
|
||||
|
||||
// Reloads
|
||||
this.reload();
|
||||
});
|
||||
};
|
||||
|
||||
private _selectBlock() {
|
||||
@@ -403,7 +444,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
protected renderEmbedView = () => {
|
||||
const { model, blobUrl } = this;
|
||||
if (!model.props.embed || !blobUrl) return null;
|
||||
if (!model.props.embed$.value || !blobUrl) return null;
|
||||
|
||||
const { std, _maxFileSize } = this;
|
||||
const provider = std.get(AttachmentEmbedProvider);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
EditIcon,
|
||||
ReplaceIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
@@ -139,27 +140,42 @@ export const attachmentViewDropdownMenu = {
|
||||
});
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
return html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`;
|
||||
},
|
||||
} as const satisfies ToolbarActionGroup<ToolbarAction>;
|
||||
|
||||
const replaceAction = {
|
||||
id: 'c.replace',
|
||||
tooltip: 'Replace attachment',
|
||||
icon: ReplaceIcon(),
|
||||
disabled(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return true;
|
||||
|
||||
const { downloading = false, uploading = false } =
|
||||
block.resourceController.state$.value;
|
||||
return downloading || uploading;
|
||||
},
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.replace().catch(console.error);
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const downloadAction = {
|
||||
id: 'c.download',
|
||||
id: 'd.download',
|
||||
tooltip: 'Download',
|
||||
icon: DownloadIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.download();
|
||||
},
|
||||
when: ctx => {
|
||||
when(ctx) {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return false;
|
||||
// Current citation attachment block does not support download
|
||||
@@ -168,7 +184,7 @@ const downloadAction = {
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
id: 'd.caption',
|
||||
id: 'e.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
@@ -221,6 +237,7 @@ const builtinToolbarConfig = {
|
||||
},
|
||||
},
|
||||
attachmentViewDropdownMenu,
|
||||
replaceAction,
|
||||
downloadAction,
|
||||
captionAction,
|
||||
{
|
||||
@@ -354,13 +371,17 @@ const builtinSurfaceToolbarConfig = {
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
...replaceAction,
|
||||
id: 'd.replace',
|
||||
},
|
||||
{
|
||||
...downloadAction,
|
||||
id: 'd.download',
|
||||
id: 'e.download',
|
||||
},
|
||||
{
|
||||
...captionAction,
|
||||
id: 'e.caption',
|
||||
id: 'f.caption',
|
||||
},
|
||||
],
|
||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||
|
||||
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
type CodeBlockModel,
|
||||
CodeBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const CodeBlockMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code-block',
|
||||
pattern: /^```([a-zA-Z0-9]*)\s$/,
|
||||
action: ({ inlineEditor, inlineRange, prefixText, pattern }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const language = match[1];
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type === 'quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
const codeId = store.addBlock<CodeBlockModel>(
|
||||
CodeBlockSchema.model.flavour,
|
||||
{ language },
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
store.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
store.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,10 @@ export const codeBlockStyles = css`
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.affine-code-block-container.disable-line-numbers v-line {
|
||||
grid-template-columns: unset;
|
||||
}
|
||||
|
||||
.affine-code-block-container div:has(> v-line) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { CodeKeymapExtension } from './code-keymap.js';
|
||||
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
|
||||
import { codeSlashMenuConfig } from './configs/slash-menu.js';
|
||||
import { effects } from './effects.js';
|
||||
import { CodeBlockMarkdownExtension } from './markdown.js';
|
||||
|
||||
const codeToolbarWidget = WidgetViewExtension(
|
||||
'affine:code',
|
||||
@@ -44,6 +45,7 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:code', literal`affine-code`),
|
||||
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
|
||||
CodeKeymapExtension,
|
||||
CodeBlockMarkdownExtension,
|
||||
...getCodeClipboardExtensions(),
|
||||
]);
|
||||
context.register([
|
||||
|
||||
@@ -331,7 +331,6 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
|
||||
this.inlineEditor$.value?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
|
||||
@@ -209,10 +209,19 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
this.richText.value?.updateComplete
|
||||
@@ -233,6 +242,12 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
'paste',
|
||||
this._onPaste
|
||||
);
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
|
||||
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
type DividerBlockModel,
|
||||
DividerBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const DividerMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'divider',
|
||||
pattern: /^(-{3,}|\*{3,}|_{3,})\s$/,
|
||||
action: ({ inlineEditor, inlineRange }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== 'quote'
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.addBlock<DividerBlockModel>(
|
||||
DividerBlockSchema.model.flavour,
|
||||
{
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
const nextBlock = parent.children.at(index + 1);
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{},
|
||||
parent
|
||||
);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { DividerMarkdownExtension } from './markdown';
|
||||
|
||||
export class DividerViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-divider-block';
|
||||
@@ -19,6 +20,7 @@ export class DividerViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register([
|
||||
BlockViewExtension('affine:divider', literal`affine-divider`),
|
||||
DividerMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
|
||||
@@ -107,10 +107,10 @@ export class EmbedHtmlFullscreenToolbar extends LitElement {
|
||||
if (this._copied) return;
|
||||
|
||||
this.embedHtml.std.clipboard
|
||||
.writeToClipboard(items => {
|
||||
items['text/plain'] = this.embedHtml.model.props.html ?? '';
|
||||
return items;
|
||||
})
|
||||
.writeToClipboard(items => ({
|
||||
...items,
|
||||
'text/plain': this.embedHtml.model.props.html ?? '',
|
||||
}))
|
||||
.then(() => {
|
||||
this._copied = true;
|
||||
setTimeout(() => (this._copied = false), 1500);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NativeClipboardProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
formatSize,
|
||||
getBlockProps,
|
||||
isInsidePageEditor,
|
||||
@@ -111,28 +112,6 @@ export async function resetImageSize(
|
||||
block.store.updateBlock(model, props);
|
||||
}
|
||||
|
||||
function convertToPng(blob: Blob): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', _ => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width;
|
||||
c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
c.toBlob(resolve, 'image/png');
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = reader.result as string;
|
||||
});
|
||||
reader.addEventListener('error', () => resolve(null));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyImageBlob(
|
||||
block: ImageBlockComponent | ImageEdgelessBlockComponent
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { textKeymap } from '@blocksuite/affine-inline-preset';
|
||||
import { ListBlockSchema } from '@blocksuite/affine-model';
|
||||
import { markdownInput } from '@blocksuite/affine-rich-text';
|
||||
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { KeymapExtension, TextSelection } from '@blocksuite/std';
|
||||
@@ -125,20 +124,6 @@ export const ListKeymapExtension = KeymapExtension(
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
type ListBlockModel,
|
||||
ListBlockSchema,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ListMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'list',
|
||||
// group 2: number
|
||||
// group 3: bullet
|
||||
// group 4: bullet
|
||||
// group 5: todo
|
||||
// group 6: todo checked
|
||||
pattern: /^((\d+\.)|(-)|(\*)|(\[ ?\])|(\[x\]))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
let type: ListType;
|
||||
|
||||
if (match[2]) {
|
||||
type = 'numbered';
|
||||
} else if (match[3] || match[4]) {
|
||||
type = 'bulleted';
|
||||
} else if (match[5] || match[6]) {
|
||||
type = 'todo';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = match[6] !== undefined;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) return;
|
||||
|
||||
if (type !== 'numbered') {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
const id = store.addBlock<ListBlockModel>(
|
||||
ListBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...(type === 'todo' ? { checked } : {}),
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
focusTextModel(std, id);
|
||||
} else {
|
||||
let order = parseInt(match[2]);
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects.js';
|
||||
import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js';
|
||||
import { ListMarkdownExtension } from './markdown.js';
|
||||
|
||||
export class ListViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-list-block';
|
||||
@@ -23,6 +24,7 @@ export class ListViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:list', literal`affine-list`),
|
||||
ListKeymapExtension,
|
||||
ListTextKeymapExtension,
|
||||
ListMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ParagraphMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'heading',
|
||||
pattern: /^((#{1,6})|(>))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const type = (
|
||||
match[2] ? `h${match[2].length}` : 'quote'
|
||||
) as ParagraphType;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (
|
||||
!matchModels(model, [ParagraphBlockModel]) &&
|
||||
matchModels(model, [ListBlockModel])
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
const id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
focusTextModel(std, id);
|
||||
} else if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== type
|
||||
) {
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.updateBlock(model, { type });
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -28,6 +28,7 @@ import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
|
||||
import { paragraphBlockStyles } from './styles.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
|
||||
@@ -341,6 +348,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
</div>
|
||||
|
||||
${children}
|
||||
${widgets}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import {
|
||||
focusTextModel,
|
||||
getInlineEditorByModel,
|
||||
markdownInput,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
@@ -148,10 +147,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
|
||||
raw.preventDefault();
|
||||
|
||||
if (markdownInput(std, model.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model.props.type.startsWith('h') && model.props.collapsed) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return true;
|
||||
@@ -199,20 +194,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
event.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Tab: ctx => {
|
||||
const [success] = std.command
|
||||
.chain()
|
||||
|
||||
@@ -20,6 +20,7 @@ import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts';
|
||||
import type { ExtendedModel } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
focusTitle,
|
||||
getDocTitleInlineEditor,
|
||||
getPrevContentBlock,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
@@ -45,10 +46,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [EdgelessTextBlockModel])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prevBlock = getPrevContentBlock(editorHost, model);
|
||||
if (!prevBlock) {
|
||||
return handleNoPreviousSibling(editorHost, model);
|
||||
@@ -123,36 +120,63 @@ function handleNoPreviousSibling(editorHost: EditorHost, model: ExtendedModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel]) && parent.isPageBlock()) {
|
||||
const focusFirstBlockStart = () => {
|
||||
const firstBlock = parent.firstChild();
|
||||
if (firstBlock) {
|
||||
focusTextModel(editorHost.std, firstBlock.id, 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel])) {
|
||||
const hasTitleEditor = getDocTitleInlineEditor(editorHost);
|
||||
const rootModel = model.store.root as RootBlockModel;
|
||||
const title = rootModel.props.title;
|
||||
|
||||
const shouldHandleTitle = parent.isPageBlock() && hasTitleEditor;
|
||||
|
||||
doc.captureSync();
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
|
||||
if (shouldHandleTitle) {
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
}
|
||||
if (model.children.length > 0 || doc.getNext(model)) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
}
|
||||
// no other blocks, preserve a empty line
|
||||
else {
|
||||
text?.clear();
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Preserve at least one block to be able to focus on container click
|
||||
if (doc.getNext(model) || model.children.length > 0) {
|
||||
if (
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
} else {
|
||||
text?.clear();
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
matchModels(parent, [EdgelessTextBlockModel]) ||
|
||||
model.children.length > 0
|
||||
matchModels(parent, [EdgelessTextBlockModel]) &&
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { ParagraphMarkdownExtension } from './markdown.js';
|
||||
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
|
||||
import {
|
||||
ParagraphKeymapExtension,
|
||||
@@ -22,11 +26,6 @@ const placeholders = {
|
||||
quote: '',
|
||||
};
|
||||
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
getPlaceholder: z.optional(
|
||||
z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string())
|
||||
@@ -61,6 +60,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider<
|
||||
ParagraphBlockConfigExtension({
|
||||
getPlaceholder,
|
||||
}),
|
||||
ParagraphMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
isInsidePageEditor,
|
||||
isTopLevelBlock,
|
||||
isUrlInClipboard,
|
||||
@@ -67,7 +68,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { PageClipboard } from '../../clipboard/index.js';
|
||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||
import { isCanvasElementWithText } from '../utils/query.js';
|
||||
import { isCanvasElementWithText, isImageBlock } from '../utils/query.js';
|
||||
import { createElementsFromClipboardDataCommand } from './command.js';
|
||||
import {
|
||||
isPureFileInClipboard,
|
||||
@@ -126,6 +127,49 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only when an image is selected, it can be pasted normally to page mode.
|
||||
if (elements.length === 1 && isImageBlock(elements[0])) {
|
||||
const element = elements[0];
|
||||
const sourceId = element.props.sourceId$.peek();
|
||||
if (!sourceId) return;
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async items => {
|
||||
const job = this.std.store.getTransformer();
|
||||
await job.assetsManager.readFromBlob(sourceId);
|
||||
|
||||
let blob = job.assetsManager.getAssets().get(sourceId) ?? null;
|
||||
if (!blob) {
|
||||
return items;
|
||||
}
|
||||
|
||||
let type = blob.type;
|
||||
let supported = false;
|
||||
|
||||
try {
|
||||
supported = ClipboardItem?.supports(type) ?? false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// TODO(@fundon): when converting jpeg to png, image may become larger and exceed the limit.
|
||||
if (!supported) {
|
||||
type = 'image/png';
|
||||
blob = await convertToPng(blob);
|
||||
}
|
||||
|
||||
if (blob) {
|
||||
return {
|
||||
...items,
|
||||
[`${type}`]: blob,
|
||||
};
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async _items => {
|
||||
const data = await prepareClipboardData(elements, this.std);
|
||||
return {
|
||||
@@ -562,6 +606,10 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
|
||||
private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = this.toolManager.lastMousePos$.peek();
|
||||
|
||||
const noteProps = {
|
||||
|
||||
@@ -69,37 +69,39 @@ export async function prepareClipboardData(
|
||||
|
||||
export function isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
const allowedTypes = new Set([
|
||||
'Files',
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/x-moz-file',
|
||||
]);
|
||||
|
||||
return types.includes('Files') && types.every(type => allowedTypes.has(type));
|
||||
}
|
||||
|
||||
export function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (types.length === 1 && types[0] !== 'text/plain') {
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
return file;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||
return file;
|
||||
}
|
||||
|
||||
export function edgelessElementsBoundFromRawData(
|
||||
|
||||
@@ -647,6 +647,16 @@ export class TableCell extends SignalWatcher(
|
||||
return this.richText$.value?.inlineEditor;
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.readonly) {
|
||||
@@ -659,10 +669,7 @@ export class TableCell extends SignalWatcher(
|
||||
this.inlineEditor?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.add(() => {
|
||||
this.removeEventListener('keydown', selectAll);
|
||||
});
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'click', (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
requestAnimationFrame(() => {
|
||||
@@ -679,6 +686,13 @@ export class TableCell extends SignalWatcher(
|
||||
}
|
||||
this.richText$.value?.updateComplete
|
||||
.then(() => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const richText = this.richText$.value;
|
||||
|
||||
@@ -195,8 +195,7 @@ export class EmbedCardEditModal extends SignalWatcher(
|
||||
|
||||
const description = this.description$.value.trim();
|
||||
|
||||
const props: AliasInfo = { title };
|
||||
if (description) props.description = description;
|
||||
const props: AliasInfo = { title, description };
|
||||
|
||||
this.onSave?.(std, blockComponent, props);
|
||||
|
||||
|
||||
@@ -112,7 +112,10 @@ export class GroupTrait {
|
||||
return;
|
||||
}
|
||||
const { staticMap, groupInfo } = staticInfo;
|
||||
const groupMap: Record<string, Group> = { ...staticMap };
|
||||
const groupMap: Record<string, Group> = {};
|
||||
Object.entries(staticMap).forEach(([key, group]) => {
|
||||
groupMap[key] = new Group(key, group.value, groupInfo, this);
|
||||
});
|
||||
this.view.rows$.value.forEach(row => {
|
||||
const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id)
|
||||
.jsonValue$.value;
|
||||
@@ -182,6 +185,7 @@ export class GroupTrait {
|
||||
) {}
|
||||
|
||||
addToGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupMap || !groupInfo) {
|
||||
@@ -254,6 +258,7 @@ export class GroupTrait {
|
||||
toGroupKey: string,
|
||||
position: InsertToPosition
|
||||
) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -290,6 +295,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
moveGroupTo(groupKey: string, position: InsertToPosition) {
|
||||
this.view.lockRows(false);
|
||||
const groups = this.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
@@ -305,6 +311,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
removeFromGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -323,6 +330,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
updateValue(rows: string[], value: unknown) {
|
||||
this.view.lockRows(false);
|
||||
const propertyId = this.property$.value?.id;
|
||||
if (!propertyId) {
|
||||
return;
|
||||
|
||||
@@ -128,6 +128,7 @@ export abstract class SingleViewBase<
|
||||
);
|
||||
|
||||
rowsDelete(rows: string[]): void {
|
||||
this.lockRows(false);
|
||||
this.dataSource.rowDelete(rows);
|
||||
}
|
||||
|
||||
@@ -258,6 +259,7 @@ export abstract class SingleViewBase<
|
||||
abstract propertyGetOrCreate(propertyId: string): Property;
|
||||
|
||||
rowAdd(insertPosition: InsertToPosition | number): string {
|
||||
this.lockRows(false);
|
||||
return this.dataSource.rowAdd(insertPosition);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,12 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddCard = () => {
|
||||
this.view.addCard('end', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
this.view.addCard('start', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -79,12 +81,14 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -66,7 +66,9 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
return this.view.rowAdd(position);
|
||||
const id = this.view.rowAdd(position);
|
||||
this.ui$.value?.requestUpdate();
|
||||
return id;
|
||||
};
|
||||
|
||||
focusFirstCell = () => {};
|
||||
|
||||
@@ -83,6 +83,7 @@ export const popCardMenu = (
|
||||
{ before: true, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
@@ -97,6 +98,7 @@ export const popCardMenu = (
|
||||
{ before: false, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -111,6 +113,7 @@ export const popCardMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
kanbanViewLogic.view.rowsDelete([cardId]);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -128,6 +128,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
if (selection.selectionType === 'card') {
|
||||
this.view.rowsDelete(selection.cards.map(v => v.cardId));
|
||||
this.selection = undefined;
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
@@ -127,6 +128,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -139,12 +141,14 @@ export class KanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -73,6 +73,7 @@ export class KanbanViewUILogic extends DataViewUILogicBase<
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
this.ui$.value?.requestUpdate();
|
||||
return rowId;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,10 +51,12 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -77,6 +79,7 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const popMobileRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
view.rowsDelete([rowId]);
|
||||
tableViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -44,6 +44,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -79,6 +80,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -376,6 +376,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
|
||||
private readonly clickAddRow = () => {
|
||||
const group = this.group$.value;
|
||||
const rowId = this.tableViewManager.rowAdd('end', group?.key);
|
||||
this.requestUpdate();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const rowIndex = this.selectionController.getRow(group?.key, rowId)
|
||||
|
||||
@@ -58,6 +58,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
return;
|
||||
}
|
||||
this.tableViewManager.rowAdd('start', group.key);
|
||||
this.requestUpdate();
|
||||
const selectionController = this.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -95,6 +96,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.tableViewManager.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -78,6 +79,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -351,6 +351,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -83,6 +83,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -102,6 +103,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,6 +127,7 @@ export class TableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -10,7 +10,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'latex',
|
||||
|
||||
pattern:
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)$|(?<blockPrefix>\$\$\$\$)|(?<inlinePrefix>\$\$)$/g,
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)\s$|(?<blockPrefix>\$\$\$\$)\s$|(?<inlinePrefix>\$\$)\s$/g,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = pattern.exec(prefixText);
|
||||
if (!match || !match.groups) return;
|
||||
@@ -33,22 +33,10 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
|
||||
|
||||
if (blockPrefix === '$$$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 4,
|
||||
index: inlineRange.index - 5,
|
||||
length: 5,
|
||||
});
|
||||
|
||||
@@ -88,34 +76,22 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
}
|
||||
|
||||
if (inlinePrefix === '$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 1,
|
||||
},
|
||||
{
|
||||
@@ -129,7 +105,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
await inlineEditor.waitForUpdate();
|
||||
|
||||
const textPoint = inlineEditor.getTextPoint(
|
||||
inlineRange.index - 2 + 1
|
||||
inlineRange.index - 3 + 1
|
||||
);
|
||||
if (!textPoint) return;
|
||||
|
||||
@@ -159,21 +135,9 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
if (!content || content.length === 0) return;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
const startIndex = inlineRange.index - 2 - content.length - 2;
|
||||
const startIndex = inlineRange.index - 1 - 2 - content.length - 2;
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2 + content.length + 2 + 1,
|
||||
|
||||
@@ -3,27 +3,18 @@ import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const LinkExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'link',
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)$/,
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const linkText = match[1];
|
||||
const linkUrl = match[2];
|
||||
const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(linkText.length + linkUrl.length + 4 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const StrikeInlineSpecExtension =
|
||||
|
||||
export const CodeInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
name: 'inline-code',
|
||||
schema: z.literal(true).optional().nullable().catch(undefined),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.code;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
{
|
||||
name: 'bolditalic',
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/,
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -25,20 +25,11 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 3 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 3 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -54,18 +45,13 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 3,
|
||||
length: 3,
|
||||
index: inlineRange.index - 4,
|
||||
length: 4,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 6,
|
||||
length: 0,
|
||||
@@ -76,26 +62,14 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
|
||||
export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'bold',
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/,
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -110,18 +84,13 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 4,
|
||||
length: 0,
|
||||
@@ -131,26 +100,14 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'italic',
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/,
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -165,18 +122,13 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
@@ -187,7 +139,7 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
export const StrikethroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'strikethrough',
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/,
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -199,20 +151,11 @@ export const StrikethroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-targetText.length - (2 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -227,12 +170,8 @@ export const StrikethroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -249,7 +188,7 @@ export const StrikethroughExtension =
|
||||
export const UnderthroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'underthrough',
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/,
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -261,20 +200,11 @@ export const UnderthroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 1 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -289,12 +219,8 @@ export const UnderthroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -310,26 +236,14 @@ export const UnderthroughExtension =
|
||||
|
||||
export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/,
|
||||
pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -344,12 +258,8 @@ export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
|
||||
@@ -10,6 +10,5 @@ export {
|
||||
onModelTextUpdated,
|
||||
selectTextModel,
|
||||
} from './dom';
|
||||
export { markdownInput } from './markdown';
|
||||
export { RichText } from './rich-text';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
DividerBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toDivider(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (
|
||||
matchModels(model, [DividerBlockModel]) ||
|
||||
(matchModels(model, [ParagraphBlockModel]) && model.props.type === 'quote')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
beforeConvert(std, model, prefix.length);
|
||||
const blockProps = {
|
||||
children: model.children,
|
||||
};
|
||||
doc.addBlock('affine:divider', blockProps, parent, index);
|
||||
|
||||
const nextBlock = parent.children[index + 1];
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { markdownInput } from './markdown-input.js';
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
type ListProps,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toList(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
listType: ListType,
|
||||
prefix: string,
|
||||
otherProperties?: Partial<ListProps>
|
||||
) {
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
return;
|
||||
}
|
||||
const { store: doc } = std;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
if (listType !== 'numbered') {
|
||||
const index = parent.children.indexOf(model);
|
||||
const blockProps = {
|
||||
type: listType,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...otherProperties,
|
||||
};
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const id = doc.addBlock('affine:list', blockProps, parent, index);
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
let order = parseInt(prefix.slice(0, -1));
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import {
|
||||
CalloutBlockModel,
|
||||
CodeBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
isHorizontalRuleMarkdown,
|
||||
isMarkdownPrefix,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { getInlineEditorByModel } from '../dom.js';
|
||||
import { toDivider } from './divider.js';
|
||||
import { toList } from './list.js';
|
||||
import { toParagraph } from './paragraph.js';
|
||||
import { toCode } from './to-code.js';
|
||||
import { getPrefixText } from './utils.js';
|
||||
|
||||
export function markdownInput(
|
||||
std: BlockStdScope,
|
||||
id?: string
|
||||
): string | undefined {
|
||||
if (!id) {
|
||||
const selection = std.selection;
|
||||
const text = selection.find(TextSelection);
|
||||
id = text?.from.blockId;
|
||||
}
|
||||
if (!id) return;
|
||||
const model = std.store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
const inline = getInlineEditorByModel(std, model);
|
||||
if (!inline) return;
|
||||
const range = inline.getInlineRange();
|
||||
if (!range) return;
|
||||
|
||||
const prefixText = getPrefixText(inline);
|
||||
if (!isMarkdownPrefix(prefixText)) return;
|
||||
|
||||
const isParagraph = matchModels(model, [ParagraphBlockModel]);
|
||||
const isHeading = isParagraph && model.props.type.startsWith('h');
|
||||
const isParagraphQuoteBlock = isParagraph && model.props.type === 'quote';
|
||||
const isCodeBlock = matchModels(model, [CodeBlockModel]);
|
||||
if (
|
||||
isHeading ||
|
||||
isParagraphQuoteBlock ||
|
||||
isCodeBlock ||
|
||||
matchModels(model.parent, [CalloutBlockModel])
|
||||
)
|
||||
return;
|
||||
|
||||
const lineInfo = inline.getLine(range.index);
|
||||
if (!lineInfo) return;
|
||||
|
||||
const { lineIndex, rangeIndexRelatedToLine } = lineInfo;
|
||||
if (lineIndex !== 0 || rangeIndexRelatedToLine > prefixText.length) return;
|
||||
|
||||
// try to add code block
|
||||
const codeMatch = prefixText.match(/^```([a-zA-Z0-9]*)$/g);
|
||||
if (codeMatch) {
|
||||
return toCode(std, model, prefixText, codeMatch[0].slice(3));
|
||||
}
|
||||
|
||||
if (isHorizontalRuleMarkdown(prefixText.trim())) {
|
||||
return toDivider(std, model, prefixText);
|
||||
}
|
||||
|
||||
switch (prefixText.trim()) {
|
||||
case '[]':
|
||||
case '[ ]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: false,
|
||||
});
|
||||
case '[x]':
|
||||
return toList(std, model, 'todo', prefixText, {
|
||||
checked: true,
|
||||
});
|
||||
case '-':
|
||||
case '*':
|
||||
return toList(std, model, 'bulleted', prefixText);
|
||||
case '#':
|
||||
return toParagraph(std, model, 'h1', prefixText);
|
||||
case '##':
|
||||
return toParagraph(std, model, 'h2', prefixText);
|
||||
case '###':
|
||||
return toParagraph(std, model, 'h3', prefixText);
|
||||
case '####':
|
||||
return toParagraph(std, model, 'h4', prefixText);
|
||||
case '#####':
|
||||
return toParagraph(std, model, 'h5', prefixText);
|
||||
case '######':
|
||||
return toParagraph(std, model, 'h6', prefixText);
|
||||
case '>':
|
||||
return toParagraph(std, model, 'quote', prefixText);
|
||||
default:
|
||||
return toList(std, model, 'numbered', prefixText);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
ParagraphBlockModel,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toParagraph(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
type: ParagraphType,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
const blockProps = {
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
};
|
||||
doc.deleteBlock(model, { deleteChildren: false });
|
||||
const id = doc.addBlock('affine:paragraph', blockProps, parent, index);
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
if (matchModels(model, [ParagraphBlockModel]) && model.props.type !== type) {
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
doc.updateBlock(model, { type });
|
||||
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
|
||||
// If the model is already a paragraph with the same type, do nothing
|
||||
return model.id;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function toCode(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefixText: string,
|
||||
language: string | null
|
||||
) {
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type === 'quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = model.store;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.captureSync();
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
const codeId = doc.addBlock('affine:code', { language }, parent, index);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
doc.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
doc.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
|
||||
return codeId;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { InlineEditor } from '@blocksuite/std/inline';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
|
||||
export function getPrefixText(inlineEditor: InlineEditor) {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return '';
|
||||
const firstLineEnd = inlineEditor.yTextString.search(/\n/);
|
||||
if (firstLineEnd !== -1 && inlineRange.index > firstLineEnd) {
|
||||
return '';
|
||||
}
|
||||
const textPoint = inlineEditor.getTextPoint(inlineRange.index);
|
||||
if (!textPoint) return '';
|
||||
const [leafStart, offsetStart] = textPoint;
|
||||
return leafStart.textContent
|
||||
? leafStart.textContent.slice(0, offsetStart)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function beforeConvert(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
index: number
|
||||
) {
|
||||
const { text } = model;
|
||||
if (!text) return;
|
||||
// Add a space after the text, then stop capturing
|
||||
// So when the user undo, the prefix will be restored with a `space`
|
||||
// Ex. (| is the cursor position)
|
||||
// *| <- user input
|
||||
// <space> -> bullet list
|
||||
// *<space>| -> undo
|
||||
text.insert(' ', index);
|
||||
focusTextModel(std, model.id, index + 1);
|
||||
std.store.captureSync();
|
||||
text.delete(0, index + 1);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import * as Y from 'yjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { onVBeforeinput, onVCompositionEnd } from './hooks.js';
|
||||
import { getPrefixText } from './utils.js';
|
||||
|
||||
interface RichTextStackItem {
|
||||
meta: Map<'richtext-v-range', InlineRange | null>;
|
||||
@@ -186,38 +187,60 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
const markdownMatches = this.markdownMatches;
|
||||
if (markdownMatches) {
|
||||
inlineEditor.disposables.addFromEvent(
|
||||
this.inlineEventSource ?? this.inlineEditorContainer,
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key !== ' ' && e.key !== 'Enter') return;
|
||||
const markdownTransform = (isEnter: boolean = false) => {
|
||||
let inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return false;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length > 0) return;
|
||||
let prefixText = getPrefixText(inlineEditor);
|
||||
if (isEnter) prefixText = `${prefixText} `;
|
||||
|
||||
const nearestLineBreakIndex = inlineEditor.yTextString
|
||||
.slice(0, inlineRange.index)
|
||||
.lastIndexOf('\n');
|
||||
const prefixText = inlineEditor.yTextString.slice(
|
||||
nearestLineBreakIndex + 1,
|
||||
inlineRange.index
|
||||
);
|
||||
|
||||
for (const match of markdownMatches) {
|
||||
const { pattern, action } = match;
|
||||
if (prefixText.match(pattern)) {
|
||||
action({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager: this.undoManager,
|
||||
for (const match of markdownMatches) {
|
||||
const { pattern, action } = match;
|
||||
if (prefixText.match(pattern)) {
|
||||
if (isEnter) {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
e.preventDefault();
|
||||
break;
|
||||
inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange) return false;
|
||||
}
|
||||
|
||||
action({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
inlineRange,
|
||||
pattern,
|
||||
undoManager: this.undoManager,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
inlineEditor.disposables.add(
|
||||
inlineEditor.slots.inputting.subscribe(data => {
|
||||
if (!inlineEditor.isComposing && data === ' ') {
|
||||
markdownTransform();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
inlineEditor.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(event => {
|
||||
if (event.key === 'Enter' && markdownTransform(true)) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,3 +52,17 @@ export function clearMarksOnDiscontinuousInput(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getPrefixText(inlineEditor: InlineEditor) {
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length > 0) return '';
|
||||
|
||||
const nearestLineBreakIndex = inlineEditor.yTextString
|
||||
.slice(0, inlineRange.index)
|
||||
.lastIndexOf('\n');
|
||||
const prefixText = inlineEditor.yTextString.slice(
|
||||
nearestLineBreakIndex + 1,
|
||||
inlineRange.index
|
||||
);
|
||||
return prefixText;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
3
blocksuite/affine/shared/src/test-utils/index.ts
Normal file
3
blocksuite/affine/shared/src/test-utils/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './affine-template';
|
||||
export * from './affine-test-utils';
|
||||
export * from './create-test-host';
|
||||
@@ -176,9 +176,7 @@ export async function openFilesWith(
|
||||
resolve(input.files ? Array.from(input.files) : null);
|
||||
});
|
||||
// The `cancel` event fires when the user cancels the dialog.
|
||||
input.addEventListener('cancel', () => {
|
||||
resolve(null);
|
||||
});
|
||||
input.addEventListener('cancel', () => resolve(null));
|
||||
// Show the picker.
|
||||
if ('showPicker' in HTMLInputElement.prototype) {
|
||||
input.showPicker();
|
||||
@@ -188,16 +186,16 @@ export async function openFilesWith(
|
||||
});
|
||||
}
|
||||
|
||||
export function openSingleFileWith(
|
||||
export async function openSingleFileWith(
|
||||
acceptType?: AcceptTypes
|
||||
): Promise<File | null> {
|
||||
return openFilesWith(acceptType, false).then(files => files?.at(0) ?? null);
|
||||
const files = await openFilesWith(acceptType, false);
|
||||
return files?.at(0) ?? null;
|
||||
}
|
||||
|
||||
export async function getImageFilesFromLocal() {
|
||||
const imageFiles = await openFilesWith('Images');
|
||||
if (!imageFiles) return [];
|
||||
return imageFiles;
|
||||
const files = await openFilesWith('Images');
|
||||
return files ?? [];
|
||||
}
|
||||
|
||||
export function downloadBlob(blob: Blob, name: string) {
|
||||
|
||||
@@ -26,3 +26,34 @@ export function readImageSize(file: File | Blob) {
|
||||
img.src = sanitizedURL;
|
||||
});
|
||||
}
|
||||
|
||||
export function convertToPng(blob: Blob): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener('load', _ => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width;
|
||||
c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
c.toBlob(resolve, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = () => resolve(null);
|
||||
|
||||
img.src = reader.result as string;
|
||||
});
|
||||
|
||||
reader.addEventListener('error', () => resolve(null));
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,35 +73,3 @@ export function substringMatchScore(name: string, query: string) {
|
||||
// normalize
|
||||
return 0.5 * score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prefix is a markdown prefix.
|
||||
* Ex. 1. 2. 3. - * [] [ ] [x] # ## ### #### ##### ###### --- *** ___ > ```
|
||||
*/
|
||||
export function isMarkdownPrefix(prefix: string) {
|
||||
return (
|
||||
!!prefix.match(
|
||||
/^(\d+\.|-|\*|\[ ?\]|\[x\]|(#){1,6}|>|```([a-zA-Z0-9]*))$/
|
||||
) || isHorizontalRuleMarkdown(prefix)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the prefix is a valid markdown horizontal rule - https://www.markdownguide.org/basic-syntax/#horizontal-rules
|
||||
* @param prefix - The string to check for horizontal rule syntax
|
||||
* @returns boolean - True if the string represents a valid horizontal rule
|
||||
*
|
||||
* Valid horizontal rules:
|
||||
* - Three or more consecutive hyphens (e.g., "---", "----")
|
||||
* - Three or more consecutive asterisks (e.g., "***", "****")
|
||||
* - Three or more consecutive underscores (e.g., "___", "____")
|
||||
*
|
||||
* Invalid examples:
|
||||
* - Mixed characters (e.g., "--*", "-*-")
|
||||
* - Less than three characters (e.g., "--", "**")
|
||||
*/
|
||||
export function isHorizontalRuleMarkdown(prefix: string) {
|
||||
const horizontalRulePattern = /^(-{3,}|\*{3,}|_{3,})$/;
|
||||
|
||||
return !!prefix.match(horizontalRulePattern);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
}
|
||||
}
|
||||
|
||||
editor-toolbar[data-open][data-inline='true'] {
|
||||
transition-property: opacity, overlay, display, transform;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
editor-toolbar[data-placement='inner'] {
|
||||
background-color: unset;
|
||||
box-shadow: unset;
|
||||
@@ -536,9 +541,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
disposables.add(subscription);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -632,6 +635,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
|
||||
// Hides toolbar
|
||||
if (Flag.None === value || flags.check(Flag.Hiding, value)) {
|
||||
if ('inline' in toolbar.dataset) delete toolbar.dataset.inline;
|
||||
if (toolbar.dataset.open) delete toolbar.dataset.open;
|
||||
// Closes dropdown menus
|
||||
toolbar
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
shift,
|
||||
size,
|
||||
} from '@floating-ui/dom';
|
||||
import { html, nothing, render } from 'lit';
|
||||
import { html, render } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
@@ -297,11 +297,22 @@ export function renderToolbar(
|
||||
render(
|
||||
join(
|
||||
renderActions(primaryActionGroup, context),
|
||||
innerToolbar ? nothing : renderToolbarSeparator()
|
||||
innerToolbar ? null : renderToolbarSeparator()
|
||||
),
|
||||
toolbar
|
||||
);
|
||||
|
||||
// Avoids shaking
|
||||
if (flavour === 'affine:note' && context.std.range.value) {
|
||||
if (!('inline' in toolbar.dataset)) {
|
||||
toolbar.dataset.inline = '';
|
||||
} else {
|
||||
toolbar.dataset.inline = 'true';
|
||||
}
|
||||
} else {
|
||||
delete toolbar.dataset.inline;
|
||||
}
|
||||
|
||||
if (toolbar.dataset.open) return;
|
||||
toolbar.dataset.open = 'true';
|
||||
}
|
||||
@@ -358,14 +369,22 @@ function renderActionItem(action: ToolbarAction, context: ToolbarContext) {
|
||||
const innerToolbar = context.placement$.value === 'inner';
|
||||
const ids = action.id.split('.');
|
||||
const id = ids[ids.length - 1];
|
||||
const label = action.label ?? action.tooltip ?? id;
|
||||
const actived =
|
||||
typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active;
|
||||
const disabled =
|
||||
typeof action.disabled === 'function'
|
||||
? action.disabled(context)
|
||||
: action.disabled;
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
data-testid=${ifDefined(id)}
|
||||
aria-label=${ifDefined(action.label ?? action.tooltip ?? id)}
|
||||
?active=${typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active}
|
||||
?disabled=${action.disabled}
|
||||
aria-label=${ifDefined(label)}
|
||||
?active=${actived}
|
||||
?disabled=${disabled}
|
||||
.tooltip=${action.tooltip}
|
||||
.iconContainerPadding=${innerToolbar ? 4 : 2}
|
||||
.iconSize=${innerToolbar ? '16px' : undefined}
|
||||
@@ -383,17 +402,24 @@ function renderMenuActionItem(action: ToolbarAction, context: ToolbarContext) {
|
||||
const innerToolbar = context.placement$.value === 'inner';
|
||||
const ids = action.id.split('.');
|
||||
const id = ids[ids.length - 1];
|
||||
const label = action.label ?? action.tooltip ?? id;
|
||||
const actived =
|
||||
typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active;
|
||||
const disabled =
|
||||
typeof action.disabled === 'function'
|
||||
? action.disabled(context)
|
||||
: action.disabled;
|
||||
const destructive = action.variant === 'destructive' ? 'delete' : undefined;
|
||||
|
||||
return html`
|
||||
<editor-menu-action
|
||||
data-testid=${ifDefined(id)}
|
||||
aria-label=${ifDefined(action.label ?? action.tooltip ?? id)}
|
||||
class="${ifDefined(
|
||||
action.variant === 'destructive' ? 'delete' : undefined
|
||||
)}"
|
||||
?active=${typeof action.active === 'function'
|
||||
? action.active(context)
|
||||
: action.active}
|
||||
?disabled=${action.disabled}
|
||||
aria-label=${ifDefined(label)}
|
||||
class="${ifDefined(destructive)}"
|
||||
?active=${actived}
|
||||
?disabled=${disabled}
|
||||
.tooltip=${ifDefined(action.tooltip)}
|
||||
.iconContainerPadding=${innerToolbar ? 4 : 2}
|
||||
.iconSize=${innerToolbar ? '16px' : undefined}
|
||||
|
||||
@@ -118,10 +118,16 @@ Get the root block of the store.
|
||||
|
||||
### addBlock()
|
||||
|
||||
> **addBlock**(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string`
|
||||
> **addBlock**\<`T`\>(`flavour`, `blockProps`, `parent?`, `parentIndex?`): `string`
|
||||
|
||||
Creates and adds a new block to the store
|
||||
|
||||
#### Type Parameters
|
||||
|
||||
##### T
|
||||
|
||||
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### flavour
|
||||
@@ -132,7 +138,7 @@ The block's flavour (type)
|
||||
|
||||
##### blockProps
|
||||
|
||||
`Partial`\<`BlockSysProps` & `Record`\<`string`, `unknown`\> & `Omit`\<`BlockProps`, `"flavour"`\>\> = `{}`
|
||||
`Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\> = `{}`
|
||||
|
||||
Optional properties for the new block
|
||||
|
||||
|
||||
@@ -124,18 +124,23 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
copySlice = async (slice: Slice) => {
|
||||
const adapterKeys = this._adapters.map(adapter => adapter.mimeType);
|
||||
|
||||
await this.writeToClipboard(async _items => {
|
||||
const items = { ..._items };
|
||||
await this.writeToClipboard(async items => {
|
||||
const filtered = (
|
||||
await Promise.all(
|
||||
adapterKeys.map(async type => {
|
||||
const item = await this._getClipboardItem(slice, type);
|
||||
if (typeof item === 'string') {
|
||||
return [type, item];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)
|
||||
).filter((adapter): adapter is string[] => Boolean(adapter));
|
||||
|
||||
await Promise.all(
|
||||
adapterKeys.map(async type => {
|
||||
const item = await this._getClipboardItem(slice, type);
|
||||
if (typeof item === 'string') {
|
||||
items[type] = item;
|
||||
}
|
||||
})
|
||||
);
|
||||
return items;
|
||||
return {
|
||||
...items,
|
||||
...Object.fromEntries(filtered),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -263,49 +268,56 @@ export class Clipboard extends LifeCycleWatcher {
|
||||
}
|
||||
|
||||
async writeToClipboard(
|
||||
updateItems: (
|
||||
items: Record<string, unknown>
|
||||
) => Promise<Record<string, unknown>> | Record<string, unknown>
|
||||
updateItems: <T extends Record<string, unknown>>(items: T) => Promise<T> | T
|
||||
) {
|
||||
const _items = {
|
||||
const items = await updateItems<
|
||||
Partial<{
|
||||
'text/plain': string;
|
||||
'text/html': string;
|
||||
'image/png': string | Blob;
|
||||
}>
|
||||
>({
|
||||
'text/plain': '',
|
||||
'text/html': '',
|
||||
'image/png': '',
|
||||
};
|
||||
|
||||
const items = await updateItems(_items);
|
||||
|
||||
const text = items['text/plain'] as string;
|
||||
const innerHTML = items['text/html'] as string;
|
||||
const png = items['image/png'] as string | Blob;
|
||||
});
|
||||
const text = items['text/plain'] ?? '';
|
||||
const innerHTML = items['text/html'] ?? '';
|
||||
const image = items['image/png'];
|
||||
|
||||
delete items['text/plain'];
|
||||
delete items['text/html'];
|
||||
delete items['image/png'];
|
||||
|
||||
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
|
||||
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
|
||||
const htmlBlob = new Blob([html], {
|
||||
type: 'text/html',
|
||||
});
|
||||
const clipboardItems: Record<string, Blob> = {
|
||||
'text/html': htmlBlob,
|
||||
};
|
||||
const clipboardItems: Record<string, Blob> = {};
|
||||
|
||||
if (image) {
|
||||
const type = 'image/png';
|
||||
|
||||
delete items[type];
|
||||
|
||||
if (typeof image === 'string') {
|
||||
clipboardItems[type] = new Blob([image], { type });
|
||||
} else if (image instanceof Blob) {
|
||||
clipboardItems[type] = image;
|
||||
}
|
||||
}
|
||||
|
||||
if (text.length > 0) {
|
||||
const textBlob = new Blob([text], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
clipboardItems['text/plain'] = textBlob;
|
||||
const type = 'text/plain';
|
||||
clipboardItems[type] = new Blob([text], { type });
|
||||
}
|
||||
|
||||
if (png instanceof Blob) {
|
||||
clipboardItems['image/png'] = png;
|
||||
} else if (png.length > 0) {
|
||||
const pngBlob = new Blob([png], {
|
||||
type: 'image/png',
|
||||
});
|
||||
clipboardItems['image/png'] = pngBlob;
|
||||
const hasInnerHTML = Boolean(innerHTML.length);
|
||||
const isEmpty = Object.keys(clipboardItems).length === 0;
|
||||
|
||||
// If there are no items, fall back to snapshot.
|
||||
if (hasInnerHTML || isEmpty) {
|
||||
const type = 'text/html';
|
||||
const snapshot = lz.compressToEncodedURIComponent(JSON.stringify(items));
|
||||
const html = `<div data-blocksuite-snapshot='${snapshot}'>${innerHTML}</div>`;
|
||||
clipboardItems[type] = new Blob([html], { type });
|
||||
}
|
||||
|
||||
await navigator.clipboard.write([new ClipboardItem(clipboardItems)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +165,9 @@ export class InlineEditor<
|
||||
inlineRangeSync: new Subject<Range | null>(),
|
||||
/**
|
||||
* Corresponding to the `compositionUpdate` and `beforeInput` events, and triggered only when the `inlineRange` is not null.
|
||||
* The parameter is the `event.data`.
|
||||
*/
|
||||
inputting: new Subject<void>(),
|
||||
inputting: new Subject<string>(),
|
||||
/**
|
||||
* Triggered only when the `inlineRange` is not null.
|
||||
*/
|
||||
|
||||
@@ -119,7 +119,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
this.editor as never
|
||||
);
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onClick = (event: MouseEvent) => {
|
||||
@@ -181,10 +181,10 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
});
|
||||
}
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onCompositionStart = () => {
|
||||
private readonly _onCompositionStart = (event: CompositionEvent) => {
|
||||
this._isComposing = true;
|
||||
if (!this.editor.rootElement) return;
|
||||
// embeds is not editable and it will break IME
|
||||
@@ -201,9 +201,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
} else {
|
||||
this._compositionInlineRange = null;
|
||||
}
|
||||
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onCompositionUpdate = () => {
|
||||
private readonly _onCompositionUpdate = (event: CompositionEvent) => {
|
||||
if (!this.editor.rootElement || !this.editor.rootElement.isConnected) {
|
||||
return;
|
||||
}
|
||||
@@ -216,7 +218,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
)
|
||||
return;
|
||||
|
||||
this.editor.slots.inputting.next();
|
||||
this.editor.slots.inputting.next(event.data ?? '');
|
||||
};
|
||||
|
||||
private readonly _onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -359,13 +361,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
|
||||
'compositionupdate',
|
||||
this._onCompositionUpdate
|
||||
);
|
||||
this.editor.disposables.addFromEvent(
|
||||
eventSource,
|
||||
'compositionend',
|
||||
(event: CompositionEvent) => {
|
||||
this._onCompositionEnd(event).catch(console.error);
|
||||
}
|
||||
);
|
||||
this.editor.disposables.addFromEvent(eventSource, 'compositionend', e => {
|
||||
this._onCompositionEnd(e).catch(console.error);
|
||||
});
|
||||
this.editor.disposables.addFromEvent(
|
||||
eventSource,
|
||||
'keydown',
|
||||
|
||||
@@ -740,9 +740,9 @@ export class Store {
|
||||
*
|
||||
* @category Block CRUD
|
||||
*/
|
||||
addBlock(
|
||||
addBlock<T extends BlockModel = BlockModel>(
|
||||
flavour: string,
|
||||
blockProps: Partial<BlockProps & Omit<BlockProps, 'flavour'>> = {},
|
||||
blockProps: Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps> = {},
|
||||
parent?: BlockModel | string | null,
|
||||
parentIndex?: number
|
||||
): string {
|
||||
|
||||
@@ -52,6 +52,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
[
|
||||
{
|
||||
id: 'docId1',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
@@ -11,11 +11,11 @@ import { JobQueue } from '../base';
|
||||
import { ConfigModule } from '../base/config';
|
||||
import { AuthService } from '../core/auth';
|
||||
import { DocReader } from '../core/doc';
|
||||
import { CopilotContextService } from '../plugins/copilot/context';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextService,
|
||||
} from '../plugins/copilot/context';
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
} from '../plugins/copilot/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderFactory,
|
||||
@@ -65,7 +65,7 @@ const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
db: PrismaClient;
|
||||
context: CopilotContextService;
|
||||
jobs: CopilotContextDocJob;
|
||||
jobs: CopilotEmbeddingJob;
|
||||
prompt: PromptService;
|
||||
factory: CopilotProviderFactory;
|
||||
storage: CopilotStorage;
|
||||
@@ -115,7 +115,7 @@ test.before(async t => {
|
||||
const context = app.get(CopilotContextService);
|
||||
const prompt = app.get(PromptService);
|
||||
const storage = app.get(CopilotStorage);
|
||||
const jobs = app.get(CopilotContextDocJob);
|
||||
const jobs = app.get(CopilotEmbeddingJob);
|
||||
|
||||
t.context.app = app;
|
||||
t.context.db = db;
|
||||
|
||||
@@ -13,11 +13,11 @@ import { AuthService } from '../core/auth';
|
||||
import { QuotaModule } from '../core/quota';
|
||||
import { ContextCategories, WorkspaceModel } from '../models';
|
||||
import { CopilotModule } from '../plugins/copilot';
|
||||
import { CopilotContextService } from '../plugins/copilot/context';
|
||||
import {
|
||||
CopilotContextDocJob,
|
||||
CopilotContextService,
|
||||
} from '../plugins/copilot/context';
|
||||
import { MockEmbeddingClient } from '../plugins/copilot/context/embedding';
|
||||
CopilotEmbeddingJob,
|
||||
MockEmbeddingClient,
|
||||
} from '../plugins/copilot/embedding';
|
||||
import { prompts, PromptService } from '../plugins/copilot/prompt';
|
||||
import {
|
||||
CopilotProviderFactory,
|
||||
@@ -69,7 +69,7 @@ const test = ava as TestFn<{
|
||||
workspaceEmbedding: CopilotWorkspaceService;
|
||||
factory: CopilotProviderFactory;
|
||||
session: ChatSessionService;
|
||||
jobs: CopilotContextDocJob;
|
||||
jobs: CopilotEmbeddingJob;
|
||||
storage: CopilotStorage;
|
||||
workflow: CopilotWorkflowService;
|
||||
executors: {
|
||||
@@ -127,7 +127,7 @@ test.before(async t => {
|
||||
const storage = module.get(CopilotStorage);
|
||||
|
||||
const context = module.get(CopilotContextService);
|
||||
const jobs = module.get(CopilotContextDocJob);
|
||||
const jobs = module.get(CopilotEmbeddingJob);
|
||||
const transcript = module.get(CopilotTranscriptionService);
|
||||
const workspaceEmbedding = module.get(CopilotWorkspaceService);
|
||||
|
||||
|
||||
@@ -53,3 +53,195 @@ Generated by [AVA](https://avajs.dev).
|
||||
> should return true when embedding table is available
|
||||
|
||||
true
|
||||
|
||||
## should merge doc status correctly
|
||||
|
||||
> basic doc status merge
|
||||
|
||||
[
|
||||
{
|
||||
id: 'doc1',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc2',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc3',
|
||||
status: 'failed',
|
||||
},
|
||||
{
|
||||
id: 'doc4',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
> mixed doc status merge
|
||||
|
||||
[
|
||||
{
|
||||
id: 'doc5',
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
id: 'doc5',
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
id: 'doc6',
|
||||
status: 'processing',
|
||||
},
|
||||
{
|
||||
id: 'doc6',
|
||||
status: 'failed',
|
||||
},
|
||||
{
|
||||
id: 'doc7',
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
> edge cases results
|
||||
|
||||
[
|
||||
{
|
||||
case: 0,
|
||||
length: 1,
|
||||
statuses: [
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
{
|
||||
case: 1,
|
||||
length: 1,
|
||||
statuses: [
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
{
|
||||
case: 2,
|
||||
length: 100,
|
||||
statuses: [
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
'processing',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
## should handle concurrent mergeDocStatus calls
|
||||
|
||||
> concurrent calls results
|
||||
|
||||
[
|
||||
{
|
||||
call: 1,
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
call: 2,
|
||||
status: 'finished',
|
||||
},
|
||||
{
|
||||
call: 3,
|
||||
status: 'processing',
|
||||
},
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -2,8 +2,10 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { AiSession, PrismaClient, User, Workspace } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { ContextEmbedStatus } from '../../models/common/copilot';
|
||||
import { CopilotContextModel } from '../../models/copilot-context';
|
||||
import { CopilotSessionModel } from '../../models/copilot-session';
|
||||
import { CopilotWorkspaceConfigModel } from '../../models/copilot-workspace';
|
||||
@@ -236,3 +238,173 @@ test('should check embedding table', async t => {
|
||||
// t.false(ret, 'should return false when embedding table is not available');
|
||||
// }
|
||||
});
|
||||
|
||||
test('should merge doc status correctly', async t => {
|
||||
const createDoc = (id: string, status?: string) => ({
|
||||
id,
|
||||
createdAt: Date.now(),
|
||||
...(status && { status: status as any }),
|
||||
});
|
||||
|
||||
const createDocWithEmbedding = async (docId: string) => {
|
||||
await t.context.db.snapshot.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
id: docId,
|
||||
blob: Buffer.from([1, 1]),
|
||||
state: Buffer.from([1, 1]),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
docId,
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const emptyResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
[]
|
||||
);
|
||||
t.deepEqual(emptyResult, []);
|
||||
|
||||
const basicDocs = [
|
||||
createDoc('doc1'),
|
||||
createDoc('doc2'),
|
||||
createDoc('doc3', 'failed'),
|
||||
createDoc('doc4', 'processing'),
|
||||
];
|
||||
const basicResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
basicDocs
|
||||
);
|
||||
t.snapshot(
|
||||
basicResult.map(d => ({ id: d.id, status: d.status })),
|
||||
'basic doc status merge'
|
||||
);
|
||||
|
||||
{
|
||||
await createDocWithEmbedding('doc5');
|
||||
|
||||
const mixedDocs = [
|
||||
createDoc('doc5'),
|
||||
createDoc('doc5', 'processing'),
|
||||
createDoc('doc6'),
|
||||
createDoc('doc6', 'failed'),
|
||||
createDoc('doc7'),
|
||||
];
|
||||
const mixedResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
mixedDocs
|
||||
);
|
||||
t.snapshot(
|
||||
mixedResult.map(d => ({ id: d.id, status: d.status })),
|
||||
'mixed doc status merge'
|
||||
);
|
||||
|
||||
const hasEmbeddingStub = Sinon.stub(
|
||||
t.context.copilotContext,
|
||||
'hasWorkspaceEmbedding'
|
||||
).resolves(new Set<string>());
|
||||
|
||||
const stubResult = await t.context.copilotContext.mergeDocStatus(
|
||||
workspace.id,
|
||||
[createDoc('doc5')]
|
||||
);
|
||||
t.is(stubResult[0].status, ContextEmbedStatus.processing);
|
||||
|
||||
hasEmbeddingStub.restore();
|
||||
}
|
||||
|
||||
{
|
||||
const testCases = [
|
||||
{
|
||||
workspaceId: 'invalid-workspace',
|
||||
docs: [{ id: 'doc1', createdAt: Date.now() }],
|
||||
},
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
docs: [{ id: 'doc1', createdAt: Date.now(), status: undefined as any }],
|
||||
},
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
docs: Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `doc-${i}`,
|
||||
createdAt: Date.now() + i,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
testCases.map(testCase =>
|
||||
t.context.copilotContext.mergeDocStatus(
|
||||
testCase.workspaceId,
|
||||
testCase.docs
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
results.map((result, index) => ({
|
||||
case: index,
|
||||
length: result.length,
|
||||
statuses: result.map(d => d.status),
|
||||
})),
|
||||
'edge cases results'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle concurrent mergeDocStatus calls', async t => {
|
||||
await t.context.db.snapshot.create({
|
||||
data: {
|
||||
workspaceId: workspace.id,
|
||||
id: 'concurrent-doc',
|
||||
blob: Buffer.from([1, 1]),
|
||||
state: Buffer.from([1, 1]),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
'concurrent-doc',
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const concurrentDocs = [
|
||||
[{ id: 'concurrent-doc', createdAt: Date.now() }],
|
||||
[{ id: 'concurrent-doc', createdAt: Date.now() + 1000 }],
|
||||
[{ id: 'non-existent-doc', createdAt: Date.now() }],
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
concurrentDocs.map(docs =>
|
||||
t.context.copilotContext.mergeDocStatus(workspace.id, docs)
|
||||
)
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
results.map((result, index) => ({
|
||||
call: index + 1,
|
||||
status: result[0].status,
|
||||
})),
|
||||
'concurrent calls results'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,10 @@ test.beforeEach(async t => {
|
||||
workspace = await t.context.models.workspace.create(user.id);
|
||||
});
|
||||
|
||||
test.afterEach.always(async () => {
|
||||
mock.reset();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
@@ -176,6 +180,15 @@ test('should get doc content in json format', async t => {
|
||||
summary: 'test summary',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app
|
||||
.GET(`/rpc/workspaces/${workspace.id}/docs/${docId}/content?full=false`)
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
})
|
||||
.expect(200);
|
||||
t.pass();
|
||||
});
|
||||
|
||||
@@ -184,7 +197,7 @@ test('should get full doc content in json format', async t => {
|
||||
mock.method(t.context.databaseDocReader, 'getFullDocContent', async () => {
|
||||
return {
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
summary: 'test summary full',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -194,7 +207,7 @@ test('should get full doc content in json format', async t => {
|
||||
.set('x-access-token', t.context.crypto.sign(docId))
|
||||
.expect({
|
||||
title: 'test title',
|
||||
summary: 'test summary',
|
||||
summary: 'test summary full',
|
||||
})
|
||||
.expect(200);
|
||||
t.pass();
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
Logger,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
RawBody,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { Args } from '@nestjs/graphql';
|
||||
import type { Response } from 'express';
|
||||
|
||||
import { NotFound, SkipThrottle } from '../../base';
|
||||
@@ -78,11 +78,12 @@ export class DocRpcController {
|
||||
async getDocContent(
|
||||
@Param('workspaceId') workspaceId: string,
|
||||
@Param('docId') docId: string,
|
||||
@Args('full', { nullable: true }) fullContent?: boolean
|
||||
@Query('full') fullContent?: string
|
||||
) {
|
||||
const content = fullContent
|
||||
? await this.docReader.getFullDocContent(workspaceId, docId)
|
||||
: await this.docReader.getDocContent(workspaceId, docId);
|
||||
const content =
|
||||
fullContent === 'true'
|
||||
? await this.docReader.getFullDocContent(workspaceId, docId)
|
||||
: await this.docReader.getDocContent(workspaceId, docId);
|
||||
if (!content) {
|
||||
throw new NotFound('Doc not found');
|
||||
}
|
||||
|
||||
@@ -605,6 +605,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'BFZk3c2ERp-sliRvA7MQ_p3NdkdCLt2Ze0DQ9i21dpA=',
|
||||
],
|
||||
blockId: 'lcZphIJe63',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
@@ -619,6 +620,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'HWvCItS78DzPGbwcuaGcfkpVDUvL98IvH5SIK8-AcL8=',
|
||||
],
|
||||
blockId: 'JlgVJdWU12',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
@@ -633,6 +635,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'ZRKpsBoC88qEMmeiXKXqywfA1rLvWoLa5rpEh9x9Oj0=',
|
||||
],
|
||||
blockId: 'lht7AqBqnF',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
@@ -1236,6 +1239,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'BFZk3c2ERp-sliRvA7MQ_p3NdkdCLt2Ze0DQ9i21dpA=',
|
||||
],
|
||||
blockId: 'lcZphIJe63',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
@@ -1250,6 +1254,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'HWvCItS78DzPGbwcuaGcfkpVDUvL98IvH5SIK8-AcL8=',
|
||||
],
|
||||
blockId: 'JlgVJdWU12',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
@@ -1264,6 +1269,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
'ZRKpsBoC88qEMmeiXKXqywfA1rLvWoLa5rpEh9x9Oj0=',
|
||||
],
|
||||
blockId: 'lht7AqBqnF',
|
||||
content: '',
|
||||
docId: 'doc-0',
|
||||
flavour: 'affine:image',
|
||||
parentBlockId: '6x7ALjUDjj',
|
||||
|
||||
Binary file not shown.
@@ -91,7 +91,9 @@ export class CopilotContextModel extends BaseModel {
|
||||
const status = finishedDoc.has(doc.id)
|
||||
? ContextEmbedStatus.finished
|
||||
: undefined;
|
||||
doc.status = status || doc.status;
|
||||
// NOTE: when the document has not been synchronized to the server or is in the embedding queue
|
||||
// the status will be empty, fallback to processing if no status is provided
|
||||
doc.status = status || doc.status || ContextEmbedStatus.processing;
|
||||
}
|
||||
|
||||
return docs;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { CopilotContextDocJob } from './job';
|
||||
export { CopilotContextResolver, CopilotContextRootResolver } from './resolver';
|
||||
export { CopilotContextService } from './service';
|
||||
|
||||
@@ -44,12 +44,12 @@ import {
|
||||
FileChunkSimilarity,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { CopilotEmbeddingJob } from '../embedding';
|
||||
import { COPILOT_LOCKER, CopilotType } from '../resolver';
|
||||
import { ChatSessionService } from '../session';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { MAX_EMBEDDABLE_SIZE } from '../types';
|
||||
import { readStream } from '../utils';
|
||||
import { CopilotContextDocJob } from './job';
|
||||
import { CopilotContextService } from './service';
|
||||
|
||||
@InputType()
|
||||
@@ -387,7 +387,7 @@ export class CopilotContextResolver {
|
||||
private readonly models: Models,
|
||||
private readonly mutex: RequestMutex,
|
||||
private readonly context: CopilotContextService,
|
||||
private readonly jobs: CopilotContextDocJob,
|
||||
private readonly jobs: CopilotEmbeddingJob,
|
||||
private readonly storage: CopilotStorage
|
||||
) {}
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ import {
|
||||
ContextFile,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { type EmbeddingClient, getEmbeddingClient } from '../embedding';
|
||||
import { PromptService } from '../prompt';
|
||||
import { CopilotProviderFactory } from '../providers';
|
||||
import { getEmbeddingClient } from './embedding';
|
||||
import { ContextSession } from './session';
|
||||
import type { EmbeddingClient } from './types';
|
||||
|
||||
const CONTEXT_SESSION_KEY = 'context-session';
|
||||
|
||||
@@ -158,14 +157,15 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
const embedding = await this.embeddingClient.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
const chunks = await this.models.copilotWorkspace.matchFileEmbedding(
|
||||
const fileChunks = await this.models.copilotWorkspace.matchFileEmbedding(
|
||||
workspaceId,
|
||||
embedding,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
if (!fileChunks.length) return [];
|
||||
|
||||
return this.embeddingClient.reRank(content, chunks, topK, signal);
|
||||
return this.embeddingClient.reRank(content, fileChunks, topK, signal);
|
||||
}
|
||||
|
||||
async matchWorkspaceDocs(
|
||||
@@ -179,14 +179,16 @@ export class CopilotContextService implements OnApplicationBootstrap {
|
||||
const embedding = await this.embeddingClient.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
const workspace = await this.models.copilotContext.matchWorkspaceEmbedding(
|
||||
embedding,
|
||||
workspaceId,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
const workspaceChunks =
|
||||
await this.models.copilotContext.matchWorkspaceEmbedding(
|
||||
embedding,
|
||||
workspaceId,
|
||||
topK * 2,
|
||||
threshold
|
||||
);
|
||||
if (!workspaceChunks.length) return [];
|
||||
|
||||
return this.embeddingClient.reRank(content, workspace, topK);
|
||||
return this.embeddingClient.reRank(content, workspaceChunks, topK, signal);
|
||||
}
|
||||
|
||||
@OnEvent('workspace.doc.embed.failed')
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
FileChunkSimilarity,
|
||||
Models,
|
||||
} from '../../../models';
|
||||
import { EmbeddingClient } from './types';
|
||||
import { EmbeddingClient } from '../embedding';
|
||||
|
||||
export class ContextSession implements AsyncDisposable {
|
||||
constructor(
|
||||
private readonly client: EmbeddingClient,
|
||||
private readonly client: EmbeddingClient | undefined,
|
||||
private readonly contextId: string,
|
||||
private readonly config: ContextConfig,
|
||||
private readonly models: Models,
|
||||
@@ -204,6 +204,7 @@ export class ContextSession implements AsyncDisposable {
|
||||
scopedThreshold: number = 0.85,
|
||||
threshold: number = 0.5
|
||||
): Promise<FileChunkSimilarity[]> {
|
||||
if (!this.client) return [];
|
||||
const embedding = await this.client.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
@@ -256,6 +257,7 @@ export class ContextSession implements AsyncDisposable {
|
||||
scopedThreshold: number = 0.85,
|
||||
threshold: number = 0.5
|
||||
) {
|
||||
if (!this.client) return [];
|
||||
const embedding = await this.client.getEmbedding(content, signal);
|
||||
if (!embedding) return [];
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user