ranges.ts 24 KB

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