Use history snapshot when doing file/project restore (#28502)

* Add getDocUpdaterCompatibleRanges utility function

* use history snapshot for file/project restore

* move overleaf-editor-core from devDependencies

GitOrigin-RevId: 62481a5304ada9d931e018418be3c0719bccf1f3
This commit is contained in:
Domagoj Kriskovic
2025-09-19 08:07:58 +00:00
committed by Copybot
parent a6e9a5c7e9
commit 484a01a173
7 changed files with 707 additions and 206 deletions
@@ -0,0 +1,397 @@
'use strict'
const { expect } = require('chai')
const {
getDocUpdaterCompatibleRanges,
} = require('../lib/doc_updater_compatible_ranges.js')
const StringFileData = require('../lib/file_data/string_file_data.js')
const File = require('../lib/file.js')
describe('getDocUpdaterCompatibleRanges', function () {
describe('with tracked deletes', function () {
beforeEach(function () {
this.content = 'the quick brown fox jumps over the lazy dog'
this.trackedChanges = [
{
range: { pos: 4, length: 6 }, // 'quick '
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 16, length: 4 }, // 'fox '
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 35, length: 5 }, // 'lazy '
tracking: {
type: 'insert',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 40, length: 3 }, // 'dog'
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
]
})
it("doesn't shift the tracked delete by itself", function () {
const fileData = new StringFileData(this.content, [], this.trackedChanges)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.changes[0].op.p).to.eq(4)
})
it('should move subsequent tracked changes by the length of previous deletes', function () {
const fileData = new StringFileData(this.content, [], this.trackedChanges)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.changes[1].op.p).to.eq(16 - 6)
expect(result.changes[2].op.p).to.eq(35 - 6 - 4)
})
it("shouldn't move subsequent tracked changes by previous inserts", function () {
const fileData = new StringFileData(this.content, [], this.trackedChanges)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.changes[3].op.p).to.eq(40 - 6 - 4)
})
})
describe('with comments and tracked deletes', function () {
beforeEach(function () {
this.content = 'the quick brown fox jumps over the lazy dog'
this.trackedChanges = [
{
range: { pos: 2, length: 5 }, // 'e qui'
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 11, length: 1 }, // 'r'
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 28, length: 9 }, // 'er the la'
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
]
})
it('should move the comment to the start of the tracked delete and remove overlapping text', function () {
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 26, length: 4 }, // 'over'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
]
const fileData = new StringFileData(
this.content,
comments,
this.trackedChanges
)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.comments[0].op.p).to.eq(2)
expect(result.comments[0].op.c).to.eq('ck bown fox jumps ovzy')
})
it('should put resolved status in op', function () {
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 26, length: 4 }, // 'over'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
{ id: 'comment-2', ranges: [], resolved: true },
{
id: 'comment-3',
ranges: [{ pos: 4, length: 1 }], // 'q'
resolved: true,
},
]
const fileData = new StringFileData(
this.content,
comments,
this.trackedChanges
)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.comments[0].op.resolved).to.be.false
expect(result.comments[1].op.resolved).to.be.true
expect(result.comments[2].op.resolved).to.be.true
})
it('should include thread id', function () {
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 26, length: 4 }, // 'over'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
{ id: 'comment-2', ranges: [], resolved: true },
{
id: 'comment-3',
ranges: [{ pos: 4, length: 1 }], // 'q'
resolved: true,
},
]
const fileData = new StringFileData(
this.content,
comments,
this.trackedChanges
)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.comments[0].op.t).to.eq('comment-1')
expect(result.comments[1].op.t).to.eq('comment-2')
expect(result.comments[2].op.t).to.eq('comment-3')
})
it('should translate detached comment to zero length op', function () {
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 26, length: 4 }, // 'over'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
{ id: 'comment-2', ranges: [], resolved: true }, // detached comment
{
id: 'comment-3',
ranges: [{ pos: 4, length: 1 }], // 'q'
resolved: true,
},
]
const fileData = new StringFileData(
this.content,
comments,
this.trackedChanges
)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.comments[1].op.p).to.eq(0)
expect(result.comments[1].op.c).to.eq('')
})
it('should position a comment entirely in a tracked delete next to the tracked delete', function () {
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 26, length: 4 }, // 'over'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
{ id: 'comment-2', ranges: [], resolved: true },
{
id: 'comment-3',
ranges: [{ pos: 4, length: 1 }], // 'q' - entirely in tracked delete
resolved: true,
},
]
const fileData = new StringFileData(
this.content,
comments,
this.trackedChanges
)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result.comments[2].op.p).to.eq(2)
expect(result.comments[2].op.c).to.eq('')
})
})
describe('with multiple tracked changes and comments', function () {
it('returns the ranges with content and adjusted positions to ignore tracked deletes', function () {
const content = 'the quick brown fox jumps over the lazy dog'
const trackedChanges = [
{
range: { pos: 4, length: 6 }, // 'quick '
tracking: {
type: 'delete',
userId: '31',
ts: '2023-01-01T00:00:00.000Z',
},
},
{
range: { pos: 10, length: 6 }, // 'brown '
tracking: {
type: 'insert',
userId: '31',
ts: '2024-01-01T00:00:00.000Z',
},
},
{
range: { pos: 35, length: 5 }, // 'lazy '
tracking: {
type: 'delete',
userId: '31',
ts: '2024-01-01T00:00:00.000Z',
},
},
]
const comments = [
{
id: 'comment-1',
ranges: [
{ pos: 4, length: 5 }, // 'quick'
{ pos: 10, length: 5 }, // 'brown'
{ pos: 35, length: 4 }, // 'lazy'
],
resolved: false,
},
{
id: 'comment-2',
ranges: [
{ pos: 0, length: 3 }, // 'the'
{ pos: 31, length: 3 }, // 'the'
],
resolved: true,
},
]
const fileData = new StringFileData(content, comments, trackedChanges)
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result).to.deep.equal({
changes: [
{
metadata: {
ts: '2023-01-01T00:00:00.000Z',
user_id: '31',
},
op: {
d: 'quick ',
p: 4,
},
},
{
metadata: {
ts: '2024-01-01T00:00:00.000Z',
user_id: '31',
},
op: {
i: 'brown ',
p: 4,
},
},
{
metadata: {
ts: '2024-01-01T00:00:00.000Z',
user_id: '31',
},
op: {
d: 'lazy ',
p: 29,
},
},
],
comments: [
{
op: {
c: 'brown fox jumps over the ',
p: 4,
t: 'comment-1',
resolved: false,
},
id: 'comment-1',
},
{
op: {
c: 'the brown fox jumps over the',
p: 0,
t: 'comment-2',
resolved: true,
},
id: 'comment-2',
},
],
})
})
})
describe('with an empty file', function () {
it('should return empty comments and changes', function () {
const fileData = new StringFileData('', [])
const file = new File(fileData)
const result = getDocUpdaterCompatibleRanges(file)
expect(result).to.deep.equal({
changes: [],
comments: [],
})
})
})
})