ranges.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  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, view: EditorView) => 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, view) : 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. const tree = ensureSyntaxTree(view.state, 1000)
  437. const nodeAtFrom = tree?.resolveInner(range.from, 1)
  438. const nodeAtTo = tree?.resolveInner(range.to, -1)
  439. const isSingleNodeSelected = nodeAtFrom === nodeAtTo
  440. if (
  441. !isSingleNodeSelected &&
  442. ancestorAtStartOfRange !== ancestorAtEndOfRange
  443. ) {
  444. // But handle the exception of case 8
  445. const ancestorAtStartIsWrappingCommand =
  446. ancestorAtStartOfRange &&
  447. isUnknownCommandWithName(
  448. ancestorAtStartOfRange,
  449. command,
  450. view.state
  451. )
  452. const ancestorAtEndIsWrappingCommand =
  453. ancestorAtEndOfRange &&
  454. isUnknownCommandWithName(ancestorAtEndOfRange, command, view.state)
  455. if (
  456. ancestorAtStartIsWrappingCommand &&
  457. ancestorAtEndIsWrappingCommand &&
  458. ancestorAtStartOfRange?.parent?.parent &&
  459. ancestorAtEndOfRange?.parent?.parent
  460. ) {
  461. // Test for case 8
  462. const nextAncestorAtStartOfRange =
  463. ancestorAtStartOfRange.parent.parent
  464. const nextAncestorAtEndOfRange = ancestorAtEndOfRange.parent.parent
  465. if (nextAncestorAtStartOfRange === nextAncestorAtEndOfRange) {
  466. // Join the two ranges
  467. const textBetweenRanges = view.state.sliceDoc(
  468. ancestorAtStartOfRange.to,
  469. ancestorAtEndOfRange.from
  470. )
  471. const ancestorStartArgumentNode =
  472. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  473. const ancestorEndArgumentNode =
  474. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  475. if (!ancestorStartArgumentNode || !ancestorEndArgumentNode) {
  476. throw new Error("Can't find argument node")
  477. }
  478. const actualContent = view.state.sliceDoc(
  479. ancestorAtStartOfRange.from,
  480. ancestorAtEndOfRange.to
  481. )
  482. const firstCommandArgument = view.state.sliceDoc(
  483. ancestorStartArgumentNode.from,
  484. ancestorStartArgumentNode.to
  485. )
  486. const secondCommandArgument = view.state.sliceDoc(
  487. ancestorEndArgumentNode.from,
  488. ancestorEndArgumentNode.to
  489. )
  490. validateReplacement(
  491. `${command}{${firstCommandArgument}}${textBetweenRanges}${command}{${secondCommandArgument}}`,
  492. actualContent
  493. )
  494. const changes = view.state.changes([
  495. {
  496. from: ancestorAtStartOfRange.from,
  497. to: ancestorAtEndOfRange.to,
  498. insert: `${command}{${firstCommandArgument}${textBetweenRanges}${secondCommandArgument}}`,
  499. },
  500. ])
  501. return {
  502. changes,
  503. range: moveRange(
  504. range,
  505. range.from,
  506. range.to - command.length - 1 - 1
  507. ),
  508. }
  509. }
  510. }
  511. if (
  512. ancestorAtEndIsWrappingCommand &&
  513. ancestorAtEndOfRange.parent?.parent === ancestorAtStartOfRange
  514. ) {
  515. // Extend to the left. Case 10
  516. const contentUpToCommand = view.state.sliceDoc(
  517. range.from,
  518. ancestorAtEndOfRange.from
  519. )
  520. const ancestorEndArgumentNode =
  521. ancestorAtEndOfRange.lastChild?.getChild('LongArg')
  522. if (!ancestorEndArgumentNode) {
  523. throw new Error("Can't find argument node")
  524. }
  525. const commandContent = view.state.sliceDoc(
  526. ancestorEndArgumentNode.from,
  527. ancestorEndArgumentNode.to
  528. )
  529. const actualContent = view.state.sliceDoc(
  530. range.from,
  531. ancestorAtEndOfRange.to
  532. )
  533. validateReplacement(
  534. `${contentUpToCommand}${command}{${commandContent}}`,
  535. actualContent
  536. )
  537. const changes = view.state.changes([
  538. {
  539. from: range.from,
  540. to: ancestorAtEndOfRange.to,
  541. insert: `${command}{${contentUpToCommand}${commandContent}}`,
  542. },
  543. ])
  544. return {
  545. changes,
  546. range: moveRange(
  547. range,
  548. range.from + command.length + 1,
  549. range.to
  550. ),
  551. }
  552. }
  553. if (
  554. ancestorAtStartIsWrappingCommand &&
  555. ancestorAtStartOfRange.parent?.parent === ancestorAtEndOfRange
  556. ) {
  557. // Extend to the right. Case 9
  558. const contentAfterCommand = view.state.sliceDoc(
  559. ancestorAtStartOfRange.to,
  560. range.to
  561. )
  562. const ancestorStartArgumentNode =
  563. ancestorAtStartOfRange.lastChild?.getChild('LongArg')
  564. if (!ancestorStartArgumentNode) {
  565. throw new Error("Can't find argument node")
  566. }
  567. const commandContent = view.state.sliceDoc(
  568. ancestorStartArgumentNode.from,
  569. ancestorStartArgumentNode.to
  570. )
  571. const actualContent = view.state.sliceDoc(
  572. ancestorAtStartOfRange.from,
  573. range.to
  574. )
  575. validateReplacement(
  576. `${command}{${commandContent}}${contentAfterCommand}`,
  577. actualContent
  578. )
  579. const changes = view.state.changes([
  580. {
  581. from: ancestorAtStartOfRange.from,
  582. to: range.to,
  583. insert: `${command}{${commandContent}${contentAfterCommand}}`,
  584. },
  585. ])
  586. return {
  587. changes,
  588. range: moveRange(range, range.from, range.to - 1),
  589. }
  590. }
  591. // Bail out in case 1
  592. // TODO: signal error to the user
  593. return { range }
  594. }
  595. const ancestor = ancestorAtStartOfRange
  596. // Bail out in case 2
  597. if (content.includes('\n\n')) {
  598. // TODO: signal error to the user
  599. return { range }
  600. }
  601. const isCursorBeforeAncestor =
  602. range.empty &&
  603. ancestor &&
  604. range.from === ancestor.from &&
  605. isUnknownCommandWithName(ancestor, command, view.state)
  606. // If we can't find an ancestor node, or if the parent is not an exsting
  607. // \textbf, then we just wrap it in a range. Case 3.
  608. if (
  609. isCursorBeforeAncestor ||
  610. !ancestor ||
  611. !isUnknownCommandWithName(ancestor, command, view.state)
  612. ) {
  613. return wrapRangeInCommand(view.state, range, command)
  614. }
  615. const argumentNode = ancestor.lastChild?.getChild('LongArg')
  616. if (!argumentNode) {
  617. throw new Error("Can't find argument node")
  618. }
  619. // We should trim from the beginning. Case 4
  620. if (range.from === argumentNode.from && range.to !== argumentNode.to) {
  621. const textAfterSelection = view.state.sliceDoc(
  622. range.to,
  623. argumentNode.to
  624. )
  625. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  626. validateReplacement(
  627. `${command}{${content}${textAfterSelection}}`,
  628. wholeCommand
  629. )
  630. const changes = view.state.changes([
  631. {
  632. from: ancestor.from,
  633. to: ancestor.to,
  634. insert: `${content}${command}{${textAfterSelection}}`,
  635. },
  636. ])
  637. return {
  638. range: moveRange(
  639. range,
  640. range.from - command.length - 1,
  641. range.to - command.length - 1
  642. ),
  643. changes,
  644. }
  645. }
  646. // We should trim from the end. Case 5
  647. if (range.to === argumentNode.to && range.from !== argumentNode.from) {
  648. const textBeforeSelection = view.state.sliceDoc(
  649. ancestor.from,
  650. range.from
  651. )
  652. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  653. validateReplacement(`${textBeforeSelection}${content}}`, wholeCommand)
  654. const changes = view.state.changes([
  655. {
  656. from: ancestor.from,
  657. to: ancestor.to,
  658. insert: `${textBeforeSelection}}${content}`,
  659. },
  660. ])
  661. // We should shift selection forward by the } we insert
  662. return {
  663. range: moveRange(range, range.from + 1, range.to + 1),
  664. changes,
  665. }
  666. }
  667. // We should split the command in two. Case 6
  668. if (range.from !== argumentNode.from && range.to !== argumentNode.to) {
  669. const textBeforeSelection = view.state.sliceDoc(
  670. ancestor.from,
  671. range.from
  672. )
  673. const textAfterSelection = view.state.sliceDoc(range.to, ancestor.to)
  674. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  675. validateReplacement(
  676. `${textBeforeSelection}${content}${textAfterSelection}`,
  677. wholeCommand
  678. )
  679. const changes = view.state.changes([
  680. {
  681. from: ancestor.from,
  682. to: ancestor.to,
  683. insert: `${textBeforeSelection}}${content}${command}{${textAfterSelection}`,
  684. },
  685. ])
  686. return {
  687. range: moveRange(range, range.from + 1, range.to + 1),
  688. changes,
  689. }
  690. }
  691. // Remove the wrapping command. Case 7
  692. if (spansWholeArgument(ancestor, range)) {
  693. const argumentContent = view.state.sliceDoc(
  694. argumentNode.from,
  695. argumentNode.to
  696. )
  697. const wholeCommand = view.state.sliceDoc(ancestor.from, ancestor.to)
  698. validateReplacement(`${command}{${content}}`, wholeCommand)
  699. const changes = view.state.changes([
  700. { from: ancestor.from, to: ancestor.to, insert: argumentContent },
  701. ])
  702. return {
  703. range: moveRange(
  704. range,
  705. range.from - command.length - 1,
  706. range.to - command.length - 1
  707. ),
  708. changes,
  709. }
  710. }
  711. // Shouldn't happen, but default to just wrapping the content
  712. return wrapRangeInCommand(view.state, range, command)
  713. }),
  714. { scrollIntoView: true, annotations }
  715. )
  716. return true
  717. }
  718. }