[overleaf-editor-core] Restructure TextOperation hierachy (#16582)

* [overleaf-editor-core] Restructure TextOperation hierachy

Restructures the hierachy of TextOperations to include a superclass
EditOperation. This superclass will later on contain other classes used
for tracked changes and comments.

* [overleaf-editor-core] Update json format of LazyStringFileData

* [history-v1+project-history] Fix TextOperation.fromJSON calls

* [overleaf-editor-core] Change EditOperationBuilder.fromRaw to fromJSON

* [overleaf-editor-core] Update apply and invert functions to accept FileData

* [overleaf-editor-core] Pass missing argument to store method

* [overleaf-editor-core] Remove unused method

* [overleaf-editor-core] User EditOperationTransformer

* [overleaf-editor-core] Clean up JSDoc comments

* [overleaf-editor-core] Add tests for EditOperation

* [overleaf-editor-core] Update JSDoc types

GitOrigin-RevId: 9c22a3a89b8483bdb87b43f329ddbdd887ffed42
This commit is contained in:
Mathias Jakobsen
2024-01-24 09:04:18 +00:00
committed by Copybot
parent fc90db231c
commit 43b2fe4a3a
21 changed files with 457 additions and 179 deletions
@@ -1,5 +1,9 @@
// @ts-check
'use strict'
/**
* @typedef {import('../operation/edit_operation')} EditOperation
*/
const assert = require('check-types').assert
const FileData = require('./')
@@ -44,9 +48,12 @@ class HollowStringFileData extends FileData {
return this
}
/** @inheritdoc */
edit(textOperation) {
this.stringLength = textOperation.applyToLength(this.stringLength)
/**
* @inheritdoc
* @param {EditOperation} operation
*/
edit(operation) {
this.stringLength = operation.applyToLength(this.stringLength)
}
}
@@ -15,6 +15,7 @@ let StringFileData = null
/**
* @typedef {import("../types").BlobStore} BlobStore
* @typedef {import("../operation/edit_operation")} EditOperation
* @typedef {import("../types").CommentRawData} CommentRawData
*/
@@ -62,33 +63,52 @@ class FileData {
throw new Error('FileData: toRaw not implemented')
}
/** @see File#getHash */
/**
* @see File#getHash
* @return {string | null | undefined}
*/
getHash() {
return null
}
/** @see File#getContent */
/**
* @see File#getContent
* @return {string | null | undefined}
*/
getContent() {
return null
}
/** @see File#isEditable */
/**
* @see File#isEditable
* @return {boolean | null | undefined} null if it is not currently known
*/
isEditable() {
return null
}
/** @see File#getByteLength */
/**
* @see File#getByteLength
* @return {number | null | undefined}
*/
getByteLength() {
return null
}
/** @see File#getStringLength */
/**
* @see File#getStringLength
* @return {number | null | undefined}
*/
getStringLength() {
return null
}
/** @see File#edit */
edit(textOperation) {
/**
* @see File#edit
* @param {EditOperation} editOperation
*/
edit(editOperation) {
throw new Error('edit not implemented for ' + JSON.stringify(this))
}
@@ -1,3 +1,4 @@
// @ts-check
'use strict'
const _ = require('lodash')
@@ -6,40 +7,41 @@ const assert = require('check-types').assert
const Blob = require('../blob')
const FileData = require('./')
const EagerStringFileData = require('./string_file_data')
const TextOperation = require('../operation/text_operation')
const EditOperation = require('../operation/edit_operation')
const EditOperationBuilder = require('../operation/edit_operation_builder')
class LazyStringFileData extends FileData {
/**
* @param {string} hash
* @param {number} stringLength
* @param {Array.<TextOperation>} [textOperations]
* @param {Array.<EditOperation>} [operations]
* @see FileData
*/
constructor(hash, stringLength, textOperations) {
constructor(hash, stringLength, operations) {
super()
assert.match(hash, Blob.HEX_HASH_RX)
assert.greaterOrEqual(stringLength, 0)
assert.maybe.array.of.instance(textOperations, TextOperation)
assert.maybe.array.of.instance(operations, EditOperation)
this.hash = hash
this.stringLength = stringLength
this.textOperations = textOperations || []
this.operations = operations || []
}
static fromRaw(raw) {
return new LazyStringFileData(
raw.hash,
raw.stringLength,
raw.textOperations && _.map(raw.textOperations, TextOperation.fromJSON)
raw.operations && _.map(raw.operations, EditOperationBuilder.fromJSON)
)
}
/** @inheritdoc */
toRaw() {
const raw = { hash: this.hash, stringLength: this.stringLength }
if (this.textOperations.length) {
raw.textOperations = _.map(this.textOperations, function (textOperation) {
return textOperation.toJSON()
if (this.operations.length) {
raw.operations = _.map(this.operations, function (operation) {
return operation.toJSON()
})
}
return raw
@@ -47,7 +49,7 @@ class LazyStringFileData extends FileData {
/** @inheritdoc */
getHash() {
if (this.textOperations.length) return null
if (this.operations.length) return null
return this.hash
}
@@ -76,16 +78,21 @@ class LazyStringFileData extends FileData {
* Get the cached text operations that are to be applied to this file to get
* from the content with its last known hash to its latest content.
*
* @return {Array.<TextOperation>}
* @return {Array.<EditOperation>}
*/
getTextOperations() {
return this.textOperations
getOperations() {
return this.operations
}
/** @inheritdoc */
/**
* @inheritdoc
* @returns {Promise<EagerStringFileData>}
*/
async toEager(blobStore) {
const content = await blobStore.getString(this.hash)
return new EagerStringFileData(computeContent(this.textOperations, content))
const file = new EagerStringFileData(content)
applyOperations(this.operations, file)
return file
}
/** @inheritdoc */
@@ -99,33 +106,30 @@ class LazyStringFileData extends FileData {
}
/** @inheritdoc */
edit(textOperation) {
this.stringLength = textOperation.applyToLength(this.stringLength)
this.textOperations.push(textOperation)
edit(operation) {
this.stringLength = operation.applyToLength(this.stringLength)
this.operations.push(operation)
}
/** @inheritdoc */
async store(blobStore) {
if (this.textOperations.length === 0) {
if (this.operations.length === 0) {
return { hash: this.hash }
}
const content = await blobStore.getString(this.hash)
const blob = await blobStore.putString(
computeContent(this.textOperations, content)
)
this.hash = blob.getHash()
this.stringLength = blob.getStringLength()
this.textOperations.length = 0
return { hash: this.hash }
const eager = await this.toEager(blobStore)
this.operations.length = 0
return eager.store(blobStore)
}
}
function computeContent(textOperations, initialFile) {
function applyTextOperation(content, textOperation) {
return textOperation.apply(content)
}
return _.reduce(textOperations, applyTextOperation, initialFile)
/**
*
* @param {EditOperation[]} operations
* @param {EagerStringFileData} file
* @returns {void}
*/
function applyOperations(operations, file) {
_.each(operations, operation => operation.apply(file))
}
module.exports = LazyStringFileData
@@ -1,3 +1,4 @@
// @ts-check
'use strict'
const assert = require('check-types').assert
@@ -7,6 +8,8 @@ const CommentList = require('./comment_list')
/**
* @typedef {import("../types").StringFileRawData} StringFileRawData
* @typedef {import("../operation/edit_operation")} EditOperation
* @typedef {import("../types").BlobStore} BlobStore
* @typedef {import("../types").CommentRawData} CommentRawData
*/
@@ -65,9 +68,11 @@ class StringFileData extends FileData {
return this.content.length
}
/** @inheritdoc */
edit(textOperation) {
this.content = textOperation.apply(this.content)
/**
* @inheritdoc
* @param {EditOperation} operation */
edit(operation) {
operation.apply(this)
}
/** @inheritdoc */
@@ -75,7 +80,10 @@ class StringFileData extends FileData {
return this.comments.getComments()
}
/** @inheritdoc */
/**
* @inheritdoc
* @returns {Promise<StringFileData>}
*/
async toEager() {
return this
}
@@ -85,7 +93,10 @@ class StringFileData extends FileData {
return FileData.createHollow(this.getByteLength(), this.getStringLength())
}
/** @inheritdoc */
/**
* @inheritdoc
* @param {BlobStore} blobStore
*/
async store(blobStore) {
const blob = await blobStore.putString(this.content)
return { hash: blob.getHash() }