HoppEnvironment.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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 = /(<<\w+>>)/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 && /\w/.test(text[start - from - 1])) start--
  42. while (end < to && /\w/.test(text[end - from])) end++
  43. if (
  44. (start === pos && side < 0) ||
  45. (end === pos && side > 0) ||
  46. !HOPP_ENVIRONMENT_REGEX.test(
  47. text.slice(start - from - 2, end - from + 2)
  48. )
  49. )
  50. return null
  51. const envName =
  52. aggregateEnvs.find(
  53. (env) => env.key === text.slice(start - from, end - from)
  54. // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
  55. )?.sourceEnv ?? "choose an environment"
  56. const envValue =
  57. aggregateEnvs.find(
  58. (env) => env.key === text.slice(start - from, end - from)
  59. // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2)
  60. )?.value ?? "not found"
  61. const result = parseTemplateStringE(envValue, aggregateEnvs)
  62. const finalEnv = E.isLeft(result) ? "error" : result.right
  63. return {
  64. pos: start,
  65. end: to,
  66. above: true,
  67. arrow: true,
  68. create() {
  69. const dom = document.createElement("span")
  70. const xmp = document.createElement("xmp")
  71. xmp.textContent = finalEnv
  72. dom.appendChild(document.createTextNode(`${envName} `))
  73. dom.appendChild(xmp)
  74. dom.className = "tooltip-theme"
  75. return { dom }
  76. },
  77. }
  78. },
  79. // HACK: This is a hack to fix hover tooltip not coming half of the time
  80. // https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
  81. // Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
  82. { hoverTime: 1 } as any
  83. )
  84. function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
  85. const className = aggregateEnvs.find(
  86. (k: { key: string }) => k.key === env.slice(2, -2)
  87. )
  88. ? HOPP_ENV_HIGHLIGHT_FOUND
  89. : HOPP_ENV_HIGHLIGHT_NOT_FOUND
  90. return Decoration.mark({
  91. class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
  92. })
  93. }
  94. const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
  95. new MatchDecorator({
  96. regexp: HOPP_ENVIRONMENT_REGEX,
  97. decoration: (m) => checkEnv(m[0], aggregateEnvs),
  98. })
  99. export const environmentHighlightStyle = (
  100. aggregateEnvs: AggregateEnvironment[]
  101. ) => {
  102. const decorator = getMatchDecorator(aggregateEnvs)
  103. return ViewPlugin.define(
  104. (view) => ({
  105. decorations: decorator.createDeco(view),
  106. update(u) {
  107. this.decorations = decorator.updateDeco(u, this.decorations)
  108. },
  109. }),
  110. {
  111. decorations: (v) => v.decorations,
  112. }
  113. )
  114. }
  115. export class HoppEnvironmentPlugin {
  116. private compartment = new Compartment()
  117. private envs: AggregateEnvironment[] = []
  118. constructor(
  119. subscribeToStream: StreamSubscriberFunc,
  120. private editorView: Ref<EditorView | undefined>
  121. ) {
  122. this.envs = getAggregateEnvs()
  123. subscribeToStream(aggregateEnvs$, (envs) => {
  124. this.envs = envs
  125. this.editorView.value?.dispatch({
  126. effects: this.compartment.reconfigure([
  127. cursorTooltipField(this.envs),
  128. environmentHighlightStyle(this.envs),
  129. ]),
  130. })
  131. })
  132. }
  133. get extension() {
  134. return this.compartment.of([
  135. cursorTooltipField(this.envs),
  136. environmentHighlightStyle(this.envs),
  137. ])
  138. }
  139. }
  140. export class HoppReactiveEnvPlugin {
  141. private compartment = new Compartment()
  142. private envs: AggregateEnvironment[] = []
  143. constructor(
  144. envsRef: Ref<AggregateEnvironment[]>,
  145. private editorView: Ref<EditorView | undefined>
  146. ) {
  147. watch(
  148. envsRef,
  149. (envs) => {
  150. this.envs = envs
  151. this.editorView.value?.dispatch({
  152. effects: this.compartment.reconfigure([
  153. cursorTooltipField(this.envs),
  154. environmentHighlightStyle(this.envs),
  155. ]),
  156. })
  157. },
  158. { immediate: true }
  159. )
  160. }
  161. get extension() {
  162. return this.compartment.of([
  163. cursorTooltipField(this.envs),
  164. environmentHighlightStyle(this.envs),
  165. ])
  166. }
  167. }