HoppEnvironment.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { watch, Ref } from "@nuxtjs/composition-api"
  2. import { Compartment } from "@codemirror/state"
  3. import { hoverTooltip } from "@codemirror/tooltip"
  4. import {
  5. Decoration,
  6. EditorView,
  7. MatchDecorator,
  8. ViewPlugin,
  9. } from "@codemirror/view"
  10. import * as E from "fp-ts/Either"
  11. import { parseTemplateStringE } from "@hoppscotch/data"
  12. import { StreamSubscriberFunc } from "~/helpers/utils/composables"
  13. import {
  14. AggregateEnvironment,
  15. aggregateEnvs$,
  16. getAggregateEnvs,
  17. } from "~/newstore/environments"
  18. const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
  19. const HOPP_ENV_HIGHLIGHT =
  20. "cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
  21. const HOPP_ENV_HIGHLIGHT_FOUND =
  22. "bg-accentDark text-accentContrast hover:bg-accent"
  23. const HOPP_ENV_HIGHLIGHT_NOT_FOUND =
  24. "bg-red-500 text-accentContrast hover:bg-red-600"
  25. const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
  26. hoverTooltip(
  27. (view, pos, side) => {
  28. const { from, to, text } = view.state.doc.lineAt(pos)
  29. // TODO: When Codemirror 6 allows this to work (not make the
  30. // popups appear half of the time) use this implementation
  31. // const wordSelection = view.state.wordAt(pos)
  32. // if (!wordSelection) return null
  33. // const word = view.state.doc.sliceString(
  34. // wordSelection.from - 2,
  35. // wordSelection.to + 2
  36. // )
  37. // if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null
  38. // Tracking the start and the end of the words
  39. let start = pos
  40. let end = pos
  41. while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
  42. start--
  43. while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
  44. if (
  45. (start === pos && side < 0) ||
  46. (end === pos && side > 0) ||
  47. !HOPP_ENVIRONMENT_REGEX.test(
  48. text.slice(start - from - 2, end - from + 2)
  49. )
  50. )
  51. return null
  52. const envName =
  53. aggregateEnvs.find(
  54. (env) => env.key === text.slice(start - from, end - from)
  55. // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
  56. )?.sourceEnv ?? "choose an environment"
  57. const envValue =
  58. aggregateEnvs.find(
  59. (env) => env.key === text.slice(start - from, end - from)
  60. // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
  61. )?.value ?? "not found"
  62. const result = parseTemplateStringE(envValue, aggregateEnvs)
  63. const finalEnv = E.isLeft(result) ? "error" : result.right
  64. return {
  65. pos: start,
  66. end: to,
  67. above: true,
  68. arrow: true,
  69. create() {
  70. const dom = document.createElement("span")
  71. const xmp = document.createElement("xmp")
  72. xmp.textContent = finalEnv
  73. dom.appendChild(document.createTextNode(`${envName} `))
  74. dom.appendChild(xmp)
  75. dom.className = "tooltip-theme"
  76. return { dom }
  77. },
  78. }
  79. },
  80. // HACK: This is a hack to fix hover tooltip not coming half of the time
  81. // https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
  82. // Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
  83. { hoverTime: 1 } as any
  84. )
  85. function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
  86. const className = aggregateEnvs.find(
  87. (k: { key: string }) => k.key === env.slice(2, -2)
  88. )
  89. ? HOPP_ENV_HIGHLIGHT_FOUND
  90. : HOPP_ENV_HIGHLIGHT_NOT_FOUND
  91. return Decoration.mark({
  92. class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
  93. })
  94. }
  95. const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
  96. new MatchDecorator({
  97. regexp: HOPP_ENVIRONMENT_REGEX,
  98. decoration: (m) => checkEnv(m[0], aggregateEnvs),
  99. })
  100. export const environmentHighlightStyle = (
  101. aggregateEnvs: AggregateEnvironment[]
  102. ) => {
  103. const decorator = getMatchDecorator(aggregateEnvs)
  104. return ViewPlugin.define(
  105. (view) => ({
  106. decorations: decorator.createDeco(view),
  107. update(u) {
  108. this.decorations = decorator.updateDeco(u, this.decorations)
  109. },
  110. }),
  111. {
  112. decorations: (v) => v.decorations,
  113. }
  114. )
  115. }
  116. export class HoppEnvironmentPlugin {
  117. private compartment = new Compartment()
  118. private envs: AggregateEnvironment[] = []
  119. constructor(
  120. subscribeToStream: StreamSubscriberFunc,
  121. private editorView: Ref<EditorView | undefined>
  122. ) {
  123. this.envs = getAggregateEnvs()
  124. subscribeToStream(aggregateEnvs$, (envs) => {
  125. this.envs = envs
  126. this.editorView.value?.dispatch({
  127. effects: this.compartment.reconfigure([
  128. cursorTooltipField(this.envs),
  129. environmentHighlightStyle(this.envs),
  130. ]),
  131. })
  132. })
  133. }
  134. get extension() {
  135. return this.compartment.of([
  136. cursorTooltipField(this.envs),
  137. environmentHighlightStyle(this.envs),
  138. ])
  139. }
  140. }
  141. export class HoppReactiveEnvPlugin {
  142. private compartment = new Compartment()
  143. private envs: AggregateEnvironment[] = []
  144. constructor(
  145. envsRef: Ref<AggregateEnvironment[]>,
  146. private editorView: Ref<EditorView | undefined>
  147. ) {
  148. watch(
  149. envsRef,
  150. (envs) => {
  151. this.envs = envs
  152. this.editorView.value?.dispatch({
  153. effects: this.compartment.reconfigure([
  154. cursorTooltipField(this.envs),
  155. environmentHighlightStyle(this.envs),
  156. ]),
  157. })
  158. },
  159. { immediate: true }
  160. )
  161. }
  162. get extension() {
  163. return this.compartment.of([
  164. cursorTooltipField(this.envs),
  165. environmentHighlightStyle(this.envs),
  166. ])
  167. }
  168. }