mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[visual] Avoid showing list environment markup when the selection is within list items (#13461)
GitOrigin-RevId: cadab83774d52dc6c4867fdd7300a1217423b837
This commit is contained in:
parent
afbd143f0f
commit
c16d2d5840
5 changed files with 126 additions and 212 deletions
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '../../utils/tree-operations/ancestors'
|
} from '../../utils/tree-operations/ancestors'
|
||||||
import { getEnvironmentName } from '../../utils/tree-operations/environments'
|
import { getEnvironmentName } from '../../utils/tree-operations/environments'
|
||||||
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
|
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
|
||||||
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
|
||||||
export const ancestorListType = (state: EditorState): string | null => {
|
export const ancestorListType = (state: EditorState): string | null => {
|
||||||
const ancestorNode = ancestorWithType(state, ListEnvironment)
|
const ancestorNode = ancestorWithType(state, ListEnvironment)
|
||||||
|
@ -303,6 +304,21 @@ const toggleListForRange = (
|
||||||
return { range }
|
return { range }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getListItems = (node: SyntaxNode): SyntaxNode[] => {
|
||||||
|
const items: SyntaxNode[] = []
|
||||||
|
|
||||||
|
node.cursor().iterate(nodeRef => {
|
||||||
|
if (nodeRef.type.is('Item')) {
|
||||||
|
items.push(nodeRef.node)
|
||||||
|
}
|
||||||
|
if (nodeRef.type.is('ListEnvironment') && nodeRef.node !== node) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
export const toggleListForRanges =
|
export const toggleListForRanges =
|
||||||
(environment: string) => (view: EditorView) => {
|
(environment: string) => (view: EditorView) => {
|
||||||
view.dispatch(
|
view.dispatch(
|
||||||
|
|
|
@ -48,6 +48,7 @@ import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
|
||||||
import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
|
import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
|
||||||
import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
|
import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
|
||||||
import { FootnoteWidget } from './visual-widgets/footnote'
|
import { FootnoteWidget } from './visual-widgets/footnote'
|
||||||
|
import { getListItems } from '../toolbar/lists'
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
fileTreeManager: {
|
fileTreeManager: {
|
||||||
|
@ -225,7 +226,6 @@ export const atomicDecorations = (options: Options) => {
|
||||||
tree.iterate({
|
tree.iterate({
|
||||||
enter(nodeRef) {
|
enter(nodeRef) {
|
||||||
if (nodeRef.type.is('$Environment')) {
|
if (nodeRef.type.is('$Environment')) {
|
||||||
if (shouldDecorate(state, nodeRef)) {
|
|
||||||
const envName = getUnstarredEnvironmentName(nodeRef.node, state)
|
const envName = getUnstarredEnvironmentName(nodeRef.node, state)
|
||||||
const hideInEnvironmentTypes = ['figure', 'table']
|
const hideInEnvironmentTypes = ['figure', 'table']
|
||||||
if (envName && hideInEnvironmentTypes.includes(envName)) {
|
if (envName && hideInEnvironmentTypes.includes(envName)) {
|
||||||
|
@ -300,7 +300,8 @@ export const atomicDecorations = (options: Options) => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!selectionIntersects(state.selection, begin) &&
|
!selectionIntersects(state.selection, begin) &&
|
||||||
!selectionIntersects(state.selection, end)
|
!selectionIntersects(state.selection, end) &&
|
||||||
|
getListItems(nodeRef.node).length > 0 // not empty
|
||||||
) {
|
) {
|
||||||
decorations.push(
|
decorations.push(
|
||||||
Decoration.replace({
|
Decoration.replace({
|
||||||
|
@ -313,7 +314,6 @@ export const atomicDecorations = (options: Options) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (nodeRef.type.is('BeginEnv')) {
|
} else if (nodeRef.type.is('BeginEnv')) {
|
||||||
// the beginning of an environment, with an environment name argument
|
// the beginning of an environment, with an environment name argument
|
||||||
const envName = getEnvironmentName(nodeRef.node, state)
|
const envName = getEnvironmentName(nodeRef.node, state)
|
||||||
|
|
|
@ -43,47 +43,27 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
|
|
||||||
// create a nested list
|
// create a nested list
|
||||||
cy.get('.cm-line')
|
cy.get('.cm-line')
|
||||||
.eq(2)
|
.eq(1)
|
||||||
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates a nested list inside an indented list', function () {
|
it('creates a nested list inside an indented list', function () {
|
||||||
const content = [
|
const content = [
|
||||||
'\\begin{itemize}',
|
'\\begin{itemize}',
|
||||||
'\\item Test',
|
' \\item Test',
|
||||||
'\\item Test',
|
' \\item Test',
|
||||||
'\\end{itemize}',
|
'\\end{itemize}',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
// create a nested list
|
// create a nested list
|
||||||
cy.get('.cm-line')
|
cy.get('.cm-line')
|
||||||
.eq(2)
|
.eq(1)
|
||||||
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
.type(isMac ? '{cmd}]' : '{ctrl}]')
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates a nested list on Tab at the start of an item', function () {
|
it('creates a nested list on Tab at the start of an item', function () {
|
||||||
|
@ -96,24 +76,14 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
// move to the start of the item and press Tab
|
// move to the start of the item and press Tab
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(1).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').type('{leftArrow}'.repeat(4))
|
cy.get('@line').type('{leftArrow}'.repeat(4))
|
||||||
cy.get('@line').trigger('keydown', {
|
cy.get('@line').trigger('keydown', {
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not creates a nested list on Tab when not at the start of an item', function () {
|
it('does not creates a nested list on Tab when not at the start of an item', function () {
|
||||||
|
@ -126,22 +96,13 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
// focus a line (at the end of a list item) and press Tab
|
// focus a line (at the end of a list item) and press Tab
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(1).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').trigger('keydown', {
|
cy.get('@line').trigger('keydown', {
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test '].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
//
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
' Test ',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes a nested list on Shift-Tab', function () {
|
it('removes a nested list on Shift-Tab', function () {
|
||||||
|
@ -154,41 +115,22 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
// move to the start of the list item and press Tab
|
// move to the start of the list item and press Tab
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(1).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').type('{leftArrow}'.repeat(4))
|
cy.get('@line').type('{leftArrow}'.repeat(4))
|
||||||
cy.get('@line').trigger('keydown', {
|
cy.get('@line').trigger('keydown', {
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
|
|
||||||
// focus the indented line and press Shift-Tab
|
// focus the indented line and press Shift-Tab
|
||||||
cy.get('.cm-line').eq(4).trigger('keydown', {
|
cy.get('.cm-line').eq(1).trigger('keydown', {
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
//
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not remove a top-level nested list on Shift-Tab', function () {
|
it('does not remove a top-level nested list on Shift-Tab', function () {
|
||||||
|
@ -201,21 +143,12 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
// focus a list item and press Shift-Tab
|
// focus a list item and press Shift-Tab
|
||||||
cy.get('.cm-line').eq(2).trigger('keydown', {
|
cy.get('.cm-line').eq(1).trigger('keydown', {
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
//
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' Test',
|
|
||||||
' Test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles up arrow at the start of a list item', function () {
|
it('handles up arrow at the start of a list item', function () {
|
||||||
|
@ -227,7 +160,7 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
].join('\n')
|
].join('\n')
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(1).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
||||||
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
||||||
|
@ -246,7 +179,7 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
].join('\n')
|
].join('\n')
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(1).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
|
||||||
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
|
||||||
|
@ -290,15 +223,12 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
||||||
].join('\n')
|
].join('\n')
|
||||||
mountEditor(content)
|
mountEditor(content)
|
||||||
|
|
||||||
cy.get('.cm-line').eq(2).as('line')
|
cy.get('.cm-line').eq(0).as('line')
|
||||||
cy.get('@line').click()
|
cy.get('@line').click()
|
||||||
cy.get('@line').type('\\ite')
|
cy.get('@line').type('\\ite')
|
||||||
cy.get('@line').type('{enter}')
|
cy.get('@line').type('{enter}')
|
||||||
cy.get('@line').type('second')
|
cy.get('@line').type('second')
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', [' first', ' second'].join(''))
|
||||||
'have.text',
|
|
||||||
['\\begin{itemize}', ' first', ' second', '\\end{itemize}'].join('')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -130,18 +130,10 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||||
|
|
||||||
clickToolbarButton('Bullet List')
|
clickToolbarButton('Bullet List')
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', ' test')
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
//
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' test',
|
|
||||||
'\\end{itemize}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.get('.cm-line').eq(1).type('ing')
|
cy.get('.cm-line').eq(0).type('ing')
|
||||||
cy.get('.cm-line').eq(1).should('have.text', ' testing')
|
cy.get('.cm-line').eq(0).should('have.text', ' testing')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should insert a numbered list', function () {
|
it('should insert a numbered list', function () {
|
||||||
|
@ -150,17 +142,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||||
|
|
||||||
clickToolbarButton('Numbered List')
|
clickToolbarButton('Numbered List')
|
||||||
|
|
||||||
cy.get('.cm-content').should(
|
cy.get('.cm-content').should('have.text', ' test')
|
||||||
'have.text',
|
|
||||||
[
|
|
||||||
//
|
|
||||||
'\\begin{enumerate}',
|
|
||||||
' test',
|
|
||||||
'\\end{enumerate}',
|
|
||||||
].join('')
|
|
||||||
)
|
|
||||||
|
|
||||||
cy.get('.cm-line').eq(1).type('ing')
|
cy.get('.cm-line').eq(0).type('ing')
|
||||||
cy.get('.cm-line').eq(1).should('have.text', ' testing')
|
cy.get('.cm-line').eq(0).should('have.text', ' testing')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -68,25 +68,21 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||||
// select the first autocomplete item
|
// select the first autocomplete item
|
||||||
cy.findByRole('option').eq(0).click()
|
cy.findByRole('option').eq(0).click()
|
||||||
|
|
||||||
cy.get('@first-line').should('have.text', '\\begin{itemize}')
|
cy.get('@first-line')
|
||||||
cy.get('@second-line')
|
|
||||||
.should('have.text', ' ')
|
.should('have.text', ' ')
|
||||||
.find('.ol-cm-item')
|
.find('.ol-cm-item')
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
cy.get('@third-line').should('have.text', '\\end{itemize}')
|
|
||||||
|
|
||||||
cy.get('@second-line').type('test{Enter}test')
|
cy.get('@first-line').type('test{Enter}test')
|
||||||
|
|
||||||
cy.get('@first-line').should('have.text', '\\begin{itemize}')
|
cy.get('@first-line')
|
||||||
|
.should('have.text', ' test')
|
||||||
|
.find('.ol-cm-item')
|
||||||
|
.should('have.length', 1)
|
||||||
cy.get('@second-line')
|
cy.get('@second-line')
|
||||||
.should('have.text', ' test')
|
.should('have.text', ' test')
|
||||||
.find('.ol-cm-item')
|
.find('.ol-cm-item')
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
cy.get('@third-line')
|
|
||||||
.should('have.text', ' test')
|
|
||||||
.find('.ol-cm-item')
|
|
||||||
.should('have.length', 1)
|
|
||||||
cy.get('@fourth-line').should('have.text', '\\end{itemize}')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('finishes a list on Enter in the last item if empty', function () {
|
it('finishes a list on Enter in the last item if empty', function () {
|
||||||
|
@ -95,10 +91,9 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||||
// select the first autocomplete item
|
// select the first autocomplete item
|
||||||
cy.findByRole('option').eq(0).click()
|
cy.findByRole('option').eq(0).click()
|
||||||
|
|
||||||
cy.get('@second-line').type('test{Enter}{Enter}')
|
cy.get('@first-line').type('test{Enter}{Enter}')
|
||||||
|
|
||||||
cy.get('.cm-line')
|
cy.get('@first-line')
|
||||||
.eq(0)
|
|
||||||
.should('have.text', ' test')
|
.should('have.text', ' test')
|
||||||
.find('.ol-cm-item')
|
.find('.ol-cm-item')
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
|
@ -113,18 +108,7 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
|
||||||
cy.findByRole('option').eq(0).click()
|
cy.findByRole('option').eq(0).click()
|
||||||
|
|
||||||
cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}')
|
cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}')
|
||||||
|
cy.get('.cm-content').should('have.text', ' testtest')
|
||||||
const lines = [
|
|
||||||
'\\begin{itemize}',
|
|
||||||
' test',
|
|
||||||
' ',
|
|
||||||
' ',
|
|
||||||
' test',
|
|
||||||
' ',
|
|
||||||
'\\end{itemize}',
|
|
||||||
]
|
|
||||||
|
|
||||||
cy.get('.cm-content').should('have.text', lines.join(''))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
forEach(['textbf', 'textit', 'underline']).it(
|
forEach(['textbf', 'textit', 'underline']).it(
|
||||||
|
|
Loading…
Reference in a new issue