ranges.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. import { EditorView } from '@codemirror/view'
  2. import {
  3. EditorSelection,
  4. EditorState,
  5. SelectionRange,
  6. TransactionSpec,
  7. } from '@codemirror/state'
  8. import {
  9. ensureSyntaxTree,
  10. foldedRanges,
  11. foldEffect,
  12. syntaxTree,
  13. } from '@codemirror/language'
  14. import { SyntaxNode } from '@lezer/common'
  15. import {
  16. ancestorOfNodeWithType,
  17. isUnknownCommandWithName,
  18. } from '../utils/tree-query'
  19. import { lastAncestorAtEndPosition } from '../utils/tree-operations/ancestors'
  20. export const wrapRanges =
  21. (
  22. prefix: string,
  23. suffix: string,
  24. wrapWholeLine = false,
  25. selection?: (range: SelectionRange) => SelectionRange
  26. ) =>
  27. (view: EditorView): boolean => {
  28. if (view.state.readOnly) {
  29. return false
  30. }
  31. view.dispatch(
  32. view.state.changeByRange(range => {
  33. const insert = { prefix, suffix }
  34. if (wrapWholeLine) {
  35. const line = view.state.doc.lineAt(range.anchor)
  36. if (range.empty) {
  37. // expand range to cover the whole line
  38. range = EditorSelection.range(line.from, line.to)
  39. }
  40. // add a newline at the start if needed
  41. if (range.from !== line.from) {
  42. insert.prefix = `\n${prefix}`
  43. }
  44. // add a newline at the end if needed
  45. if (range.to !== line.to) {
  46. insert.suffix = `${suffix}\n`
  47. }
  48. }
  49. const content = view.state.sliceDoc(range.from, range.to)
  50. // map through the prefix only
  51. const changedRange = range.map(
  52. view.state.changes([
  53. { from: range.from, insert: `${insert.prefix}` },
  54. ]),
  55. 1
  56. )
  57. return {
  58. range: selection ? selection(changedRange) : changedRange,
  59. // create a single change, including the content
  60. changes: [
  61. {
  62. from: range.from,
  63. to: range.to,
  64. insert: `${insert.prefix}${content}${insert.suffix}`,
  65. },
  66. ],
  67. }
  68. }),
  69. { scrollIntoView: true }
  70. )
  71. return true
  72. }
  73. export const changeCase =
  74. (upper = true) =>
  75. (view: EditorView) => {
  76. if (view.state.readOnly) {
  77. return false
  78. }
  79. view.dispatch(
  80. view.state.changeByRange(range => {
  81. // ignore empty ranges
  82. if (range.empty) {
  83. return { range }
  84. }
  85. const text = view.state.doc.sliceString(range.from, range.to)
  86. return {
  87. range,
  88. changes: [
  89. {
  90. from: range.from,
  91. to: range.to,
  92. insert: upper ? text.toUpperCase() : text.toLowerCase(),
  93. },
  94. ],
  95. }
  96. })
  97. )
  98. return true
  99. }
  100. export const duplicateSelection = (view: EditorView) => {
  101. if (view.state.readOnly) {
  102. return false
  103. }
  104. const foldedRangesInDocument = foldedRanges(view.state)
  105. view.dispatch(
  106. view.state.changeByRange(range => {
  107. const folds: { offset: number; base: number; length: number }[] = []
  108. if (range.empty) {
  109. const line = view.state.doc.lineAt(range.from)
  110. let lineStart = line.from
  111. let lineEnd = line.to
  112. // Calculate line start/end including folded ranges
  113. //
  114. // Note that at each iteration of the while loop, new folded ranges
  115. // can be included. This happens when there are multiple folded ranges
  116. // on a single editor line (but spanning multiple actual lines)
  117. //
  118. // For example, the following document:
  119. // 1: \begin{document}
  120. // 2: test
  121. // 3: \end{document}\begin{document}
  122. // 4: test
  123. // 5: \end{document}
  124. //
  125. // Can be folded to:
  126. // \begin{document}<...>\end{document}\begin{document}<...>\end{document}
  127. //
  128. // In this case, the first iterations of the while loop below will only
  129. // include lines 3-5, since the overlapping folded range for line 5
  130. // is only the fold on lines 3-5. Hence in the while loop, we expand the
  131. // selection until we include all the ranges.
  132. let changed
  133. do {
  134. changed = false
  135. foldedRangesInDocument.between(lineStart, lineEnd, (from, to) => {
  136. const newLineStart = Math.min(
  137. view.state.doc.lineAt(from).from,
  138. lineStart
  139. )
  140. const newLineEnd = Math.max(view.state.doc.lineAt(to).to, lineEnd)
  141. if (newLineStart !== lineStart || newLineEnd !== lineEnd) {
  142. changed = true
  143. lineStart = newLineStart
  144. lineEnd = newLineEnd
  145. }
  146. })
  147. } while (changed)
  148. // Collect information needed to fold duplicated lines
  149. foldedRangesInDocument.between(lineStart, lineEnd, (from, to) => {
  150. folds.push({
  151. offset: from - lineStart,
  152. base: lineEnd + view.state.lineBreak.length,
  153. length: to - from,
  154. })
  155. })
  156. // Duplicate the selected lines downwards
  157. return {
  158. range,
  159. changes: [
  160. {
  161. from: lineEnd,
  162. insert:
  163. view.state.lineBreak + view.state.doc.slice(lineStart, lineEnd),
  164. },
  165. ],
  166. // Add a fold effect for each fold in the original line
  167. effects: folds.map(fold =>
  168. foldEffect.of({
  169. from: fold.base + fold.offset,
  170. to: fold.base + fold.offset + fold.length,
  171. })
  172. ),
  173. }
  174. } else {
  175. // Duplicate selected text at head of selection
  176. let newSelectionRange = range
  177. if (range.head > range.anchor) {
  178. // Duplicating to the right, so we need to update the selected range
  179. newSelectionRange = EditorSelection.range(
  180. range.head,
  181. range.head + (range.to - range.from)
  182. )
  183. }
  184. return {
  185. // The new range is the duplicated section, placed at the head of the
  186. // original selection
  187. range: newSelectionRange,
  188. changes: [
  189. {
  190. from: range.head,
  191. insert: view.state.doc.slice(range.from, range.to),
  192. },
  193. ],
  194. }
  195. }
  196. })
  197. )
  198. return true
  199. }
  200. function getParentNode(
  201. position: number | SyntaxNode,
  202. state: EditorState,
  203. assoc: 0 | 1 | -1 = 1
  204. ): SyntaxNode | undefined {
  205. const tree = ensureSyntaxTree(state, 1000)
  206. let node: SyntaxNode | undefined | null = null
  207. if (typeof position === 'number') {
  208. node = tree?.resolveInner(position, assoc)?.parent
  209. const ancestorAtEndPos = lastAncestorAtEndPosition(node, position)
  210. if (ancestorAtEndPos?.parent) {
  211. node = ancestorAtEndPos.parent
  212. }
  213. } else {
  214. node = position?.parent
  215. }
  216. while (
  217. ['LongArg', 'TextArgument', 'OpenBrace', 'CloseBrace'].includes(
  218. node?.type.name || ''
  219. )
  220. ) {
  221. node = node!.parent
  222. }
  223. return node || undefined
  224. }
  225. function wrapRangeInCommand(
  226. state: EditorState,
  227. range: SelectionRange,
  228. command: string
  229. ) {
  230. const content = state.sliceDoc(range.from, range.to)
  231. const changes = state.changes([
  232. {
  233. from: range.from,
  234. to: range.to,
  235. insert: `${command}{${content}}`,
  236. },
  237. ])
  238. return {
  239. changes,
  240. range: moveRange(
  241. range,
  242. range.from + command.length + 1,
  243. range.from + command.length + content.length + 1
  244. ),
  245. }
  246. }
  247. function moveRange(range: SelectionRange, newFrom: number, newTo: number) {
  248. const forwards = range.from === range.anchor
  249. return forwards
  250. ? EditorSelection.range(newFrom, newTo)
  251. : EditorSelection.range(newTo, newFrom)
  252. }
  253. function validateReplacement(expected: string, actual: string) {
  254. if (expected !== actual) {
  255. throw new Error('Replacement in toggleRange failed validation.')
  256. }
  257. }
  258. function getWrappingAncestor(
  259. node: SyntaxNode,
  260. command: string,
  261. state: EditorState
  262. ): SyntaxNode | null {
  263. for (
  264. let ancestor: SyntaxNode | null = node;
  265. ancestor;
  266. ancestor = ancestor.parent
  267. ) {
  268. if (isUnknownCommandWithName(ancestor, command, state)) {
  269. return ancestor
  270. }
  271. if (ancestor.type.is('UnknownCommand')) {
  272. // We could multiple levels deep in bold/non-bold. So bail out in this case
  273. return null
  274. }
  275. }
  276. return null
  277. }
  278. function adjustRangeIfNeeded(
  279. command: string,
  280. range: SelectionRange,
  281. state: EditorState
  282. ) {
  283. // Try to adjust the selection, if it is either
  284. // 1. \textbf<{test>}
  285. // 2. \textbf{<test}>
  286. // 3. \textbf<{test}>
  287. // 4. <\textbf{test}>
  288. // 4. \textbf<>{test}
  289. const tree = syntaxTree(state)
  290. if (tree.length < range.to) {
  291. return range
  292. }
  293. const nodeLeft = tree.resolveInner(range.from, 1)
  294. const nodeRight = tree.resolveInner(range.to, -1)
  295. const parentLeft = getWrappingAncestor(nodeLeft, command, state)
  296. const parentRight = getWrappingAncestor(nodeRight, command, state)
  297. const parent = getParentNode(nodeLeft, state)
  298. if (parent?.type.is('UnknownCommand') && spansWholeArgument(parent, range)) {
  299. return bubbleUpRange(
  300. command,
  301. ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
  302. range,
  303. state
  304. )
  305. }
  306. if (!parentLeft) {
  307. // We're not trying to unbold, so don't bother adjusting range
  308. return bubbleUpRange(
  309. command,
  310. ancestorOfNodeWithType(nodeLeft, 'UnknownCommand'),
  311. range,
  312. state
  313. )
  314. }
  315. if (nodeLeft.type.is('CtrlSeq') && range.from === range.to) {
  316. const command = nodeLeft.parent?.parent
  317. if (!command) {
  318. return range
  319. }
  320. return EditorSelection.cursor(command.from)
  321. }
  322. let { from, to } = range
  323. if (nodeLeft.type.is('CtrlSeq')) {
  324. from = nodeLeft.to + 1
  325. }
  326. if (nodeLeft.type.is('OpenBrace')) {
  327. from = nodeLeft.to
  328. }
  329. // We know that parentLeft is the UnknownCommand, so now we check if we're
  330. // to the right of the closing brace. (parent is TextArgument, grandparent is
  331. // UnknownCommand)
  332. if (parentLeft === parentRight && nodeRight.type.is('CloseBrace')) {
  333. to = nodeRight.from
  334. }
  335. return bubbleUpRange(command, parentLeft, moveRange(range, from, to), state)
  336. }
  337. function spansWholeArgument(
  338. commandNode: SyntaxNode | null,
  339. range: SelectionRange
  340. ): boolean {
  341. const argument = commandNode?.getChild('TextArgument')?.getChild('LongArg')
  342. const res = Boolean(
  343. argument && argument.from === range.from && argument.to === range.to
  344. )
  345. return res
  346. }
  347. function bubbleUpRange(
  348. command: string,
  349. node: SyntaxNode | null,
  350. range: SelectionRange,
  351. state: EditorState
  352. ) {
  353. let currentRange = range
  354. for (
  355. let ancestorCommand: SyntaxNode | null = ancestorOfNodeWithType(
  356. node,
  357. 'UnknownCommand'
  358. );
  359. spansWholeArgument(ancestorCommand, currentRange);
  360. ancestorCommand = ancestorOfNodeWithType(
  361. ancestorCommand.parent,
  362. 'UnknownCommand'
  363. )
  364. ) {
  365. if (!ancestorCommand) {
  366. break
  367. }
  368. currentRange = moveRange(
  369. currentRange,
  370. ancestorCommand.from,
  371. ancestorCommand.to
  372. )
  373. if (isUnknownCommandWithName(ancestorCommand, command, state)) {
  374. const argumentNode = ancestorCommand
  375. .getChild('TextArgument')
  376. ?.getChild('LongArg')
  377. if (!argumentNode) {
  378. return range
  379. }
  380. return moveRange(range, argumentNode.from, argumentNode.to)
  381. }
  382. }
  383. return range
  384. }
  385. export function toggleRanges(
  386. command: string,
  387. annotations?: TransactionSpec['annotations']
  388. ) {
  389. /* There are a number of situations we need to handle in this function.
  390. * In the following examples, the selection range is marked within <>
  391. * 1. If the parent node at start and end of selection is different -> do
  392. * nothing & show error. Case 8 is an exception to this.
  393. * -> For good and bad, this disallows \textbf{this <is} weird
  394. * \textit{to> do}
  395. * 2. If selection contains a BlankLine (i.e. two newlines in a row) -> do
  396. * nothing & show error
  397. * -> \textbf doesn't allow paragraph breaks).
  398. * 3. If the selection is not within a \textbf -> wrap it in a \textbf
  399. * 4. If selection is at the beginning of a \textbf -> shrink the \textbf
  400. * command
  401. * -> e.g. \textbf{<this is> a test} → <this is>\textbf{ a test}
  402. * 5. Similarly for selection at end of \textbf command
  403. * 6. If selection is in the middle of a \textbf command -> split the command
  404. * into two
  405. * -> e.g. \textbf{this <is a> test} → \textbf{this }<is a>\textbf{ test}
  406. * 7. If the selection is a whole \textbf command → remove the wrapping
  407. * command.
  408. * 8. If the selection spans two \textbf's with the same parent then join the
  409. * two
  410. * -> e.g. \textbf{this <is} a \textbf{te>st} → \textbf{this <is a te>st}
  411. * 9. If the selection spans the end of a \textbf, into the parent of the
  412. * command, then extend the \textbf.
  413. * -> e.g. \textbf{this <is} a test> → \textbf{this <is a test>}
  414. * 10. Similarly for selections spanning the beginning of the selection
  415. */
  416. return (view: EditorView): boolean => {
  417. if (view.state.readOnly) {
  418. return false
  419. }
  420. view.dispatch(
  421. view.state.changeByRange(initialRange => {
  422. const range = adjustRangeIfNeeded(command, initialRange, view.state)
  423. const content = view.state.sliceDoc(range.from, range.to)
  424. const ancestorAtStartOfRange = getParentNode(
  425. range.from,
  426. view.state,
  427. range.from === 0 ? 1 : -1
  428. )
  429. const ancestorAtEndOfRange = range.empty
  430. ? ancestorAtStartOfRange
  431. : getParentNode(
  432. range.to,
  433. view.state,
  434. range.to === view.state.doc.length ? -1 : 1
  435. )
  436. if (ancestorAtStartOfRange !== ancestorAtEndOfRange) {
  437. // But handle the exception of case 8
  438. const ancestorAtStartIsWrappingCommand =
  439. ancestorAtStartOfRange &&
  440. isUnknownCommandWithName(
  441. ancestorAtStartOfRange,
  442. command,
  443. view.state
  444. )
  445. const ancestorAtEndIsWrappingCommand =
  446. ancestorAtEndOfRange &&
  447. isUnknownCommandWithName(ancestorAtEndOfRange, command, view.state)
  448. if (
  449. ancestorAtStartIsWrappingCommand &&
  450. ancestorAtEndIsWrappingCommand &&
  451. ancestorAtStartOfRange?.parent?.parent &&
  452. ancestorAtEndOfRange?.parent?.parent
  453. ) {
  454. // Test for case 8
  455. const nextAncestorAtStartOfRange =
  456. ancestorAtStartOfRange.parent.parent
  457. const nextAncestorAtEndOfRange = ancestorAtEndOfRange.parent.parent
  458. if (nextAncestorAtStartOfRange === nextAncestorAtEndOfRange) {
  459. // Join the two ranges
  460. const textBetweenRanges = view.state.sliceDoc(
  461. ancestorAtStartOfRange.to,
  462. ancestorAtEndOfRange.from
  463. )
  464. const ancestorStartArgumentNode =
  465. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  466. const ancestorEndArgumentNode =
  467. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  468. if (!ancestorStartArgumentNode || !ancestorEndArgumentNode) {
  469. throw new Error("Can't find argument node")
  470. }
  471. const actualContent = view.state.sliceDoc(
  472. ancestorAtStartOfRange.from,
  473. ancestorAtEndOfRange.to
  474. )
  475. const firstCommandArgument = view.state.sliceDoc(
  476. ancestorStartArgumentNode.from,
  477. ancestorStartArgumentNode.to
  478. )
  479. const secondCommandArgument = view.state.sliceDoc(
  480. ancestorEndArgumentNode.from,
  481. ancestorEndArgumentNode.to
  482. )
  483. validateReplacement(
  484. `${command}{${firstCommandArgument}}${textBetweenRanges}${command}{${secondCommandArgument}}`,
  485. actualContent
  486. )
  487. const changes = view.state.changes([
  488. {
  489. from: ancestorAtStartOfRange.from,
  490. to: ancestorAtEndOfRange.to,
  491. insert: `${command}{${firstCommandArgument}${textBetweenRanges}${secondCommandArgument}}`,
  492. },
  493. ])
  494. return {
  495. changes,
  496. range: moveRange(
  497. range,
  498. range.from,
  499. range.to - command.length - 1 - 1
  500. ),
  501. }
  502. }
  503. }
  504. if (
  505. ancestorAtEndIsWrappingCommand &&
  506. ancestorAtEndOfRange.parent?.parent === ancestorAtStartOfRange
  507. ) {
  508. // Extend to the left. Case 10
  509. const contentUpToCommand = view.state.sliceDoc(
  510. range.from,
  511. ancestorAtEndOfRange.from
  512. )
  513. const ancestorEndArgumentNode =
  514. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  515. if (!ancestorEndArgumentNode) {
  516. throw new Error("Can't find argument node")
  517. }
  518. const commandContent = view.state.sliceDoc(
  519. ancestorEndArgumentNode.from,
  520. ancestorEndArgumentNode.to
  521. )
  522. const actualContent = view.state.sliceDoc(
  523. range.from,
  524. ancestorAtEndOfRange.to
  525. )
  526. validateReplacement(
  527. `${contentUpToCommand}${command}{${commandContent}}`,
  528. actualContent
  529. )
  530. const changes = view.state.changes([
  531. {
  532. from: range.from,
  533. to: ancestorAtEndOfRange.to,
  534. insert: `${command}{${contentUpToCommand}${commandContent}}`,
  535. },
  536. ])
  537. return {
  538. changes,
  539. range: moveRange(
  540. range,
  541. range.from + command.length + 1,
  542. range.to
  543. ),
  544. }
  545. }
  546. if (
  547. ancestorAtStartIsWrappingCommand &&
  548. ancestorAtStartOfRange.parent?.parent === ancestorAtEndOfRange
  549. ) {
  550. // Extend to the right. Case 9
  551. const contentAfterCommand = view.state.sliceDoc(
  552. ancestorAtStartOfRange.to,
  553. range.to
  554. )
  555. const ancestorStartArgumentNode =
  556. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  557. if (!ancestorStartArgumentNode) {
  558. throw new Error("Can't find argument node")
  559. }
  560. const commandContent = view.state.sliceDoc(
  561. ancestorStartArgumentNode.from,
  562. ancestorStartArgumentNode.to
  563. )
  564. const actualContent = view.state.sliceDoc(
  565. ancestorAtStartOfRange.from,
  566. range.to
  567. )
  568. validateReplacement(
  569. `${command}{${commandContent}}${contentAfterCommand}`,
  570. actualContent
  571. )
  572. const changes = view.state.changes([
  573. {
  574. from: ancestorAtStartOfRange.from,
  575. to: range.to,
  576. insert: `${command}{${commandContent}${contentAfterCommand}}`,
  577. },
  578. ])
  579. return {
  580. changes,
  581. range: moveRange(range, range.from, range.to - 1),
  582. }
  583. }
  584. // Bail out in case 1
  585. // TODO: signal error to the user
  586. return { range }
  587. }
  588. const ancestor = ancestorAtStartOfRange
  589. // Bail out in case 2
  590. if (content.includes('\n\n')) {
  591. // TODO: signal error to the user
  592. return { range }
  593. }
  594. const isCursorBeforeAncestor =
  595. range.empty &&
  596. ancestor &&
  597. range.from === ancestor.from &&
  598. isUnknownCommandWithName(ancestor, command, view.state)
  599. // If we can't find an ancestor node, or if the parent is not an exsting
  600. // \textbf, then we just wrap it in a range. Case 3.
  601. if (
  602. isCursorBeforeAncestor ||
  603. !ancestor ||
  604. !isUnknownCommandWithName(ancestor, command, view.state)
  605. ) {
  606. return wrapRangeInCommand(view.state, range, command)
  607. }
  608. const argumentNode = ancestor.lastChild?.getChild('LongArg')
  609. if (!argumentNode) {
  610. throw new Error("Can't find argument node")
  611. }
  612. // We should trim from the beginning. Case 4
  613. if (range.from === argumentNode.from && range.to !== argumentNode.to) {
  614. const textAfterSelection = view.state.sliceDoc(
  615. range.to,
  616. argumentNode.to
  617. )
  618. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  619. validateReplacement(
  620. `${command}{${content}${textAfterSelection}}`,
  621. wholeCommand
  622. )
  623. const changes = view.state.changes([
  624. {
  625. from: ancestor.from,
  626. to: ancestor.to,
  627. insert: `${content}${command}{${textAfterSelection}}`,
  628. },
  629. ])
  630. return {
  631. range: moveRange(
  632. range,
  633. range.from - command.length - 1,
  634. range.to - command.length - 1
  635. ),
  636. changes,
  637. }
  638. }
  639. // We should trim from the end. Case 5
  640. if (range.to === argumentNode.to && range.from !== argumentNode.from) {
  641. const textBeforeSelection = view.state.sliceDoc(
  642. ancestor.from,
  643. range.from
  644. )
  645. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  646. validateReplacement(`${textBeforeSelection}${content}}`, wholeCommand)
  647. const changes = view.state.changes([
  648. {
  649. from: ancestor.from,
  650. to: ancestor.to,
  651. insert: `${textBeforeSelection}}${content}`,
  652. },
  653. ])
  654. // We should shift selection forward by the } we insert
  655. return {
  656. range: moveRange(range, range.from + 1, range.to + 1),
  657. changes,
  658. }
  659. }
  660. // We should split the command in two. Case 6
  661. if (range.from !== argumentNode.from && range.to !== argumentNode.to) {
  662. const textBeforeSelection = view.state.sliceDoc(
  663. ancestor.from,
  664. range.from
  665. )
  666. const textAfterSelection = view.state.sliceDoc(range.to, ancestor.to)
  667. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  668. validateReplacement(
  669. `${textBeforeSelection}${content}${textAfterSelection}`,
  670. wholeCommand
  671. )
  672. const changes = view.state.changes([
  673. {
  674. from: ancestor.from,
  675. to: ancestor.to,
  676. insert: `${textBeforeSelection}}${content}${command}{${textAfterSelection}`,
  677. },
  678. ])
  679. return {
  680. range: moveRange(range, range.from + 1, range.to + 1),
  681. changes,
  682. }
  683. }
  684. // Remove the wrapping command. Case 7
  685. if (spansWholeArgument(ancestor, range)) {
  686. const argumentContent = view.state.sliceDoc(
  687. argumentNode.from,
  688. argumentNode.to
  689. )
  690. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  691. validateReplacement(`${command}{${content}}`, wholeCommand)
  692. const changes = view.state.changes([
  693. { from: ancestor.from, to: ancestor.to, insert: argumentContent },
  694. ])
  695. return {
  696. range: moveRange(
  697. range,
  698. range.from - command.length - 1,
  699. range.to - command.length - 1
  700. ),
  701. changes,
  702. }
  703. }
  704. // Shouldn't happen, but default to just wrapping the content
  705. return wrapRangeInCommand(view.state, range, command)
  706. }),
  707. { scrollIntoView: true, annotations }
  708. )
  709. return true
  710. }
  711. }