mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[rich text] Cover the whole command when selecting end-to-end (#11683)
GitOrigin-RevId: c3559ce68798047e7001b2a9857f2c168633af6c
This commit is contained in:
parent
7e20d41c4c
commit
7202d7413e
5 changed files with 158 additions and 19 deletions
|
@ -0,0 +1,88 @@
|
|||
import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
|
||||
import { syntaxTree } from '@codemirror/language'
|
||||
import { Tree } from '@lezer/common'
|
||||
import {
|
||||
ancestorOfNodeWithType,
|
||||
descendantsOfNodeWithType,
|
||||
} from '../../utils/tree-operations/ancestors'
|
||||
import { getMousedownSelection, selectionIntersects } from './selection'
|
||||
|
||||
/**
|
||||
* A custom extension that updates the selection in a transaction if the mouse pointer was used
|
||||
* to position a cursor at the start or end of an argument (the cursor is placed inside the brace),
|
||||
* or to drag a range across the whole range of an argument (the selection is placed inside the braces),
|
||||
* when the selection was not already inside the command.
|
||||
*/
|
||||
export const selectDecoratedArgument = EditorState.transactionFilter.of(tr => {
|
||||
if (tr.selection && tr.isUserEvent('select.pointer')) {
|
||||
const tree = syntaxTree(tr.state)
|
||||
let selection = tr.selection
|
||||
const mousedownSelection = getMousedownSelection(tr.state)
|
||||
let replaced = false
|
||||
for (const [index, range] of selection.ranges.entries()) {
|
||||
const replacementRange =
|
||||
selectArgument(tree, range, mousedownSelection, 1) ||
|
||||
selectArgument(tree, range, mousedownSelection, -1)
|
||||
if (replacementRange) {
|
||||
selection = selection.replaceRange(replacementRange, index)
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if (replaced) {
|
||||
return [tr, { selection }]
|
||||
}
|
||||
}
|
||||
|
||||
return tr
|
||||
})
|
||||
|
||||
const selectArgument = (
|
||||
tree: Tree,
|
||||
range: SelectionRange,
|
||||
mousedownSelection: EditorSelection | undefined,
|
||||
side: -1 | 1
|
||||
): SelectionRange | undefined => {
|
||||
const anchor = tree.resolveInner(range.anchor, side)
|
||||
|
||||
const ancestorCommand = ancestorOfNodeWithType(anchor, '$Command')
|
||||
if (!ancestorCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
const mousedownSelectionInside =
|
||||
mousedownSelection !== undefined &&
|
||||
selectionIntersects(mousedownSelection, ancestorCommand)
|
||||
if (mousedownSelectionInside) {
|
||||
return
|
||||
}
|
||||
|
||||
const [inner] = descendantsOfNodeWithType(ancestorCommand, '$TextArgument')
|
||||
if (!inner) {
|
||||
return
|
||||
}
|
||||
|
||||
if (side === 1) {
|
||||
if (
|
||||
range.anchor === inner.from + 1 ||
|
||||
range.anchor === ancestorCommand.from
|
||||
) {
|
||||
if (range.empty) {
|
||||
// selecting at the start
|
||||
return EditorSelection.cursor(inner.from + 1)
|
||||
} else if (Math.abs(range.head - inner.to) < 2) {
|
||||
// selecting from the start to the end
|
||||
return EditorSelection.range(inner.from + 1, inner.to - 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (range.anchor === inner.to - 1 || range.anchor === ancestorCommand.to) {
|
||||
if (range.empty) {
|
||||
// selecting at the end
|
||||
return EditorSelection.cursor(inner.to - 1)
|
||||
} else if (Math.abs(range.head - ancestorCommand.from) < 2) {
|
||||
// selecting from the end to the start
|
||||
return EditorSelection.range(inner.to - 1, inner.from + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
import { EditorSelection, StateEffect, Line, Text } from '@codemirror/state'
|
||||
import {
|
||||
EditorSelection,
|
||||
StateEffect,
|
||||
Line,
|
||||
Text,
|
||||
StateField,
|
||||
EditorState,
|
||||
} from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { hasEffect, updateHasEffect } from '../../utils/effects'
|
||||
|
||||
|
@ -61,7 +68,7 @@ export const updateHasMouseDownEffect = updateHasEffect(mouseDownEffect)
|
|||
* A listener for mousedown and mouseup events, dispatching an event
|
||||
* to record the current mousedown status, which is stored in a state field.
|
||||
*/
|
||||
export const mouseDownListener = EditorView.domEventHandlers({
|
||||
const mouseDownListener = EditorView.domEventHandlers({
|
||||
mousedown: (event, view) => {
|
||||
// not wrapped in a timeout, so update listeners know that the mouse is down before they process the selection
|
||||
view.dispatch({
|
||||
|
@ -77,3 +84,28 @@ export const mouseDownListener = EditorView.domEventHandlers({
|
|||
})
|
||||
},
|
||||
})
|
||||
|
||||
const mousedownSelectionState = StateField.define<EditorSelection | undefined>({
|
||||
create() {
|
||||
return undefined
|
||||
},
|
||||
update(value, tr) {
|
||||
if (value && tr.docChanged) {
|
||||
value = value.map(tr.changes)
|
||||
}
|
||||
|
||||
for (const effect of tr.effects) {
|
||||
// store the previous selection on mousedown
|
||||
if (effect.is(mouseDownEffect)) {
|
||||
value = effect.value ? tr.startState.selection : undefined
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
})
|
||||
|
||||
export const getMousedownSelection = (state: EditorState) =>
|
||||
state.field(mousedownSelectionState)
|
||||
|
||||
export const mousedown = [mouseDownListener, mousedownSelectionState]
|
||||
|
|
|
@ -12,7 +12,7 @@ import { markDecorations } from './mark-decorations'
|
|||
import { EditorView, ViewPlugin } from '@codemirror/view'
|
||||
import { visualKeymap } from './visual-keymap'
|
||||
import { skipPreambleWithCursor } from './skip-preamble-cursor'
|
||||
import { mouseDownEffect, mouseDownListener } from './selection'
|
||||
import { mousedown, mouseDownEffect } from './selection'
|
||||
import { findEffect } from '../../utils/effects'
|
||||
import { forceParsing, syntaxTree } from '@codemirror/language'
|
||||
import { hasLanguageLoadedEffect } from '../language'
|
||||
|
@ -23,6 +23,7 @@ import { listItemMarker } from './list-item-marker'
|
|||
import { figureModalPasteHandler } from '../figure-modal'
|
||||
import { isSplitTestEnabled } from '../../../../utils/splitTestUtils'
|
||||
import { toolbarPanel } from '../toolbar/toolbar-panel'
|
||||
import { selectDecoratedArgument } from './select-decorated-argument'
|
||||
|
||||
type Options = {
|
||||
visual: boolean
|
||||
|
@ -192,7 +193,7 @@ const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
|
|||
const extension = (options: Options) => [
|
||||
visualTheme,
|
||||
visualHighlightStyle,
|
||||
mouseDownListener,
|
||||
mousedown,
|
||||
listItemMarker,
|
||||
markDecorations,
|
||||
atomicDecorations(options),
|
||||
|
@ -200,6 +201,7 @@ const extension = (options: Options) => [
|
|||
visualKeymap,
|
||||
scrollJumpAdjuster,
|
||||
isSplitTestEnabled('source-editor-toolbar') ? [] : toolbarPanel(),
|
||||
selectDecoratedArgument,
|
||||
showContentWhenParsed,
|
||||
figureModalPasteHandler(),
|
||||
]
|
||||
|
|
|
@ -111,8 +111,16 @@ export const LaTeXLanguage = LRLanguage.define({
|
|||
}
|
||||
} else if (Tokens.envName.includes(type.name)) {
|
||||
types.push('$EnvName')
|
||||
} else if (type.name.endsWith('Command')) {
|
||||
types.push('$Command')
|
||||
} else if (type.name.endsWith('Argument')) {
|
||||
types.push('$Argument')
|
||||
if (
|
||||
type.name.endsWith('TextArgument') ||
|
||||
type.is('SectioningArgument')
|
||||
) {
|
||||
types.push('$TextArgument')
|
||||
}
|
||||
} else if (type.name.endsWith('Environment')) {
|
||||
types.push('$Environment')
|
||||
} else if (type.name.endsWith('Brace')) {
|
||||
|
|
|
@ -154,6 +154,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(0).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -164,12 +167,8 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
clickToolbarButton('Bullet List')
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -187,6 +186,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(0).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -197,12 +199,8 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(0).click()
|
||||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-line').eq(0).click()
|
||||
|
||||
cy.get('.cm-content').should('have.text', 'test')
|
||||
})
|
||||
|
||||
|
@ -212,6 +210,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -227,23 +228,26 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Increase Indent')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
' \\begin{enumerate}',
|
||||
' test',
|
||||
' \\end{enumerate}',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(2).click()
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-line').eq(0).type('{upArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -262,6 +266,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
|
@ -277,24 +284,26 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
|||
|
||||
clickToolbarButton('Increase Indent')
|
||||
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
[
|
||||
//
|
||||
'\\begin{enumerate}',
|
||||
' test',
|
||||
' \\begin{enumerate}',
|
||||
' test',
|
||||
' \\end{enumerate}',
|
||||
'\\end{enumerate}',
|
||||
].join('')
|
||||
)
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
cy.get('.cm-line').eq(0).click()
|
||||
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
// expose the markup
|
||||
cy.get('.cm-line').eq(1).type('{rightArrow}')
|
||||
|
||||
cy.get('.cm-content').should(
|
||||
'have.text',
|
||||
|
|
Loading…
Reference in a new issue