From aca60c02c086551e8179e3a90fafe5e92a6eea6a Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Mon, 11 May 2026 12:33:58 -0400 Subject: [PATCH] Merge pull request #33391 from overleaf/em-bibtex-projection-32449 Use a projected state field for BibTeX entries in the editor GitOrigin-RevId: 5034be8bdc0cb4b9d854135ac117046c1b3750e7 --- .../utils/tree-operations/commands.ts | 4 ++ .../utils/tree-operations/environments.ts | 2 + .../utils/tree-operations/outline.ts | 2 + .../utils/tree-operations/projection.ts | 14 ++++- .../languages/latex/latex-outline.test.ts | 31 +++++++++++ .../source-editor/utils/projection.test.ts | 55 +++++++++++++++++++ 6 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 services/web/test/frontend/features/source-editor/utils/projection.test.ts diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts index 947280b8eb..ba4efd4b37 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts @@ -79,6 +79,7 @@ export const enterNode = ( items.push({ line: state.doc.lineAt(node.from).number, + toLine: state.doc.lineAt(node.to).number, title: commandName, from: node.from, to: node.to, @@ -106,6 +107,7 @@ export const enterNode = ( items.push({ line: state.doc.lineAt(node.from).number, + toLine: state.doc.lineAt(node.to).number, title: commandName, from: node.from, to: node.to, @@ -127,6 +129,7 @@ export const enterNode = ( } items.push({ line: state.doc.lineAt(node.from).number, + toLine: state.doc.lineAt(node.to).number, title: commandName, from: node.from, to: node.to, @@ -180,6 +183,7 @@ export const enterNode = ( items.push({ line: state.doc.lineAt(commandNode.from).number, + toLine: state.doc.lineAt(commandNode.to).number, title: text, from: commandNode.from, to: commandNode.to, diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/environments.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/environments.ts index e09a4a40fe..eae54ea140 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/environments.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/environments.ts @@ -45,6 +45,7 @@ export const enterNode = ( from: envNameNode.from, to: envNameNode.to, line: state.doc.lineAt(envNameNode.from).number, + toLine: state.doc.lineAt(envNameNode.to).number, type: 'usage', raw: state.sliceDoc(node.from, node.to), } @@ -74,6 +75,7 @@ export const enterNode = ( from: envNameNode.from, to: envNameNode.to, line: state.doc.lineAt(envNameNode.from).number, + toLine: state.doc.lineAt(envNameNode.to).number, type: 'definition', raw: state.sliceDoc(node.from, node.to), } diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts index a83d39d01f..8a3b089285 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/outline.ts @@ -155,6 +155,7 @@ export const enterNode = ( const thisNode = { line: state.doc.lineAt(command.from).number, + toLine: state.doc.lineAt(command.to).number, title: getEntryText(state, name), from: command.from, to: command.to, @@ -181,6 +182,7 @@ export const enterNode = ( : '' const thisNode = { line: state.doc.lineAt(beginEnv.from).number, + toLine: state.doc.lineAt(beginEnv.to).number, title, from: beginEnv.from, to: beginEnv.to, diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/projection.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/projection.ts index 55c191784f..89681491e5 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/projection.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/projection.ts @@ -12,6 +12,7 @@ export abstract class ProjectionItem { readonly from: number = 0 readonly to: number = 0 readonly line: number = 0 + readonly toLine: number = 0 } /* eslint-disable no-unused-vars */ @@ -48,9 +49,15 @@ export function updatePosition( const { from, to } = item const newFrom = transaction.changes.mapPos(from) const newTo = transaction.changes.mapPos(to) - const lineNumber = transaction.state.doc.lineAt(newFrom).number + const newLine = transaction.state.doc.lineAt(newFrom).number + const newToLine = transaction.state.doc.lineAt(newTo).number - if (newFrom === from && newTo === to && lineNumber === item.line) { + if ( + newFrom === from && + newTo === to && + newLine === item.line && + newToLine === item.toLine + ) { // Optimisation - if the item hasn't moved, don't create a new object // If items are not immutable this can introduce problems return item @@ -60,7 +67,8 @@ export function updatePosition( ...item, from: newFrom, to: newTo, - line: lineNumber, + line: newLine, + toLine: newToLine, } } diff --git a/services/web/test/frontend/features/source-editor/languages/latex/latex-outline.test.ts b/services/web/test/frontend/features/source-editor/languages/latex/latex-outline.test.ts index 9d5429ec14..75fb4ee004 100644 --- a/services/web/test/frontend/features/source-editor/languages/latex/latex-outline.test.ts +++ b/services/web/test/frontend/features/source-editor/languages/latex/latex-outline.test.ts @@ -94,6 +94,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { level: SECTION_LEVEL, title: 'sec title', line: 2, + toLine: 2, }, { from: 35, @@ -101,6 +102,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { level: SUB_SECTION_LEVEL, title: 'subsec title', line: 4, + toLine: 4, }, ]) }) @@ -127,6 +129,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { level: SECTION_LEVEL, title: 'sec title 1', line: 2, + toLine: 2, }, { from: 37, @@ -134,6 +137,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { level: SECTION_LEVEL, title: 'sec title 2', line: 4, + toLine: 4, }, ]) }) @@ -152,6 +156,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 16, title: 'title ', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -170,6 +175,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 17, title: 'title 1', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -189,6 +195,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 15, title: 'title', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -208,6 +215,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 15, title: 'title', line: 1, + toLine: 1, level: SECTION_LEVEL, }, { @@ -215,6 +223,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 37, title: 'subtitle', line: 2, + toLine: 2, level: SUB_SECTION_LEVEL, }, ]) @@ -229,6 +238,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 15, title: 'title', line: 1, + toLine: 1, level: SECTION_LEVEL, }, { @@ -236,6 +246,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 38, title: 'subtitle', line: 3, + toLine: 3, level: SUB_SECTION_LEVEL, }, ]) @@ -254,6 +265,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 15, title: 'title', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -282,6 +294,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 17, title: 'section', line: 1, + toLine: 1, level: SECTION_LEVEL, }, { @@ -289,6 +302,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 72, title: 'subsubsection', line: 3, + toLine: 3, level: SUB_SUB_SECTION_LEVEL, }, ]) @@ -304,6 +318,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 17, title: 'section', line: 1, + toLine: 1, level: SECTION_LEVEL, }, { @@ -311,6 +326,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 41, title: 'subsection', line: 2, + toLine: 2, level: SUB_SECTION_LEVEL, }, { @@ -318,6 +334,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 71, title: 'subsubsection', line: 3, + toLine: 3, level: SUB_SUB_SECTION_LEVEL, }, ]) @@ -343,6 +360,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 17, title: 'section', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -373,6 +391,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 11, title: 'book', line: 1, + toLine: 1, level: BOOK_LEVEL, }, { @@ -380,6 +399,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 23, title: 'part', line: 2, + toLine: 2, level: PART_LEVEL, }, { @@ -387,6 +407,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 41, title: 'chapter', line: 3, + toLine: 3, level: CHAPTER_LEVEL, }, { @@ -394,6 +415,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 59, title: 'section', line: 4, + toLine: 4, level: SECTION_LEVEL, }, { @@ -401,6 +423,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 83, title: 'subsection', line: 5, + toLine: 5, level: SUB_SECTION_LEVEL, }, { @@ -408,6 +431,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 113, title: 'subsubsection', line: 6, + toLine: 6, level: SUB_SUB_SECTION_LEVEL, }, { @@ -415,6 +439,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 135, title: 'paragraph', line: 7, + toLine: 7, level: PARAGRAPH_LEVEL, }, { @@ -422,6 +447,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 163, title: 'subparagraph', line: 8, + toLine: 8, level: SUB_PARAGRAPH_LEVEL, }, ]) @@ -443,6 +469,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 30, title: 'section', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -466,6 +493,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 113, title: 'The function f(x) = x^2: Properties of x.', line: 1, + toLine: 1, level: SECTION_LEVEL, }, ]) @@ -487,6 +515,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 22, title: 'test', line: 2, + toLine: 2, level: SECTION_LEVEL, }, { @@ -494,6 +523,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 41, title: 'test2', line: 3, + toLine: 3, level: SUB_SECTION_LEVEL, }, ]) @@ -516,6 +546,7 @@ describe('CodeMirror LaTeX-FileOutline', function () { to: 28, title: 'frame title', line: 1, + toLine: 1, level: FRAME_LEVEL, }, ]) diff --git a/services/web/test/frontend/features/source-editor/utils/projection.test.ts b/services/web/test/frontend/features/source-editor/utils/projection.test.ts new file mode 100644 index 0000000000..08e645e722 --- /dev/null +++ b/services/web/test/frontend/features/source-editor/utils/projection.test.ts @@ -0,0 +1,55 @@ +import { expect } from 'chai' +import { EditorState } from '@codemirror/state' +import { + ProjectionItem, + updatePosition, +} from '../../../../../frontend/js/features/source-editor/utils/tree-operations/projection' + +class TestItem extends ProjectionItem {} + +const itemAt = ( + from: number, + to: number, + line: number, + toLine: number +): TestItem => Object.assign(new TestItem(), { from, to, line, toLine }) + +describe('updatePosition', function () { + it('returns the same instance when positions and lines are unchanged', function () { + const state = EditorState.create({ doc: 'first line\nsecond line' }) + const tr = state.update({ changes: { from: 0, to: 0, insert: '' } }) + const item = itemAt(11, 22, 2, 2) + expect(updatePosition(item, tr)).to.equal(item) + }) + + it('refreshes both line and toLine after an upstream insert adds lines', function () { + const state = EditorState.create({ doc: 'first line\nsecond line' }) + const tr = state.update({ + changes: { from: 0, to: 0, insert: 'top\nmiddle\n' }, + }) + // Item originally covered "second line" (lines 2..2) at offsets 11..22. + const item = itemAt(11, 22, 2, 2) + const updated = updatePosition(item, tr) + expect(updated).to.not.equal(item) + expect(updated.from).to.equal(22) + expect(updated.to).to.equal(33) + expect(updated.line).to.equal(4) + expect(updated.toLine).to.equal(4) + }) + + it('refreshes line and toLine independently for multi-line items', function () { + const state = EditorState.create({ + doc: 'a\nbb\nccc\ndddd\neeeee', + }) + // Item covers "bb\nccc" at offsets 2..8, on lines 2..3. + const item = itemAt(2, 8, 2, 3) + const tr = state.update({ + changes: { from: 0, to: 0, insert: 'X\n' }, + }) + const updated = updatePosition(item, tr) + expect(updated.from).to.equal(4) + expect(updated.to).to.equal(10) + expect(updated.line).to.equal(3) + expect(updated.toLine).to.equal(4) + }) +})