diff --git a/packages/core/src/docs/data-model/json-x/json-x.ts b/packages/core/src/docs/data-model/json-x/json-x.ts index cab21680227..cd8623c18ee 100644 --- a/packages/core/src/docs/data-model/json-x/json-x.ts +++ b/packages/core/src/docs/data-model/json-x/json-x.ts @@ -15,12 +15,12 @@ */ import type { Doc, JSONOp, Path } from 'ot-json1'; -import * as json1 from 'ot-json1'; +import type { Nullable } from '../../../shared'; import type { IDocumentBody, IDocumentData } from '../../../types/interfaces'; +import type { TextXAction } from '../text-x/action-types'; import type { TPriority } from '../text-x/text-x'; +import * as json1 from 'ot-json1'; import { TextX } from '../text-x/text-x'; -import type { TextXAction } from '../text-x/action-types'; -import type { Nullable } from '../../../shared'; export interface ISubType { name: string; @@ -37,7 +37,7 @@ export interface ISubType { [k: string]: any; }; -export { JSONOp as JSONXActions, Path as JSONXPath, json1 as JSON1 }; +export { json1 as JSON1, JSONOp as JSONXActions, Path as JSONXPath }; export class JSONX { // static name = 'json-x'; diff --git a/packages/core/src/docs/data-model/text-x/__tests__/action-iterator.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/action-iterator.spec.ts index dcd7281ec78..34ab6fa929d 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/action-iterator.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/action-iterator.spec.ts @@ -15,9 +15,9 @@ */ import { describe, expect, it } from 'vitest'; +import { BooleanNumber } from '../../../../types/enum/text-style'; import { ActionIterator } from '../action-iterator'; import { TextXActionType } from '../action-types'; -import { BooleanNumber } from '../../../../types/enum/text-style'; describe('Test action iterator', () => { it('test action iterator basic use', () => { diff --git a/packages/core/src/docs/data-model/text-x/__tests__/apply-utils.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/apply-utils.spec.ts index 4b9f50c6a98..09de1fbd93d 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/apply-utils.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/apply-utils.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type { Nullable } from 'vitest'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import type { Nullable } from '../../../../shared'; +import type { IDocumentBody, ITextRun } from '../../../../types/interfaces'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { UpdateDocsAttributeType } from '../../../../shared'; import { BooleanNumber } from '../../../../types/enum'; -import type { IDocumentBody, ITextRun } from '../../../../types/interfaces'; import { deleteParagraphs, deleteTextRuns, insertTextRuns } from '../apply-utils/common'; import { coverTextRuns } from '../apply-utils/update-apply'; @@ -523,10 +523,12 @@ describe('test case in apply utils', () => { 10 ); - expect(body?.textRuns!.length).toBe(3); + expect(body?.textRuns!.length).toBe(4); expect(body?.textRuns![0].ts?.bl).toBe(BooleanNumber.FALSE); expect(body?.textRuns![1].ts?.bl).toBe(BooleanNumber.FALSE); expect(body?.textRuns![2].ts?.bl).toBe(BooleanNumber.FALSE); + expect(body?.textRuns![2].ts?.it).toBe(BooleanNumber.TRUE); + expect(body?.textRuns![3].ts?.bl).toBe(BooleanNumber.FALSE); }); it('If textRuns doesn\'t intersect, they shouldn\'t be merged', async () => { diff --git a/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts new file mode 100644 index 00000000000..b98dba4b107 --- /dev/null +++ b/packages/core/src/docs/data-model/text-x/__tests__/apply.spec.ts @@ -0,0 +1,253 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { IDocumentBody } from '../../../../types/interfaces'; +import type { TextXAction } from '../action-types'; +import { describe, expect, it } from 'vitest'; +import { BooleanNumber } from '../../../../types/enum'; +import { TextXActionType } from '../action-types'; +import { TextX } from '../text-x'; + +function getDefaultDoc() { + const doc: IDocumentBody = { + dataStream: 'w\r\n', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }; + + return doc; +} + +function getDefaultDocWithLength2() { + const doc: IDocumentBody = { + dataStream: 'ww\r\n', + textRuns: [ + { + st: 0, + ed: 2, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }; + + return doc; +} + +describe('apply method', () => { + it('should get the same result when apply two actions by order OR composed first case 1', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'h', + }, + }, + ]; + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.FALSE, + }, + }, + ], + }, + segmentId: '', + }, + ]; + + const doc1 = getDefaultDoc(); + const doc2 = getDefaultDoc(); + const doc3 = getDefaultDoc(); + const doc4 = getDefaultDoc(); + + const resultA = TextX.apply(TextX.apply(doc1, actionsA), TextX.transform(actionsB, actionsA, 'left')); + const resultB = TextX.apply(TextX.apply(doc2, actionsB), TextX.transform(actionsA, actionsB, 'right')); + + const composedAction1 = TextX.compose(actionsA, TextX.transform(actionsB, actionsA, 'left')); + const composedAction2 = TextX.compose(actionsB, TextX.transform(actionsA, actionsB, 'right')); + + const resultC = TextX.apply(doc3, composedAction1); + const resultD = TextX.apply(doc4, composedAction2); + + expect(resultA).toEqual(resultB); + expect(resultC).toEqual(resultD); + expect(resultA).toEqual(resultC); + expect(composedAction1).toEqual(composedAction2); + }); + + // https://github.com/dream-num/univer-pro/issues/2943 + it('should get the same result when apply two actions by order OR composed first case 2', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + segmentId: '', + len: 1, + }, + { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'h', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }, + ]; + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 1, + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.FALSE, + }, + }, + ], + }, + segmentId: '', + }, + ]; + + const doc1 = getDefaultDoc(); + const doc2 = getDefaultDoc(); + const doc3 = getDefaultDoc(); + const doc4 = getDefaultDoc(); + + const resultA = TextX.apply(TextX.apply(doc1, actionsA), TextX.transform(actionsB, actionsA, 'left')); + const resultB = TextX.apply(TextX.apply(doc2, actionsB), TextX.transform(actionsA, actionsB, 'right')); + + const composedAction1 = TextX.compose(actionsA, TextX.transform(actionsB, actionsA, 'left')); + const composedAction2 = TextX.compose(actionsB, TextX.transform(actionsA, actionsB, 'right')); + + const resultC = TextX.apply(doc3, composedAction1); + const resultD = TextX.apply(doc4, composedAction2); + + // console.log(JSON.stringify(resultA, null, 2)); + // console.log(JSON.stringify(resultB, null, 2)); + + // console.log(JSON.stringify(composedAction2, null, 2)); + // console.log(JSON.stringify(resultC, null, 2)); + + expect(resultA).toEqual(resultB); + expect(resultC).toEqual(resultD); + expect(resultA).toEqual(resultC); + expect(composedAction1).toEqual(composedAction2); + }); + + it('should get the same result when apply two actions by order OR composed first case 3', () => { + const actionsA: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + segmentId: '', + len: 1, + }, + { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'h', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }, + ]; + const actionsB: TextXAction[] = [ + { + t: TextXActionType.RETAIN, + len: 2, + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 2, + ts: { + bl: BooleanNumber.FALSE, + }, + }, + ], + }, + segmentId: '', + }, + ]; + + const doc1 = getDefaultDocWithLength2(); + const doc2 = getDefaultDocWithLength2(); + const doc3 = getDefaultDocWithLength2(); + const doc4 = getDefaultDocWithLength2(); + + const resultA = TextX.apply(TextX.apply(doc1, actionsA), TextX.transform(actionsB, actionsA, 'left')); + const resultB = TextX.apply(TextX.apply(doc2, actionsB), TextX.transform(actionsA, actionsB, 'right')); + + const composedAction1 = TextX.compose(actionsA, TextX.transform(actionsB, actionsA, 'left')); + const composedAction2 = TextX.compose(actionsB, TextX.transform(actionsA, actionsB, 'right')); + + const resultC = TextX.apply(doc3, composedAction1); + const resultD = TextX.apply(doc4, composedAction2); + + // console.log(JSON.stringify(resultA, null, 2)); + // console.log(JSON.stringify(resultB, null, 2)); + + // console.log('composedAction1', JSON.stringify(composedAction1, null, 2)); + + // console.log(JSON.stringify(resultC, null, 2)); + + expect(resultA).toEqual(resultB); + expect(resultC).toEqual(resultD); + expect(resultA).toEqual(resultC); + expect(composedAction1).toEqual(composedAction2); + }); +}); diff --git a/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts index 72c721de2b6..05c7f5e4593 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/compose.spec.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; import type { TextXAction } from '../action-types'; -import { TextXActionType } from '../action-types'; +import { describe, expect, it } from 'vitest'; import { BooleanNumber } from '../../../../types/enum/text-style'; +import { TextXActionType } from '../action-types'; import { TextX } from '../text-x'; describe('compose test cases', () => { diff --git a/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts index ee9f80627e8..61f632b1a69 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/invert.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; import type { IDocumentBody } from '../../../../types/interfaces'; -import { BooleanNumber } from '../../../../types/enum'; -import { TextX } from '../text-x'; import type { TextXAction } from '../action-types'; -import { TextXActionType } from '../action-types'; +import { describe, expect, it } from 'vitest'; import { UpdateDocsAttributeType } from '../../../../shared/command-enum'; +import { BooleanNumber } from '../../../../types/enum'; +import { TextXActionType } from '../action-types'; +import { TextX } from '../text-x'; describe('test TextX static methods invert and makeInvertible', () => { it('test TextX static method invert', () => { diff --git a/packages/core/src/docs/data-model/text-x/__tests__/text-x.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/text-x.spec.ts index febfda0ea53..58d585c460b 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/text-x.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/text-x.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; -import { TextX } from '../text-x'; import type { IDocumentBody } from '../../../../types/interfaces/i-document-data'; -import { BooleanNumber } from '../../../../types/enum/text-style'; +import { describe, expect, it } from 'vitest'; import { UpdateDocsAttributeType } from '../../../../shared/command-enum'; +import { BooleanNumber } from '../../../../types/enum/text-style'; import { TextXActionType } from '../action-types'; +import { TextX } from '../text-x'; describe('test TextX methods and branches', () => { describe('test TextX methods', () => { diff --git a/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts index 318557505b6..2191f6bc7d7 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/transform.spec.ts @@ -672,4 +672,235 @@ describe('transform()', () => { expect(TextX._transform(actionsA, actionsB, 'right')).toEqual(expectedActionsWithPriorityFalse); expect(TextX._transform(actionsA, actionsB, 'left')).toEqual(expectedActionsWithPriorityTrue); }); + + it('insert after the retain attributes', () => { + const actionA: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + }, + }]; + + const actionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + const transformedActionA: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + customRanges: [], + customDecorations: [], + }, + }]; + + const transformedActionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + customRanges: [], + customDecorations: [], + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + expect(TextX._transform(actionB, actionA, 'right')).toEqual(transformedActionA); + expect(TextX._transform(actionA, actionB, 'left')).toEqual(transformedActionB); + }); + + it('insert before the retain attributes', () => { + const actionA: TextXAction[] = [{ + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + }, + }]; + + const actionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + const transformedActionA: TextXAction[] = [{ + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + customRanges: [], + customDecorations: [], + }, + }]; + + const transformedActionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + customRanges: [], + customDecorations: [], + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + expect(TextX._transform(actionB, actionA, 'right')).toEqual(transformedActionA); + expect(TextX._transform(actionA, actionB, 'left')).toEqual(transformedActionB); + }); + + it('insert between the retain attributes', () => { + const actionA: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + }, + }]; + + const actionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 2, + segmentId: '', + body: { + dataStream: '', + textRuns: [ + { + st: 0, + ed: 2, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + const transformedActionA: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.INSERT, + len: 1, + line: 0, + body: { + dataStream: 'e', + customRanges: [], + customDecorations: [], + }, + }]; + + const transformedActionB: TextXAction[] = [{ + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + customRanges: [], + customDecorations: [], + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + }, { + t: TextXActionType.RETAIN, + len: 1, + segmentId: '', + body: { + dataStream: '', + customRanges: [], + customDecorations: [], + textRuns: [ + { + st: 0, + ed: 1, + ts: { + bl: BooleanNumber.TRUE, + }, + }, + ], + }, + }]; + + expect(TextX._transform(actionB, actionA, 'right')).toEqual(transformedActionA); + expect(TextX._transform(actionA, actionB, 'left')).toEqual(transformedActionB); + }); }); diff --git a/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts b/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts index 62af9ed16ed..789cf5d7f7b 100644 --- a/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts +++ b/packages/core/src/docs/data-model/text-x/__tests__/utils.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { describe, expect, it } from 'vitest'; import type { IDocumentBody } from '../../../../types/interfaces/i-document-data'; -import { BooleanNumber } from '../../../../types/enum/text-style'; -import { composeBody, getBodySlice, isUselessRetainAction } from '../utils'; import type { IRetainAction } from '../action-types'; +import { describe, expect, it } from 'vitest'; +import { BooleanNumber } from '../../../../types/enum/text-style'; import { TextXActionType } from '../action-types'; +import { composeBody, getBodySlice, isUselessRetainAction } from '../utils'; describe('test text-x utils', () => { it('test getBodySlice fn', () => { diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/common.ts b/packages/core/src/docs/data-model/text-x/apply-utils/common.ts index fd35b573607..8d63885fb6c 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/common.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/common.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -import { horizontalLineSegmentsSubtraction, sortRulesFactory, Tools } from '../../../../shared'; -import { isSameStyleTextRun } from '../../../../shared/compare'; -import { DataStreamTreeTokenType } from '../../types'; import type { Nullable } from '../../../../shared'; import type { ICustomBlock, @@ -28,6 +25,9 @@ import type { ISectionBreak, ITextRun, } from '../../../../types/interfaces'; +import { horizontalLineSegmentsSubtraction, sortRulesFactory, Tools } from '../../../../shared'; +import { isSameStyleTextRun } from '../../../../shared/compare'; +import { DataStreamTreeTokenType } from '../../types'; export function normalizeTextRuns(textRuns: ITextRun[]) { const results: ITextRun[] = []; @@ -78,7 +78,7 @@ export function normalizeTextRuns(textRuns: ITextRun[]) { * @param textLength The length of the inserted content text. * @param currentIndex Determining the index where the content will be inserted into the current content. */ -// eslint-disable-next-line max-lines-per-function + export function insertTextRuns( body: IDocumentBody, insertBody: IDocumentBody, @@ -105,61 +105,32 @@ export function insertTextRuns( for (let i = 0; i < len; i++) { const textRun = textRuns[i]; - const nextRun = textRuns[i + 1]; + // const nextRun = textRuns[i + 1]; const { st, ed } = textRun; - if (ed < currentIndex) { + if (ed <= currentIndex) { newTextRuns.push(textRun); - } else if (currentIndex >= st && currentIndex <= ed) { - // The inline format used to handle no selection will insert a textRun - // with `st` equal to `ed` at the current cursor, - // and when we insert text, the style should follow the new textRun. - if (nextRun && nextRun.st === nextRun.ed && currentIndex === nextRun.st) { - newTextRuns.push(textRun); - continue; - } - - if (!hasInserted) { - hasInserted = true; - textRun.ed += textLength; - - const pendingTextRuns = []; - - if (insertTextRuns.length) { - const startSplitTextRun = { - ...textRun, - st, - ed: insertTextRuns[0].st, - }; - - if (startSplitTextRun.ed > startSplitTextRun.st) { - pendingTextRuns.push(startSplitTextRun); - } + } else if (currentIndex > st && currentIndex < ed) { + hasInserted = true; - pendingTextRuns.push(...insertTextRuns); - - const lastInsertTextRuns = insertTextRuns[insertTextRuns.length - 1]; + const firstSplitTextRun = { + ...textRun, + ed: currentIndex, + }; - const endSplitTextRun = { - ...textRun, - st: lastInsertTextRuns.ed, - ed: ed + textLength, - }; + newTextRuns.push(firstSplitTextRun); - if (endSplitTextRun.ed > endSplitTextRun.st) { - pendingTextRuns.push(endSplitTextRun); - } - } else { - pendingTextRuns.push(textRun); - } + if (insertTextRuns.length) { + newTextRuns.push(...insertTextRuns); + } - newTextRuns.push(...pendingTextRuns); - } else { - textRun.st += textLength; - textRun.ed += textLength; + const lastSplitTextRun = { + ...textRun, + st: currentIndex + textLength, + ed: ed + textLength, + }; - newTextRuns.push(textRun); - } + newTextRuns.push(lastSplitTextRun); } else { // currentIndex < st textRun.st += textLength; @@ -179,8 +150,6 @@ export function insertTextRuns( newTextRuns.push(...insertTextRuns); } - // console.log(JSON.stringify(newTextRuns, null, 2)); - body.textRuns = normalizeTextRuns(newTextRuns); } @@ -534,12 +503,7 @@ export function deleteTextRuns(body: IDocumentBody, textLength: number, currentI ed: ed - startIndex, }); - // https://github.com/dream-num/univer-pro/issues/2044. - if (startIndex === st) { - textRun.ed = st; - } else { - continue; - } + continue; } else if (st <= startIndex && ed >= endIndex) { /** * If the selection range is smaller than the current textRun, @@ -727,6 +691,7 @@ export function deleteTables(body: IDocumentBody, textLength: number, currentInd const endIndex = currentIndex + textLength - 1; const removeTables: ICustomTable[] = []; + if (tables) { const newTables = []; for (let i = 0, len = tables.length; i < len; i++) { @@ -758,6 +723,7 @@ export function deleteTables(body: IDocumentBody, textLength: number, currentInd } body.tables = newTables; } + return removeTables; } @@ -768,6 +734,7 @@ export function deleteCustomRanges(body: IDocumentBody, textLength: number, curr const endIndex = currentIndex + textLength - 1; const removeCustomRanges: ICustomRange[] = []; + if (customRanges) { const newCustomRanges = []; for (let i = 0, len = customRanges.length; i < len; i++) { @@ -803,6 +770,7 @@ export function deleteCustomDecorations(body: IDocumentBody, textLength: number, const startIndex = currentIndex; const endIndex = currentIndex + textLength - 1; const removeCustomDecorations: ICustomDecoration[] = []; + if (customDecorations) { const newCustomDecorations = []; for (let i = 0, len = customDecorations.length; i < len; i++) { @@ -827,5 +795,6 @@ export function deleteCustomDecorations(body: IDocumentBody, textLength: number, } body.customDecorations = newCustomDecorations; } + return removeCustomDecorations; } diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts index 36efceb5134..0b05f262781 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/delete-apply.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { deleteContent } from '../../../../shared'; import type { IDocumentBody } from '../../../../types/interfaces'; +import { deleteContent } from '../../../../shared'; import { deleteCustomBlocks, deleteCustomDecorations, diff --git a/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts b/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts index 26634950164..3e438f3c939 100644 --- a/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply-utils/insert-apply.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import type { IDocumentBody } from '../../../../types/interfaces'; import { insertTextToContent } from '../../../../shared'; import { insertCustomBlocks, @@ -24,7 +25,6 @@ import { insertTables, insertTextRuns, } from './common'; -import type { IDocumentBody } from '../../../../types/interfaces'; export function updateAttributeByInsert( body: IDocumentBody, diff --git a/packages/core/src/docs/data-model/text-x/apply.ts b/packages/core/src/docs/data-model/text-x/apply.ts index c56229a85a7..b93b2e72a3b 100644 --- a/packages/core/src/docs/data-model/text-x/apply.ts +++ b/packages/core/src/docs/data-model/text-x/apply.ts @@ -14,10 +14,10 @@ * limitations under the License. */ +import type { IDocumentBody } from '../../../types/interfaces'; import { MemoryCursor } from '../../../common/memory-cursor'; import { Tools } from '../../../shared'; import { UpdateDocsAttributeType } from '../../../shared/command-enum'; -import type { IDocumentBody } from '../../../types/interfaces'; import { type TextXAction, TextXActionType } from './action-types'; import { updateAttributeByDelete } from './apply-utils/delete-apply'; import { updateAttributeByInsert } from './apply-utils/insert-apply'; @@ -51,6 +51,7 @@ function insertApply( textLength: number, currentIndex: number ) { + // No need to insert empty text. if (textLength === 0) { return; } @@ -64,9 +65,8 @@ export function textXApply(doc: IDocumentBody, actions: TextXAction[]): IDocumen memoryCursor.reset(); actions.forEach((action) => { - // FIXME: @JOCS Since updateApply modifies the action(used in undo/redo), - // so make a deep copy here, does updateApply need to - // be modified to have no side effects in the future? + // Since updateApply modifies the action(used in undo/redo), + // so make a deep copy here. const clonedAction = Tools.deepClone(action); switch (clonedAction.t) { diff --git a/packages/docs-ui/src/basics/paragraph.ts b/packages/docs-ui/src/basics/paragraph.ts index 8d7cef2ac73..b8b66c7a11f 100644 --- a/packages/docs-ui/src/basics/paragraph.ts +++ b/packages/docs-ui/src/basics/paragraph.ts @@ -14,8 +14,22 @@ * limitations under the License. */ -import type { ICustomTable, IParagraph } from '@univerjs/core'; +import type { ICustomTable, IParagraph, ITextRun } from '@univerjs/core'; export function hasParagraphInTable(paragraph: IParagraph, tables: ICustomTable[]) { return tables.some((table) => paragraph.startIndex > table.startIndex && paragraph.startIndex < table.endIndex); } + +export function getTextRunAtPosition(textRuns: ITextRun[], position: number) { + for (let i = textRuns.length - 1; i >= 0; i--) { + const textRun = textRuns[i]; + const { st, ed } = textRun; + if (st === ed && position === st) { + return textRun; + } + + if (position > st && position <= ed) { + return textRun; + } + } +} diff --git a/packages/docs-ui/src/commands/commands/auto-format.command.ts b/packages/docs-ui/src/commands/commands/auto-format.command.ts index 45b683e34d5..d3734ace8a4 100644 --- a/packages/docs-ui/src/commands/commands/auto-format.command.ts +++ b/packages/docs-ui/src/commands/commands/auto-format.command.ts @@ -42,6 +42,7 @@ export const AfterSpaceCommand: ICommand = { async handler(accessor) { const autoFormatService = accessor.get(DocAutoFormatService); const mutations = autoFormatService.onAutoFormat(AfterSpaceCommand.id); + return (await sequenceExecuteAsync(mutations, accessor.get(ICommandService))).result; }, }; @@ -52,6 +53,7 @@ export const EnterCommand: ICommand = { async handler(accessor) { const autoFormatService = accessor.get(DocAutoFormatService); const mutations = autoFormatService.onAutoFormat(EnterCommand.id); + return (await sequenceExecuteAsync(mutations, accessor.get(ICommandService))).result; }, }; diff --git a/packages/docs-ui/src/commands/commands/core-editing.command.ts b/packages/docs-ui/src/commands/commands/core-editing.command.ts index 38ff20f9aec..9a24a83ade4 100644 --- a/packages/docs-ui/src/commands/commands/core-editing.command.ts +++ b/packages/docs-ui/src/commands/commands/core-editing.command.ts @@ -51,10 +51,10 @@ export const InsertCommand: ICommand = { // eslint-disable-next-line max-lines-per-function handler: async (accessor, params: IInsertCommandParams) => { const commandService = accessor.get(ICommandService); - const { range, segmentId, body, unitId, cursorOffset, extendLastRange } = params; const docSelectionManagerService = accessor.get(DocSelectionManagerService); const univerInstanceService = accessor.get(IUniverInstanceService); + const docDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); if (docDataModel == null) { @@ -64,7 +64,7 @@ export const InsertCommand: ICommand = { const activeRange = docSelectionManagerService.getActiveTextRange(); const originBody = docDataModel.getSelfOrHeaderFooterModel(activeRange?.segmentId ?? '').getBody(); - if (!originBody) { + if (originBody == null) { return false; } const actualRange = extendLastRange @@ -104,6 +104,7 @@ export const InsertCommand: ICommand = { } } else { const { dos, retain } = BuildTextUtils.selection.getDeleteActions(actualRange, segmentId, 0, originBody); + textX.push(...dos); doMutation.params.textRanges = [{ startOffset: startOffset + cursorMove + retain, diff --git a/packages/docs-ui/src/commands/commands/ime-input.command.ts b/packages/docs-ui/src/commands/commands/ime-input.command.ts index 828d5831558..e302eea2de7 100644 --- a/packages/docs-ui/src/commands/commands/ime-input.command.ts +++ b/packages/docs-ui/src/commands/commands/ime-input.command.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import type { ICommand, ICommandInfo } from '@univerjs/core'; +import type { DocumentDataModel, ICommand, ICommandInfo } from '@univerjs/core'; import type { IRichTextEditingMutationParams } from '@univerjs/docs'; -import { BuildTextUtils, CommandType, ICommandService, IUniverInstanceService, JSONX, TextX, TextXActionType } from '@univerjs/core'; +import { BuildTextUtils, CommandType, ICommandService, IUniverInstanceService, JSONX, TextX, TextXActionType, UniverInstanceType } from '@univerjs/core'; import { RichTextEditingMutation } from '@univerjs/docs'; import { IRenderManagerService, type ITextRangeWithStyle } from '@univerjs/engine-render'; +import { getTextRunAtPosition } from '../../basics/paragraph'; import { DocIMEInputManagerService } from '../../services/doc-ime-input-manager.service'; import { getRichTextEditPath } from '../util'; @@ -40,10 +41,10 @@ export const IMEInputCommand: ICommand = { const { unitId, newText, oldTextLen, isCompositionEnd, isCompositionStart } = params; const commandService = accessor.get(ICommandService); const renderManagerService = accessor.get(IRenderManagerService); + const univerInstanceService = accessor.get(IUniverInstanceService); const imeInputManagerService = renderManagerService.getRenderById(unitId)?.with(DocIMEInputManagerService); - const univerInstanceService = accessor.get(IUniverInstanceService); - const docDataModel = univerInstanceService.getCurrentUniverDocInstance(); + const docDataModel = univerInstanceService.getUnit(unitId, UniverInstanceType.UNIVER_DOC); if (docDataModel == null || imeInputManagerService == null) { return false; @@ -53,6 +54,7 @@ export const IMEInputCommand: ICommand = { if (!previousActiveRange) { return false; } + const { style, segmentId } = previousActiveRange; const body = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); @@ -84,12 +86,16 @@ export const IMEInputCommand: ICommand = { }, }; + const curTextRun = getTextRunAtPosition(body.textRuns ?? [], startOffset + oldTextLen); + const textX = new TextX(); const jsonX = JSONX.getInstance(); if (!previousActiveRange.collapsed && isCompositionStart) { - const { dos, retain, cursor } = BuildTextUtils.selection.getDeleteActions(previousActiveRange, segmentId, 0, body); + const { dos, retain } = BuildTextUtils.selection.getDeleteActions(previousActiveRange, segmentId, 0, body); + textX.push(...dos); + doMutation.params!.textRanges = [{ startOffset: startOffset + len + retain, endOffset: startOffset + len + retain, @@ -116,6 +122,13 @@ export const IMEInputCommand: ICommand = { t: TextXActionType.INSERT, body: { dataStream: newText, + textRuns: curTextRun + ? [{ + ...curTextRun, + st: 0, + ed: newText.length, + }] + : [], }, len: newText.length, line: 0, diff --git a/packages/docs-ui/src/controllers/render-controllers/doc-ime-input.controller.ts b/packages/docs-ui/src/controllers/render-controllers/doc-ime-input.controller.ts index 1bd3b0f1aac..94dfd9c45ba 100644 --- a/packages/docs-ui/src/controllers/render-controllers/doc-ime-input.controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/doc-ime-input.controller.ts @@ -14,23 +14,21 @@ * limitations under the License. */ +import type { DocumentDataModel, Nullable } from '@univerjs/core'; +import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; +import type { Subscription } from 'rxjs'; +import type { IEditorInputConfig } from '../../services/selection/doc-selection-render.service'; import { Disposable, ICommandService, Inject, - IUniverInstanceService, Tools, } from '@univerjs/core'; -import { DocSkeletonManagerService } from '@univerjs/docs'; -import { IRenderManagerService } from '@univerjs/engine-render'; -import type { DocumentDataModel, Nullable } from '@univerjs/core'; -import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; -import type { Subscription } from 'rxjs'; +import { DocSkeletonManagerService } from '@univerjs/docs'; import { IMEInputCommand } from '../../commands/commands/ime-input.command'; import { DocIMEInputManagerService } from '../../services/doc-ime-input-manager.service'; import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service'; -import type { IEditorInputConfig } from '../../services/selection/doc-selection-render.service'; export class DocIMEInputController extends Disposable implements IRenderModule { private _previousIMEContent: string = ''; @@ -45,10 +43,9 @@ export class DocIMEInputController extends Disposable implements IRenderModule { constructor( private readonly _context: IRenderContext, - @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, - @IRenderManagerService private readonly _renderManagerSrv: IRenderManagerService, @Inject(DocSelectionRenderService) private readonly _docSelectionRenderService: DocSelectionRenderService, @Inject(DocIMEInputManagerService) private readonly _docImeInputManagerService: DocIMEInputManagerService, + @Inject(DocSkeletonManagerService) private readonly _docSkeletonManagerService: DocSkeletonManagerService, @ICommandService private readonly _commandService: ICommandService ) { super(); @@ -105,14 +102,9 @@ export class DocIMEInputController extends Disposable implements IRenderModule { return; } - const documentModel = this._univerInstanceService.getCurrentUniverDocInstance(); - if (!documentModel) { - return; - } + const unitId = this._context.unitId; - const skeleton = this._renderManagerSrv.getRenderById(documentModel.getUnitId()) - ?.with(DocSkeletonManagerService) - .getSkeleton(); + const skeleton = this._docSkeletonManagerService.getSkeleton(); const { event, activeRange } = config; @@ -129,7 +121,7 @@ export class DocIMEInputController extends Disposable implements IRenderModule { } await this._commandService.executeCommand(IMEInputCommand.id, { - unitId: documentModel.getUnitId(), + unitId, newText: content, oldTextLen: this._previousIMEContent.length, isCompositionStart: this._isCompositionStart, diff --git a/packages/docs-ui/src/controllers/render-controllers/doc-input.controller.ts b/packages/docs-ui/src/controllers/render-controllers/doc-input.controller.ts index 62d26b22536..f6da47c868f 100644 --- a/packages/docs-ui/src/controllers/render-controllers/doc-input.controller.ts +++ b/packages/docs-ui/src/controllers/render-controllers/doc-input.controller.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { Disposable, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_ZEN_EDITOR_UNIT_ID_KEY, ICommandService, Inject } from '@univerjs/core'; -import { DocSkeletonManagerService } from '@univerjs/docs'; import type { DocumentDataModel, Nullable } from '@univerjs/core'; import type { IRenderContext, IRenderModule } from '@univerjs/engine-render'; import type { Subscription } from 'rxjs'; +import { Disposable, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY, DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_ZEN_EDITOR_UNIT_ID_KEY, ICommandService, Inject } from '@univerjs/core'; +import { DocSkeletonManagerService } from '@univerjs/docs'; +import { getTextRunAtPosition } from '../../basics/paragraph'; import { AfterSpaceCommand } from '../../commands/commands/auto-format.command'; import { InsertCommand } from '../../commands/commands/core-editing.command'; import { DocSelectionRenderService } from '../../services/selection/doc-selection-render.service'; @@ -53,7 +54,7 @@ export class DocInputController extends Disposable implements IRenderModule { return; } - const unitId = this._context.unitId; + const { unitId } = this._context; const { event, content = '', activeRange } = config; @@ -61,20 +62,31 @@ export class DocInputController extends Disposable implements IRenderModule { const skeleton = this._docSkeletonManagerService.getSkeleton(); - if (e.data == null || skeleton == null) { - return; - } - - if (!skeleton || !activeRange) { + if (e.data == null || skeleton == null || activeRange == null) { return; } const { segmentId } = activeRange; const UNITS = [DOCS_NORMAL_EDITOR_UNIT_ID_KEY, DOCS_ZEN_EDITOR_UNIT_ID_KEY, DOCS_FORMULA_BAR_EDITOR_UNIT_ID_KEY]; + const docDataModel = this._context.unit; + const originBody = docDataModel.getSelfOrHeaderFooterModel(segmentId).getBody(); + + // Insert content's style should follow the text style of the current position. + const curTextRun = getTextRunAtPosition(originBody?.textRuns ?? [], activeRange.endOffset); + await this._commandService.executeCommand(InsertCommand.id, { unitId, body: { dataStream: content, + textRuns: curTextRun + ? [ + { + ...curTextRun, + st: 0, + ed: content.length, + }, + ] + : [], }, range: activeRange, segmentId,