From 7bb4427f930c91180688c77908abe2efa40f1157 Mon Sep 17 00:00:00 2001 From: Malik Glossop Date: Wed, 18 Mar 2026 10:04:12 +0100 Subject: [PATCH] Merge pull request #32114 from overleaf/mg-context-menu-cursor-movemenet Move cursor on right-click within same line GitOrigin-RevId: 8b622e9f557ecb1a33b7ba1a80d5752e05a72718 --- .../source-editor/extensions/context-menu.ts | 10 ++-- .../codemirror-editor-context-menu.spec.tsx | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/extensions/context-menu.ts b/services/web/frontend/js/features/source-editor/extensions/context-menu.ts index b4a0aa66e9..d8b9d24399 100644 --- a/services/web/frontend/js/features/source-editor/extensions/context-menu.ts +++ b/services/web/frontend/js/features/source-editor/extensions/context-menu.ts @@ -153,13 +153,11 @@ function isPositionInsideSelection(pos: number, from: number, to: number) { function isPositionInsideAnyRangeOrCursor(view: EditorView, pos: number) { for (const range of view.state.selection.ranges) { - // If it's a cursor, treat a right-click anywhere on the same line as "inside". - // This avoids collapsing multi-cursor selections when right-clicking on blank lines - // or to the right of the caret. + // If it's a cursor (not a selection), only treat it as "inside" when + // right-clicking exactly on the cursor position. This allows cursor + // movement when clicking elsewhere on the same line. if (range.from === range.to) { - const clickedLine = view.state.doc.lineAt(pos) - const cursorLine = view.state.doc.lineAt(range.from) - if (clickedLine.number === cursorLine.number) { + if (pos === range.from) { return true } continue diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx index 8c9eccec9d..816526e9f8 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx @@ -162,6 +162,61 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.findByRole('menu').should('not.exist') }) + it('should move the cursor when right-clicking a different position on the same line', function () { + grantClipboardPermissions() + + const pasteContent = 'XX' + const scope = mockScope() + + cy.mount( + + + + + + ) + + // Stub clipboard to return known content for pasting + cy.window().then(win => { + const getTypeStub = cy.stub() + getTypeStub + .withArgs('text/plain') + .resolves(new Blob([pasteContent], { type: 'text/plain' })) + + cy.stub(win.navigator.clipboard, 'read').resolves([ + { + types: ['text/plain'], + getType: getTypeStub, + }, + ]) + cy.stub(win.navigator.clipboard, 'readText').resolves(pasteContent) + }) + + cy.get('.cm-line').eq(16).as('line') + cy.get('@line').click() + cy.get('@line').type('aaaa bbbb') + + // Right-click the left side of the line — opens context menu with cursor near start + cy.get('@line').rightclick('left') + cy.findByRole('menu').should('be.visible') + + // Right-click the right side of the same line while menu is still open — + // cursor should move to the end of the line + cy.get('@line').rightclick('right') + cy.findByRole('menu').should('be.visible') + + // Paste via context menu — content goes wherever the cursor is + cy.findByRole('menu').within(() => { + cy.findByRole('menuitem', { name: pasteLabelMatcher }).click() + }) + + // If cursor moved: "aaaa bbbbXX" (pasted at end) + cy.get('@line').should($line => { + const text = $line.text() + expect(text).to.equal('aaaa bbbb' + pasteContent) + }) + }) + it('should should close when clicking outside the editor', function () { const scope = mockScope() const outsideEditorButtonName = 'Recompile'