annotations.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { EditorView, ViewUpdate } from '@codemirror/view'
  2. import { Diagnostic, linter, lintGutter } from '@codemirror/lint'
  3. import {
  4. Compartment,
  5. Extension,
  6. RangeSet,
  7. RangeValue,
  8. StateEffect,
  9. StateField,
  10. Text,
  11. } from '@codemirror/state'
  12. import { Annotation } from '../../../../../types/annotation'
  13. import { debugConsole } from '@/utils/debugging'
  14. import { sendMB } from '@/infrastructure/event-tracking'
  15. const compileLintSourceConf = new Compartment()
  16. export const annotations = () => [
  17. compileDiagnosticsState,
  18. compileLintSourceConf.of(compileLogLintSource()),
  19. /**
  20. * The built-in lint gutter extension, configured with zero hover delay.
  21. */
  22. lintGutter({
  23. hoverTime: 0,
  24. }),
  25. /**
  26. * A theme which moves the lint gutter outside the line numbers.
  27. */
  28. EditorView.baseTheme({
  29. '.cm-gutter-lint': {
  30. order: -1,
  31. },
  32. }),
  33. ]
  34. export const lintSourceConfig = {
  35. delay: 100,
  36. // Show highlights only for errors
  37. markerFilter(diagnostics: readonly Diagnostic[]) {
  38. return diagnostics.filter(d => d.severity === 'error')
  39. },
  40. // Do not show any tooltips for highlights within the editor content
  41. tooltipFilter() {
  42. return []
  43. },
  44. needsRefresh(update: ViewUpdate) {
  45. return update.selectionSet
  46. },
  47. }
  48. /**
  49. * A lint source using the compile log diagnostics
  50. */
  51. const compileLogLintSource = (): Extension =>
  52. linter(view => {
  53. const items: Diagnostic[] = []
  54. const cursor = view.state.field(compileDiagnosticsState).iter()
  55. while (cursor.value !== null) {
  56. const { diagnostic } = cursor.value
  57. items.push({
  58. ...diagnostic,
  59. from: cursor.from,
  60. to: cursor.to,
  61. renderMessage: () => renderMessage(diagnostic),
  62. })
  63. cursor.next()
  64. }
  65. return items
  66. }, lintSourceConfig)
  67. interface CompileLogDiagnostic extends Diagnostic {
  68. compile?: true
  69. ruleId?: string
  70. }
  71. class CompileLogDiagnosticRangeValue extends RangeValue {
  72. constructor(public diagnostic: CompileLogDiagnostic) {
  73. super()
  74. }
  75. }
  76. const setCompileDiagnosticsEffect = StateEffect.define<CompileLogDiagnostic[]>()
  77. /**
  78. * A state field for the compile log diagnostics
  79. */
  80. export const compileDiagnosticsState = StateField.define<
  81. RangeSet<CompileLogDiagnosticRangeValue>
  82. >({
  83. create() {
  84. return RangeSet.empty
  85. },
  86. update(value, transaction) {
  87. for (const effect of transaction.effects) {
  88. if (effect.is(setCompileDiagnosticsEffect)) {
  89. return RangeSet.of(
  90. effect.value.map(diagnostic =>
  91. new CompileLogDiagnosticRangeValue(diagnostic).range(
  92. diagnostic.from,
  93. diagnostic.to
  94. )
  95. ),
  96. true
  97. )
  98. }
  99. }
  100. if (transaction.docChanged) {
  101. value = value.map(transaction.changes)
  102. }
  103. return value
  104. },
  105. })
  106. export const setAnnotations = (doc: Text, annotations: Annotation[]) => {
  107. const diagnostics: Diagnostic[] = []
  108. for (const annotation of annotations) {
  109. // ignore "whole document" (row: -1) annotations
  110. if (annotation.row !== -1) {
  111. try {
  112. diagnostics.push(convertAnnotationToDiagnostic(doc, annotation))
  113. } catch (error) {
  114. // ignore invalid annotations
  115. debugConsole.debug('invalid annotation position', error)
  116. }
  117. }
  118. }
  119. return {
  120. effects: setCompileDiagnosticsEffect.of(diagnostics),
  121. }
  122. }
  123. export const showCompileLogDiagnostics = (show: boolean) => {
  124. return {
  125. effects: [
  126. // reconfigure the compile log lint source
  127. compileLintSourceConf.reconfigure(show ? compileLogLintSource() : []),
  128. ],
  129. }
  130. }
  131. const convertAnnotationToDiagnostic = (
  132. doc: Text,
  133. annotation: Annotation
  134. ): CompileLogDiagnostic => {
  135. if (annotation.row < 0) {
  136. throw new Error(`Invalid annotation row ${annotation.row}`)
  137. }
  138. const line = doc.line(annotation.row + 1)
  139. return {
  140. from: line.from,
  141. to: line.to, // NOTE: highlight whole line as synctex doesn't output column number
  142. severity: annotation.type,
  143. message: annotation.text,
  144. ruleId: annotation.ruleId,
  145. compile: true,
  146. }
  147. }
  148. export const renderMessage = (
  149. diagnostic: Pick<
  150. CompileLogDiagnostic,
  151. 'message' | 'severity' | 'ruleId' | 'compile'
  152. >
  153. ) => {
  154. const { message, severity, ruleId, compile = false } = diagnostic
  155. const div = document.createElement('div')
  156. div.textContent = message
  157. window.setTimeout(() => {
  158. if (div.isConnected) {
  159. sendMB('lint-gutter-marker-view', { severity, ruleId, compile })
  160. }
  161. }, 500) // 500ms delay to indicate intention, rather than accidental hover
  162. return div
  163. }