mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
[visual] Add support for description list environments (#13460)
GitOrigin-RevId: d1ddfeed4ba453afa348e57d75fdb3e12d29f5fc
This commit is contained in:
@@ -10,8 +10,10 @@ import {
|
|||||||
ancestorListType,
|
ancestorListType,
|
||||||
toggleListForRanges,
|
toggleListForRanges,
|
||||||
unwrapBulletList,
|
unwrapBulletList,
|
||||||
|
unwrapDescriptionList,
|
||||||
unwrapNumberedList,
|
unwrapNumberedList,
|
||||||
wrapInBulletList,
|
wrapInBulletList,
|
||||||
|
wrapInDescriptionList,
|
||||||
wrapInNumberedList,
|
wrapInNumberedList,
|
||||||
} from './lists'
|
} from './lists'
|
||||||
import { snippet } from '@codemirror/autocomplete'
|
import { snippet } from '@codemirror/autocomplete'
|
||||||
@@ -114,6 +116,8 @@ export const indentDecrease: Command = view => {
|
|||||||
return unwrapBulletList(view)
|
return unwrapBulletList(view)
|
||||||
case 'enumerate':
|
case 'enumerate':
|
||||||
return unwrapNumberedList(view)
|
return unwrapNumberedList(view)
|
||||||
|
case 'description':
|
||||||
|
return unwrapDescriptionList(view)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -136,6 +140,8 @@ export const indentIncrease: Command = view => {
|
|||||||
return wrapInBulletList(view)
|
return wrapInBulletList(view)
|
||||||
case 'enumerate':
|
case 'enumerate':
|
||||||
return wrapInNumberedList(view)
|
return wrapInNumberedList(view)
|
||||||
|
case 'description':
|
||||||
|
return wrapInDescriptionList(view)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,5 +343,7 @@ export const toggleListForRanges =
|
|||||||
|
|
||||||
export const wrapInBulletList = wrapRangesInList('itemize')
|
export const wrapInBulletList = wrapRangesInList('itemize')
|
||||||
export const wrapInNumberedList = wrapRangesInList('enumerate')
|
export const wrapInNumberedList = wrapRangesInList('enumerate')
|
||||||
|
export const wrapInDescriptionList = wrapRangesInList('description')
|
||||||
export const unwrapBulletList = unwrapRangesFromList('itemize')
|
export const unwrapBulletList = unwrapRangesFromList('itemize')
|
||||||
export const unwrapNumberedList = unwrapRangesFromList('enumerate')
|
export const unwrapNumberedList = unwrapRangesFromList('enumerate')
|
||||||
|
export const unwrapDescriptionList = unwrapRangesFromList('description')
|
||||||
|
|||||||
+61
-17
@@ -51,6 +51,9 @@ import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-g
|
|||||||
import {
|
import {
|
||||||
CloseBrace,
|
CloseBrace,
|
||||||
OpenBrace,
|
OpenBrace,
|
||||||
|
CloseBracket,
|
||||||
|
OpenBracket,
|
||||||
|
OptionalArgument,
|
||||||
ShortTextArgument,
|
ShortTextArgument,
|
||||||
TextArgument,
|
TextArgument,
|
||||||
} from '../../lezer-latex/latex.terms.mjs'
|
} from '../../lezer-latex/latex.terms.mjs'
|
||||||
@@ -74,6 +77,7 @@ import {
|
|||||||
validateParsedTable,
|
validateParsedTable,
|
||||||
} from '../../components/table-generator/utils'
|
} from '../../components/table-generator/utils'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import { DescriptionItemWidget } from './visual-widgets/description-item'
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
previewByPath: (path: string) => PreviewPath | null
|
previewByPath: (path: string) => PreviewPath | null
|
||||||
@@ -101,13 +105,17 @@ function decorateArgumentBraces(
|
|||||||
argumentNode: SyntaxNode | null | undefined,
|
argumentNode: SyntaxNode | null | undefined,
|
||||||
start: number,
|
start: number,
|
||||||
decorateEmptyArguments = false,
|
decorateEmptyArguments = false,
|
||||||
endWidget?: WidgetType
|
endWidget?: WidgetType,
|
||||||
|
braceTypes = {
|
||||||
|
open: OpenBrace,
|
||||||
|
close: CloseBrace,
|
||||||
|
}
|
||||||
): Range<Decoration>[] {
|
): Range<Decoration>[] {
|
||||||
if (!argumentNode) {
|
if (!argumentNode) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const openBrace = argumentNode.getChild('OpenBrace')
|
const openBrace = argumentNode.getChild(braceTypes.open)
|
||||||
const closeBrace = argumentNode.getChild('CloseBrace')
|
const closeBrace = argumentNode.getChild(braceTypes.close)
|
||||||
|
|
||||||
if (openBrace && closeBrace) {
|
if (openBrace && closeBrace) {
|
||||||
if (
|
if (
|
||||||
@@ -120,10 +128,9 @@ function decorateArgumentBraces(
|
|||||||
widget: startWidget,
|
widget: startWidget,
|
||||||
}).range(start, openBrace.to),
|
}).range(start, openBrace.to),
|
||||||
|
|
||||||
Decoration.replace({ widget: endWidget }).range(
|
Decoration.replace({
|
||||||
closeBrace.from,
|
widget: endWidget,
|
||||||
closeBrace.to
|
}).range(closeBrace.from, closeBrace.to),
|
||||||
),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +384,7 @@ export const atomicDecorations = (options: Options) => {
|
|||||||
switch (envName) {
|
switch (envName) {
|
||||||
case 'itemize':
|
case 'itemize':
|
||||||
case 'enumerate':
|
case 'enumerate':
|
||||||
|
case 'description':
|
||||||
startListEnvironment(envName)
|
startListEnvironment(envName)
|
||||||
listDepth++
|
listDepth++
|
||||||
break
|
break
|
||||||
@@ -480,6 +488,7 @@ export const atomicDecorations = (options: Options) => {
|
|||||||
switch (envName) {
|
switch (envName) {
|
||||||
case 'itemize':
|
case 'itemize':
|
||||||
case 'enumerate':
|
case 'enumerate':
|
||||||
|
case 'description':
|
||||||
if (currentListEnvironment === envName) {
|
if (currentListEnvironment === envName) {
|
||||||
endListEnvironment()
|
endListEnvironment()
|
||||||
}
|
}
|
||||||
@@ -963,16 +972,51 @@ export const atomicDecorations = (options: Options) => {
|
|||||||
state.sliceDoc(line.from, nodeRef.from)
|
state.sliceDoc(line.from, nodeRef.from)
|
||||||
)
|
)
|
||||||
const from = onlySpaceBeforeNode ? line.from : nodeRef.from
|
const from = onlySpaceBeforeNode ? line.from : nodeRef.from
|
||||||
decorations.push(
|
|
||||||
Decoration.replace({
|
if (currentListEnvironment === 'description') {
|
||||||
widget: new ItemWidget(
|
const argumentNode = nodeRef.node.getChild(OptionalArgument)
|
||||||
currentListEnvironment || 'document',
|
const to = argumentNode ? argumentNode.from : nodeRef.to
|
||||||
currentOrdinal,
|
|
||||||
listDepth
|
const onlySpaceAfterNode =
|
||||||
),
|
!argumentNode &&
|
||||||
}).range(from, nodeRef.to)
|
/^\s*$/.test(state.sliceDoc(nodeRef.to, line.to))
|
||||||
)
|
|
||||||
return false
|
if (!onlySpaceAfterNode) {
|
||||||
|
// decorate the \item command and subsequent whitespace, if there is other content on the line
|
||||||
|
decorations.push(
|
||||||
|
Decoration.replace({
|
||||||
|
widget: new DescriptionItemWidget(listDepth),
|
||||||
|
}).range(from, to)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argumentNode) {
|
||||||
|
// decorate the optional argument
|
||||||
|
const decorateBrackets = shouldDecorate(state, argumentNode)
|
||||||
|
|
||||||
|
decorations.push(
|
||||||
|
...decorateArgumentBraces(
|
||||||
|
new BraceWidget(decorateBrackets ? '' : '['),
|
||||||
|
argumentNode,
|
||||||
|
from,
|
||||||
|
false,
|
||||||
|
new BraceWidget(decorateBrackets ? '' : ']'),
|
||||||
|
{ open: OpenBracket, close: CloseBracket }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decorations.push(
|
||||||
|
Decoration.replace({
|
||||||
|
widget: new ItemWidget(
|
||||||
|
currentListEnvironment || 'document',
|
||||||
|
currentOrdinal,
|
||||||
|
listDepth
|
||||||
|
),
|
||||||
|
}).range(from, nodeRef.to)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (nodeRef.type.is('NewTheoremCommand')) {
|
} else if (nodeRef.type.is('NewTheoremCommand')) {
|
||||||
const result = parseTheoremArguments(state, nodeRef.node)
|
const result = parseTheoremArguments(state, nodeRef.node)
|
||||||
|
|||||||
+4
-1
@@ -49,7 +49,10 @@ const chooseTargetPosition = (
|
|||||||
targetNode = node
|
targetNode = node
|
||||||
} else if (node.type.is('ItemCtrlSeq')) {
|
} else if (node.type.is('ItemCtrlSeq')) {
|
||||||
targetNode = node.parent
|
targetNode = node.parent
|
||||||
} else if (node.type.is('Whitespace')) {
|
} else if (
|
||||||
|
node.type.is('Whitespace') &&
|
||||||
|
node.nextSibling?.type.is('Command')
|
||||||
|
) {
|
||||||
targetNode = node.nextSibling?.firstChild?.firstChild
|
targetNode = node.nextSibling?.firstChild?.firstChild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
indentIncrease,
|
indentIncrease,
|
||||||
} from '../toolbar/commands'
|
} from '../toolbar/commands'
|
||||||
import { createListItem } from '@/features/source-editor/extensions/visual/utils/list-item'
|
import { createListItem } from '@/features/source-editor/extensions/visual/utils/list-item'
|
||||||
|
import { getListType } from '../../utils/tree-operations/lists'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A keymap which provides behaviours for the visual editor,
|
* A keymap which provides behaviours for the visual editor,
|
||||||
@@ -37,7 +38,7 @@ export const visualKeymap = Prec.highest(
|
|||||||
|
|
||||||
if (line.number === endLine.number - 1) {
|
if (line.number === endLine.number - 1) {
|
||||||
// last item line
|
// last item line
|
||||||
if (line.text.trim() === '\\item') {
|
if (/^\\item(\[])?$/.test(line.text.trim())) {
|
||||||
// no content on this line
|
// no content on this line
|
||||||
|
|
||||||
// outside the end of the current list
|
// outside the end of the current list
|
||||||
@@ -85,8 +86,7 @@ export const visualKeymap = Prec.highest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle a list item that isn't at the end of a list
|
// handle a list item that isn't at the end of a list
|
||||||
|
let insert = '\n' + createListItem(state, from)
|
||||||
const insert = '\n' + createListItem(state, from)
|
|
||||||
|
|
||||||
const countWhitespaceAfterPosition = (pos: number) => {
|
const countWhitespaceAfterPosition = (pos: number) => {
|
||||||
const line = state.doc.lineAt(pos)
|
const line = state.doc.lineAt(pos)
|
||||||
@@ -95,9 +95,16 @@ export const visualKeymap = Prec.highest(
|
|||||||
return matches ? matches[1].length : 0
|
return matches ? matches[1].length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// move the cursor past any whitespace on the new line
|
let pos: number
|
||||||
const pos =
|
|
||||||
from + insert.length + countWhitespaceAfterPosition(from)
|
if (getListType(state, listNode) === 'description') {
|
||||||
|
insert = insert.replace(/\\item $/, '\\item[] ')
|
||||||
|
// position the cursor inside the square brackets
|
||||||
|
pos = from + insert.length - 2
|
||||||
|
} else {
|
||||||
|
// move the cursor past any whitespace on the new line
|
||||||
|
pos = from + insert.length + countWhitespaceAfterPosition(from)
|
||||||
|
}
|
||||||
|
|
||||||
handled = true
|
handled = true
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const visualHighlightStyle = syntaxHighlighting(
|
|||||||
{ tag: tags.string, class: 'ol-cm-monospace' },
|
{ tag: tags.string, class: 'ol-cm-monospace' },
|
||||||
{ tag: tags.punctuation, class: 'ol-cm-punctuation' },
|
{ tag: tags.punctuation, class: 'ol-cm-punctuation' },
|
||||||
{ tag: tags.literal, class: 'ol-cm-monospace' },
|
{ tag: tags.literal, class: 'ol-cm-monospace' },
|
||||||
|
{ tag: tags.strong, class: 'ol-cm-strong' },
|
||||||
{
|
{
|
||||||
tag: tags.monospace,
|
tag: tags.monospace,
|
||||||
fontFamily: 'var(--source-font-family)',
|
fontFamily: 'var(--source-font-family)',
|
||||||
@@ -70,6 +71,9 @@ const mainVisualTheme = EditorView.theme({
|
|||||||
fontVariant: 'normal',
|
fontVariant: 'normal',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
},
|
},
|
||||||
|
'.ol-cm-strong': {
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
'.ol-cm-punctuation': {
|
'.ol-cm-punctuation': {
|
||||||
fontFamily: 'var(--source-font-family)',
|
fontFamily: 'var(--source-font-family)',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
@@ -184,7 +188,7 @@ const mainVisualTheme = EditorView.theme({
|
|||||||
'.ol-cm-begin-theorem > .ol-cm-environment-padding:first-of-type': {
|
'.ol-cm-begin-theorem > .ol-cm-environment-padding:first-of-type': {
|
||||||
flex: 0,
|
flex: 0,
|
||||||
},
|
},
|
||||||
'.ol-cm-item': {
|
'.ol-cm-item, .ol-cm-description-item': {
|
||||||
paddingInlineStart: 'calc(var(--list-depth) * 2ch)',
|
paddingInlineStart: 'calc(var(--list-depth) * 2ch)',
|
||||||
},
|
},
|
||||||
'.ol-cm-item::before': {
|
'.ol-cm-item::before': {
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
import { WidgetType } from '@codemirror/view'
|
||||||
|
|
||||||
|
export class DescriptionItemWidget extends WidgetType {
|
||||||
|
constructor(public listDepth: number) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM() {
|
||||||
|
const element = document.createElement('span')
|
||||||
|
element.classList.add('ol-cm-description-item')
|
||||||
|
this.setProperties(element)
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
eq(widget: DescriptionItemWidget) {
|
||||||
|
return widget.listDepth === this.listDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM(element: HTMLElement) {
|
||||||
|
this.setProperties(element)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreEvent(event: Event): boolean {
|
||||||
|
return event.type !== 'mousedown' && event.type !== 'mouseup'
|
||||||
|
}
|
||||||
|
|
||||||
|
setProperties(element: HTMLElement) {
|
||||||
|
element.style.setProperty('--list-depth', String(this.listDepth))
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -18,6 +18,12 @@ export const environments = new Map([
|
|||||||
\\end{array}`,
|
\\end{array}`,
|
||||||
],
|
],
|
||||||
['center', snippet('center')],
|
['center', snippet('center')],
|
||||||
|
[
|
||||||
|
'description',
|
||||||
|
`\\begin{description}
|
||||||
|
\t\\item[$1] $2
|
||||||
|
\\end{description}`,
|
||||||
|
],
|
||||||
['document', snippetNoIndent('document')],
|
['document', snippetNoIndent('document')],
|
||||||
['equation', snippet('equation')],
|
['equation', snippet('equation')],
|
||||||
['equation*', snippet('equation*')],
|
['equation*', snippet('equation*')],
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const LaTeXLanguage = LRLanguage.define({
|
|||||||
'DocumentClass/OptionalArgument/ShortOptionalArg/Normal':
|
'DocumentClass/OptionalArgument/ShortOptionalArg/Normal':
|
||||||
t.attributeValue,
|
t.attributeValue,
|
||||||
'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
|
'DocumentClass/ShortTextArgument/ShortArg/Normal': t.typeName,
|
||||||
|
'ListEnvironment/BeginEnv/OptionalArgument/...': t.monospace,
|
||||||
Number: t.number,
|
Number: t.number,
|
||||||
OpenBrace: t.brace,
|
OpenBrace: t.brace,
|
||||||
CloseBrace: t.brace,
|
CloseBrace: t.brace,
|
||||||
@@ -193,6 +194,7 @@ export const LaTeXLanguage = LRLanguage.define({
|
|||||||
'BareFilePathArgument/SpaceDelimitedLiteralArgContent':
|
'BareFilePathArgument/SpaceDelimitedLiteralArgContent':
|
||||||
t.attributeValue,
|
t.attributeValue,
|
||||||
TrailingContent: t.comment,
|
TrailingContent: t.comment,
|
||||||
|
'Item/OptionalArgument/ShortOptionalArg/...': t.strong,
|
||||||
// TODO: t.strong, t.emphasis
|
// TODO: t.strong, t.emphasis
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ KnownCommand {
|
|||||||
CenteringCtrlSeq
|
CenteringCtrlSeq
|
||||||
} |
|
} |
|
||||||
Item {
|
Item {
|
||||||
ItemCtrlSeq optionalWhitespace?
|
ItemCtrlSeq OptionalArgument? optionalWhitespace?
|
||||||
} |
|
} |
|
||||||
Maketitle {
|
Maketitle {
|
||||||
MaketitleCtrlSeq optionalWhitespace?
|
MaketitleCtrlSeq optionalWhitespace?
|
||||||
|
|||||||
@@ -744,6 +744,7 @@ const otherKnownEnvNames = {
|
|||||||
enumerate: ListEnvName,
|
enumerate: ListEnvName,
|
||||||
itemize: ListEnvName,
|
itemize: ListEnvName,
|
||||||
table: TableEnvName,
|
table: TableEnvName,
|
||||||
|
description: ListEnvName,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const specializeEnvName = (name, terms) => {
|
export const specializeEnvName = (name, terms) => {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export const withinFormattingCommand = (state: EditorState) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListEnvironmentName = 'itemize' | 'enumerate'
|
export type ListEnvironmentName = 'itemize' | 'enumerate' | 'description'
|
||||||
|
|
||||||
export const listDepthForNode = (node: SyntaxNode) => {
|
export const listDepthForNode = (node: SyntaxNode) => {
|
||||||
let depth = 0
|
let depth = 0
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { EditorState } from '@codemirror/state'
|
||||||
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
|
||||||
|
export const getListType = (
|
||||||
|
state: EditorState,
|
||||||
|
listEnvironmentNode: SyntaxNode
|
||||||
|
) => {
|
||||||
|
const beginEnvNameNode = listEnvironmentNode
|
||||||
|
.getChild('BeginEnv')
|
||||||
|
?.getChild('EnvNameGroup')
|
||||||
|
?.getChild('ListEnvName')
|
||||||
|
|
||||||
|
const endEnvNameNode = listEnvironmentNode
|
||||||
|
.getChild('EndEnv')
|
||||||
|
?.getChild('EnvNameGroup')
|
||||||
|
?.getChild('ListEnvName')
|
||||||
|
|
||||||
|
if (beginEnvNameNode && endEnvNameNode) {
|
||||||
|
return state.sliceDoc(beginEnvNameNode.from, beginEnvNameNode.to).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -285,4 +285,32 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
|
|||||||
[' foo', ' bar', ' baz', ' ', ' test'].join('')
|
[' foo', ' bar', ' baz', ' ', ' test'].join('')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('decorates a description list', function () {
|
||||||
|
const content = [
|
||||||
|
'\\begin{description}',
|
||||||
|
'\\item[foo] Bar',
|
||||||
|
'\\item Test',
|
||||||
|
'\\end{description}',
|
||||||
|
].join('\n')
|
||||||
|
mountEditor(content)
|
||||||
|
|
||||||
|
cy.get('.cm-line').eq(1).click()
|
||||||
|
|
||||||
|
cy.get('.cm-content').should('have.text', ['foo Bar', 'Test'].join(''))
|
||||||
|
|
||||||
|
cy.get('.cm-line').eq(1).type('{Enter}baz')
|
||||||
|
|
||||||
|
cy.get('.cm-content').should(
|
||||||
|
'have.text',
|
||||||
|
['foo Bar', 'Test', '[baz] '].join('')
|
||||||
|
)
|
||||||
|
|
||||||
|
cy.get('.cm-line').eq(2).type('{rightArrow}{rightArrow}Test')
|
||||||
|
|
||||||
|
cy.get('.cm-content').should(
|
||||||
|
'have.text',
|
||||||
|
['foo Bar', 'Test', 'baz Test'].join('')
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user