commands.ts 3.6 KB

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