ranges.ts 24 KB

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