mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Add Cmd+Shift+X strikeout shortcut to the visual markdown editor
GitOrigin-RevId: d76fce965c0bffd393f5ca5176d3b61453ac2f93
This commit is contained in:
committed by
Copybot
parent
a8d888ea0c
commit
74609f74ed
@@ -17,6 +17,12 @@ export const shortcuts = () => {
|
||||
preventDefault: true,
|
||||
run: toggleWrapRanges('_', '_', 'Emphasis'),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-Shift-x',
|
||||
mac: 'Mod-Shift-x',
|
||||
preventDefault: true,
|
||||
run: toggleWrapRanges('~~', '~~', 'Strikethrough', 'StrikethroughMark'),
|
||||
},
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
+50
-30
@@ -5,48 +5,64 @@ import { SyntaxNode } from '@lezer/common'
|
||||
import { ancestorOfNodeWithType } from '../../utils/tree-query'
|
||||
import { wrapRanges } from '../../commands/ranges'
|
||||
|
||||
// Bold parses as `StrongEmphasis`, italic as `Emphasis`; in both cases the
|
||||
// delimiters are `EmphasisMark` children of that node.
|
||||
type EmphasisNodeType = 'StrongEmphasis' | 'Emphasis'
|
||||
// Bold parses as `StrongEmphasis`, italic as `Emphasis`, strikethrough as
|
||||
// `Strikethrough`; their delimiters are `EmphasisMark`/`StrikethroughMark`.
|
||||
type InlineNodeType = 'StrongEmphasis' | 'Emphasis' | 'Strikethrough'
|
||||
type DelimiterMarkType = 'EmphasisMark' | 'StrikethroughMark'
|
||||
|
||||
// The enclosing emphasis node of the requested type that fully contains the
|
||||
// range, or null. The exact type is matched so bold and italic aren't confused.
|
||||
const enclosingEmphasis = (
|
||||
// Rebuild a range at new offsets while keeping the user's selection direction
|
||||
const moveRange = (range: SelectionRange, newFrom: number, newTo: number) => {
|
||||
const forwards = range.from === range.anchor
|
||||
return forwards
|
||||
? EditorSelection.range(newFrom, newTo)
|
||||
: EditorSelection.range(newTo, newFrom)
|
||||
}
|
||||
|
||||
// The enclosing node of the requested type that fully contains the range, or
|
||||
// null. The exact type is matched so bold, italic and strikethrough aren't
|
||||
// confused with one another.
|
||||
const enclosingNode = (
|
||||
view: EditorView,
|
||||
range: SelectionRange,
|
||||
nodeType: EmphasisNodeType
|
||||
nodeType: InlineNodeType
|
||||
): SyntaxNode | null => {
|
||||
const tree = ensureSyntaxTree(view.state, range.to, 1000)
|
||||
if (!tree) {
|
||||
return null
|
||||
}
|
||||
|
||||
const node = tree.resolveInner(range.from, range.empty ? 0 : 1)
|
||||
const emphasis = ancestorOfNodeWithType(node, nodeType)
|
||||
if (!emphasis) {
|
||||
// For a collapsed cursor, try both sides so a cursor touching either
|
||||
// delimiter boundary still resolves into the node rather than past it.
|
||||
const resolveEnclosing = (side: -1 | 1) =>
|
||||
ancestorOfNodeWithType(tree.resolveInner(range.from, side), nodeType)
|
||||
const enclosing = range.empty
|
||||
? (resolveEnclosing(-1) ?? resolveEnclosing(1))
|
||||
: resolveEnclosing(1)
|
||||
if (!enclosing) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (range.from < emphasis.from || range.to > emphasis.to) {
|
||||
if (range.from < enclosing.from || range.to > enclosing.to) {
|
||||
return null
|
||||
}
|
||||
|
||||
return emphasis
|
||||
return enclosing
|
||||
}
|
||||
|
||||
const emphasisMarks = (
|
||||
node: SyntaxNode
|
||||
const delimiterMarks = (
|
||||
node: SyntaxNode,
|
||||
markType: DelimiterMarkType
|
||||
): { open: SyntaxNode; close: SyntaxNode } | null => {
|
||||
let open: SyntaxNode | null = null
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.type.is('EmphasisMark')) {
|
||||
if (child.type.is(markType)) {
|
||||
open = child
|
||||
break
|
||||
}
|
||||
}
|
||||
let close: SyntaxNode | null = null
|
||||
for (let child = node.lastChild; child; child = child.prevSibling) {
|
||||
if (child.type.is('EmphasisMark')) {
|
||||
if (child.type.is(markType)) {
|
||||
close = child
|
||||
break
|
||||
}
|
||||
@@ -62,7 +78,8 @@ const emphasisMarks = (
|
||||
const innerMarksToStrip = (
|
||||
view: EditorView,
|
||||
range: SelectionRange,
|
||||
nodeType: EmphasisNodeType
|
||||
nodeType: InlineNodeType,
|
||||
markType: DelimiterMarkType
|
||||
): { from: number; to: number }[] => {
|
||||
const result: { from: number; to: number }[] = []
|
||||
if (range.empty) {
|
||||
@@ -81,7 +98,7 @@ const innerMarksToStrip = (
|
||||
node.from >= range.from &&
|
||||
node.to <= range.to
|
||||
) {
|
||||
const marks = emphasisMarks(node.node)
|
||||
const marks = delimiterMarks(node.node, markType)
|
||||
if (marks) {
|
||||
result.push({ from: marks.open.from, to: marks.open.to })
|
||||
result.push({ from: marks.close.from, to: marks.close.to })
|
||||
@@ -93,14 +110,16 @@ const innerMarksToStrip = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle a markdown inline emphasis delimiter (bold or italic). Unwraps when the
|
||||
* cursor/selection is already inside a span of that type, flattens any same-type
|
||||
* spans contained in the selection, and otherwise wraps via `wrapRanges`.
|
||||
* Toggle a markdown inline delimiter (bold, italic or strikethrough). Unwraps
|
||||
* when the cursor/selection is already inside a span of that type, flattens any
|
||||
* same-type spans contained in the selection, and otherwise wraps via
|
||||
* `wrapRanges`.
|
||||
*/
|
||||
export const toggleWrapRanges = (
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
nodeType: EmphasisNodeType
|
||||
nodeType: InlineNodeType,
|
||||
markType: DelimiterMarkType = 'EmphasisMark'
|
||||
) => {
|
||||
const wrap = wrapRanges(prefix, suffix)
|
||||
|
||||
@@ -112,8 +131,8 @@ export const toggleWrapRanges = (
|
||||
const ranges = view.state.selection.ranges
|
||||
const needsCustom = ranges.some(
|
||||
range =>
|
||||
enclosingEmphasis(view, range, nodeType) !== null ||
|
||||
innerMarksToStrip(view, range, nodeType).length > 0
|
||||
enclosingNode(view, range, nodeType) !== null ||
|
||||
innerMarksToStrip(view, range, nodeType, markType).length > 0
|
||||
)
|
||||
|
||||
if (!needsCustom) {
|
||||
@@ -122,18 +141,19 @@ export const toggleWrapRanges = (
|
||||
|
||||
view.dispatch(
|
||||
view.state.changeByRange(range => {
|
||||
const emphasis = enclosingEmphasis(view, range, nodeType)
|
||||
if (!emphasis) {
|
||||
const enclosing = enclosingNode(view, range, nodeType)
|
||||
if (!enclosing) {
|
||||
// Strip any same-type spans inside the selection before wrapping so
|
||||
// the result is a single flat span rather than nested markup.
|
||||
const innerMarks = innerMarksToStrip(view, range, nodeType)
|
||||
const innerMarks = innerMarksToStrip(view, range, nodeType, markType)
|
||||
if (innerMarks.length > 0) {
|
||||
const deletedLength = innerMarks.reduce(
|
||||
(total, mark) => total + (mark.to - mark.from),
|
||||
0
|
||||
)
|
||||
return {
|
||||
range: EditorSelection.range(
|
||||
range: moveRange(
|
||||
range,
|
||||
range.from + prefix.length,
|
||||
range.to + prefix.length - deletedLength
|
||||
),
|
||||
@@ -160,7 +180,7 @@ export const toggleWrapRanges = (
|
||||
}
|
||||
}
|
||||
|
||||
const marks = emphasisMarks(emphasis)
|
||||
const marks = delimiterMarks(enclosing, markType)
|
||||
if (!marks) {
|
||||
return { range }
|
||||
}
|
||||
@@ -176,7 +196,7 @@ export const toggleWrapRanges = (
|
||||
Math.min(Math.max(pos, innerFrom), innerTo) - openLength
|
||||
|
||||
return {
|
||||
range: EditorSelection.range(mapPos(range.from), mapPos(range.to)),
|
||||
range: moveRange(range, mapPos(range.from), mapPos(range.to)),
|
||||
changes: [
|
||||
{ from: open.from, to: open.to },
|
||||
{ from: close.from, to: close.to },
|
||||
|
||||
Reference in New Issue
Block a user