commands.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import { EditorState } from '@codemirror/state'
  2. import { SyntaxNode, SyntaxNodeRef } from '@lezer/common'
  3. import { getOptionalArgumentText } from './common'
  4. import { NodeIntersectsChangeFn, ProjectionItem } from './projection'
  5. /**
  6. * A projection of a command in the document
  7. */
  8. export class Command extends ProjectionItem {
  9. readonly title: string = ''
  10. readonly optionalArgCount: number = 0
  11. readonly requiredArgCount: number = 0
  12. }
  13. /**
  14. * Extracts Command instances from the syntax tree.
  15. * `\newcommand` and `\renewcommand` are treated specially
  16. */
  17. export const enterNode = (
  18. state: EditorState,
  19. node: SyntaxNodeRef,
  20. items: Command[],
  21. nodeIntersectsChange: NodeIntersectsChangeFn
  22. ): any => {
  23. if (node.type.is('NewCommand') || node.type.is('RenewCommand')) {
  24. if (!nodeIntersectsChange(node.node)) {
  25. // This should already be in `items`
  26. return
  27. }
  28. let commandName = node.node.getChild('LiteralArgContent')
  29. if (!commandName) {
  30. commandName = node.node.getChild('Csname')
  31. }
  32. if (!commandName) {
  33. return
  34. }
  35. const commandNameText = state.doc.sliceString(
  36. commandName.from,
  37. commandName.to
  38. )
  39. if (commandNameText.length < 1) {
  40. return
  41. }
  42. const optionalArguments = node.node.getChildren('OptionalArgument')
  43. let argCountNumber = 0
  44. if (optionalArguments.length > 0) {
  45. const argumentCountNode = optionalArguments[0]
  46. const argCountText = getOptionalArgumentText(state, argumentCountNode)
  47. if (argCountText) {
  48. try {
  49. argCountNumber = parseInt(argCountText, 10)
  50. } catch (err) {}
  51. }
  52. }
  53. const commandDefinitionHasOptionalArgument = optionalArguments.length === 2
  54. if (commandDefinitionHasOptionalArgument && argCountNumber > 0) {
  55. argCountNumber--
  56. }
  57. const thisCommand: Readonly<Command> = {
  58. line: state.doc.lineAt(node.from).number,
  59. title: commandNameText,
  60. from: node.from,
  61. to: node.to,
  62. optionalArgCount: commandDefinitionHasOptionalArgument ? 1 : 0,
  63. requiredArgCount: argCountNumber,
  64. }
  65. items.push(thisCommand)
  66. } else if (
  67. node.type.is('UnknownCommand') ||
  68. node.type.is('KnownCommand') ||
  69. node.type.is('MathUnknownCommand') ||
  70. node.type.is('DefinitionFragmentUnknownCommand')
  71. ) {
  72. let commandNode: SyntaxNode | null = node.node
  73. if (node.type.is('KnownCommand')) {
  74. // KnownCommands are defined as
  75. //
  76. // KnownCommand {
  77. // CommandName {
  78. // CommandCtrlSeq [args]
  79. // }
  80. // }
  81. // So for a KnownCommand, use the first child as the actual command node
  82. commandNode = commandNode.firstChild
  83. }
  84. if (!commandNode) {
  85. return
  86. }
  87. if (!nodeIntersectsChange(node.node)) {
  88. // This should already be in `items`
  89. return
  90. }
  91. const ctrlSeq = commandNode.getChild('$CtrlSeq')
  92. if (!ctrlSeq) {
  93. return
  94. }
  95. if (ctrlSeq.type.is('$CtrlSym')) {
  96. return
  97. }
  98. const optionalArguments = commandNode.getChildren('OptionalArgument')
  99. const commandArgumentsIncludingOptional =
  100. commandNode.getChildren('$Argument')
  101. const text = state.doc.sliceString(ctrlSeq.from, ctrlSeq.to)
  102. const thisCommand = {
  103. line: state.doc.lineAt(commandNode.from).number,
  104. title: text,
  105. from: commandNode.from,
  106. to: commandNode.to,
  107. optionalArgCount: optionalArguments.length,
  108. requiredArgCount:
  109. commandArgumentsIncludingOptional.length - optionalArguments.length,
  110. }
  111. items.push(thisCommand)
  112. }
  113. }