[visual] Add support for description list environments (#13460)
GitOrigin-RevId: d1ddfeed4ba453afa348e57d75fdb3e12d29f5fc
This commit is contained in:
@@ -10,8 +10,10 @@ import {
|
||||
ancestorListType,
|
||||
toggleListForRanges,
|
||||
unwrapBulletList,
|
||||
unwrapDescriptionList,
|
||||
unwrapNumberedList,
|
||||
wrapInBulletList,
|
||||
wrapInDescriptionList,
|
||||
wrapInNumberedList,
|
||||
} from './lists'
|
||||
import { snippet } from '@codemirror/autocomplete'
|
||||
@@ -114,6 +116,8 @@ export const indentDecrease: Command = view => {
|
||||
return unwrapBulletList(view)
|
||||
case 'enumerate':
|
||||
return unwrapNumberedList(view)
|
||||
case 'description':
|
||||
return unwrapDescriptionList(view)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -136,6 +140,8 @@ export const indentIncrease: Command = view => {
|
||||
return wrapInBulletList(view)
|
||||
case 'enumerate':
|
||||
return wrapInNumberedList(view)
|
||||
case 'description':
|
||||
return wrapInDescriptionList(view)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -343,5 +343,7 @@ export const toggleListForRanges =
|
||||
|
||||
export const wrapInBulletList = wrapRangesInList('itemize')
|
||||
export const wrapInNumberedList = wrapRangesInList('enumerate')
|
||||
export const wrapInDescriptionList = wrapRangesInList('description')
|
||||
export const unwrapBulletList = unwrapRangesFromList('itemize')
|
||||
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 {
|
||||
CloseBrace,
|
||||
OpenBrace,
|
||||
CloseBracket,
|
||||
OpenBracket,
|
||||
OptionalArgument,
|
||||
ShortTextArgument,
|
||||
TextArgument,
|
||||
} from '../../lezer-latex/latex.terms.mjs'
|
||||
@@ -74,6 +77,7 @@ import {
|
||||
validateParsedTable,
|
||||
} from '../../components/table-generator/utils'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { DescriptionItemWidget } from './visual-widgets/description-item'
|
||||
|
||||
type Options = {
|
||||
previewByPath: (path: string) => PreviewPath | null
|
||||
@@ -101,13 +105,17 @@ function decorateArgumentBraces(
|
||||
argumentNode: SyntaxNode | null | undefined,
|
||||
start: number,
|
||||
decorateEmptyArguments = false,
|
||||
endWidget?: WidgetType
|
||||
endWidget?: WidgetType,
|
||||
braceTypes = {
|
||||
open: OpenBrace,
|
||||
close: CloseBrace,
|
||||
}
|
||||
): Range<Decoration>[] {
|
||||
if (!argumentNode) {
|
||||
return []
|
||||
}
|
||||
const openBrace = argumentNode.getChild('OpenBrace')
|
||||
const closeBrace = argumentNode.getChild('CloseBrace')
|
||||
const openBrace = argumentNode.getChild(braceTypes.open)
|
||||
const closeBrace = argumentNode.getChild(braceTypes.close)
|
||||
|
||||
if (openBrace && closeBrace) {
|
||||
if (
|
||||
@@ -120,10 +128,9 @@ function decorateArgumentBraces(
|
||||
widget: startWidget,
|
||||
}).range(start, openBrace.to),
|
||||
|
||||
Decoration.replace({ widget: endWidget }).range(
|
||||
closeBrace.from,
|
||||
closeBrace.to
|
||||
),
|
||||
Decoration.replace({
|
||||
widget: endWidget,
|
||||
}).range(closeBrace.from, closeBrace.to),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -377,6 +384,7 @@ export const atomicDecorations = (options: Options) => {
|
||||
switch (envName) {
|
||||
case 'itemize':
|
||||
case 'enumerate':
|
||||
case 'description':
|
||||
startListEnvironment(envName)
|
||||
listDepth++
|
||||
break
|
||||
@@ -480,6 +488,7 @@ export const atomicDecorations = (options: Options) => {
|
||||
switch (envName) {
|
||||
case 'itemize':
|
||||
case 'enumerate':
|
||||
case 'description':
|
||||
if (currentListEnvironment === envName) {
|
||||
endListEnvironment()
|
||||
}
|
||||
@@ -963,16 +972,51 @@ export const atomicDecorations = (options: Options) => {
|
||||
state.sliceDoc(line.from, nodeRef.from)
|
||||
)
|
||||
const from = onlySpaceBeforeNode ? line.from : nodeRef.from
|
||||
decorations.push(
|
||||
Decoration.replace({
|
||||
widget: new ItemWidget(
|
||||
currentListEnvironment || 'document',
|
||||
currentOrdinal,
|
||||
listDepth
|
||||
),
|
||||
}).range(from, nodeRef.to)
|
||||
)
|
||||
return false
|
||||
|
||||
if (currentListEnvironment === 'description') {
|
||||
const argumentNode = nodeRef.node.getChild(OptionalArgument)
|
||||
const to = argumentNode ? argumentNode.from : nodeRef.to
|
||||
|
||||
const onlySpaceAfterNode =
|
||||
!argumentNode &&
|
||||
/^\s*$/.test(state.sliceDoc(nodeRef.to, line.to))
|
||||
|
||||
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')) {
|
||||
const result = parseTheoremArguments(state, nodeRef.node)
|
||||
|
||||
+4
-1
@@ -49,7 +49,10 @@ const chooseTargetPosition = (
|
||||
targetNode = node
|
||||
} else if (node.type.is('ItemCtrlSeq')) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
indentIncrease,
|
||||
} from '../toolbar/commands'
|
||||
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,
|
||||
@@ -37,7 +38,7 @@ export const visualKeymap = Prec.highest(
|
||||
|
||||
if (line.number === endLine.number - 1) {
|
||||
// last item line
|
||||
if (line.text.trim() === '\\item') {
|
||||
if (/^\\item(\[])?$/.test(line.text.trim())) {
|
||||
// no content on this line
|
||||
|
||||
// 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
|
||||
|
||||
const insert = '\n' + createListItem(state, from)
|
||||
let insert = '\n' + createListItem(state, from)
|
||||
|
||||
const countWhitespaceAfterPosition = (pos: number) => {
|
||||
const line = state.doc.lineAt(pos)
|
||||
@@ -95,9 +95,16 @@ export const visualKeymap = Prec.highest(
|
||||
return matches ? matches[1].length : 0
|
||||
}
|
||||
|
||||
// move the cursor past any whitespace on the new line
|
||||
const pos =
|
||||
from + insert.length + countWhitespaceAfterPosition(from)
|
||||
let pos: number
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const visualHighlightStyle = syntaxHighlighting(
|
||||
{ tag: tags.string, class: 'ol-cm-monospace' },
|
||||
{ tag: tags.punctuation, class: 'ol-cm-punctuation' },
|
||||
{ tag: tags.literal, class: 'ol-cm-monospace' },
|
||||
{ tag: tags.strong, class: 'ol-cm-strong' },
|
||||
{
|
||||
tag: tags.monospace,
|
||||
fontFamily: 'var(--source-font-family)',
|
||||
@@ -70,6 +71,9 @@ const mainVisualTheme = EditorView.theme({
|
||||
fontVariant: 'normal',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'.ol-cm-strong': {
|
||||
fontWeight: 700,
|
||||
},
|
||||
'.ol-cm-punctuation': {
|
||||
fontFamily: 'var(--source-font-family)',
|
||||
lineHeight: 1,
|
||||
@@ -184,7 +188,7 @@ const mainVisualTheme = EditorView.theme({
|
||||
'.ol-cm-begin-theorem > .ol-cm-environment-padding:first-of-type': {
|
||||
flex: 0,
|
||||
},
|
||||
'.ol-cm-item': {
|
||||
'.ol-cm-item, .ol-cm-description-item': {
|
||||
paddingInlineStart: 'calc(var(--list-depth) * 2ch)',
|
||||
},
|
||||
'.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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user