diff --git a/packages/hoppscotch-app/components/smart/EnvInput.vue b/packages/hoppscotch-app/components/smart/EnvInput.vue index 761cc4564..3fd0d251f 100644 --- a/packages/hoppscotch-app/components/smart/EnvInput.vue +++ b/packages/hoppscotch-app/components/smart/EnvInput.vue @@ -35,10 +35,12 @@ import { EditorState, Extension } from "@codemirror/state" import clone from "lodash/clone" import { tooltips } from "@codemirror/tooltip" import { history, historyKeymap } from "@codemirror/history" +import { HoppRESTVar } from "@hoppscotch/data" import { inputTheme } from "~/helpers/editor/themes/baseTheme" import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment" import { useReadonlyStream } from "~/helpers/utils/composables" import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments" +import { HoppReactiveVarPlugin } from "~/helpers/editor/extensions/HoppVariable" const props = withDefaults( defineProps<{ @@ -46,6 +48,7 @@ const props = withDefaults( placeholder: string styles: string envs: { key: string; value: string; source: string }[] | null + vars: { key: string; value: string }[] | null focus: boolean readonly: boolean }>(), @@ -54,6 +57,7 @@ const props = withDefaults( placeholder: "", styles: "", envs: null, + vars: null, focus: false, readonly: false, } @@ -120,7 +124,17 @@ const envVars = computed(() => : aggregateEnvs.value ) +const varVars = computed(() => + props.vars + ? props.vars.map((x) => ({ + key: x.key, + value: x.value, + })) + : ([{ key: "size", value: "500" }] as HoppRESTVar[]) +) + const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view) +const varTooltipPlugin = new HoppReactiveVarPlugin(varVars, view) const initView = (el: any) => { const extensions: Extension = [ @@ -146,6 +160,7 @@ const initView = (el: any) => { position: "absolute", }), envTooltipPlugin, + varTooltipPlugin, placeholderExt(props.placeholder), EditorView.domEventHandlers({ paste(ev) { diff --git a/packages/hoppscotch-app/helpers/editor/extensions/HoppVariable.ts b/packages/hoppscotch-app/helpers/editor/extensions/HoppVariable.ts new file mode 100644 index 000000000..401f00a98 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/extensions/HoppVariable.ts @@ -0,0 +1,180 @@ +import { watch, Ref } from "@nuxtjs/composition-api" +import { Compartment } from "@codemirror/state" +import { hoverTooltip } from "@codemirror/tooltip" +import { + Decoration, + EditorView, + MatchDecorator, + ViewPlugin, +} from "@codemirror/view" +import * as E from "fp-ts/Either" +import { HoppRESTVar, parseTemplateStringE } from "@hoppscotch/data" + +const HOPP_ENVIRONMENT_REGEX = /({{\w+}})/g + +const HOPP_ENV_HIGHLIGHT = + "cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight" +const HOPP_ENV_HIGHLIGHT_FOUND = + "bg-accentDark text-accentContrast hover:bg-accent" +const HOPP_ENV_HIGHLIGHT_NOT_FOUND = + "bg-red-500 text-accentContrast hover:bg-red-600" + +const cursorTooltipField = (aggregateEnvs: HoppRESTVar[]) => + hoverTooltip( + (view, pos, side) => { + const { from, to, text } = view.state.doc.lineAt(pos) + + // TODO: When Codemirror 6 allows this to work (not make the + // popups appear half of the time) use this implementation + // const wordSelection = view.state.wordAt(pos) + // if (!wordSelection) return null + // const word = view.state.doc.sliceString( + // wordSelection.from - 2, + // wordSelection.to + 2 + // ) + // if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null + + // Tracking the start and the end of the words + let start = pos + let end = pos + + while (start > from && /\w/.test(text[start - from - 1])) start-- + while (end < to && /\w/.test(text[end - from])) end++ + + if ( + (start === pos && side < 0) || + (end === pos && side > 0) || + !HOPP_ENVIRONMENT_REGEX.test( + text.slice(start - from - 2, end - from + 2) + ) + ) + return null + + const envValue = + aggregateEnvs.find( + (env) => env.key === text.slice(start - from, end - from) + // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2) + )?.value ?? "not found" + + const result = parseTemplateStringE(envValue, aggregateEnvs) + + const finalEnv = E.isLeft(result) ? "error" : result.right + + return { + pos: start, + end: to, + above: true, + arrow: true, + create() { + const dom = document.createElement("span") + const xmp = document.createElement("xmp") + xmp.textContent = finalEnv + dom.appendChild(xmp) + dom.className = "tooltip-theme" + return { dom } + }, + } + }, + // HACK: This is a hack to fix hover tooltip not coming half of the time + // https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622 + // Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent + { hoverTime: 1 } as any + ) + +function checkEnv(env: string, aggregateEnvs: HoppRESTVar[]) { + const className = aggregateEnvs.find( + (k: { key: string }) => k.key === env.slice(2, -2) + ) + ? HOPP_ENV_HIGHLIGHT_FOUND + : HOPP_ENV_HIGHLIGHT_NOT_FOUND + + return Decoration.mark({ + class: `${HOPP_ENV_HIGHLIGHT} ${className}`, + }) +} + +const getMatchDecorator = (aggregateEnvs: HoppRESTVar[]) => + new MatchDecorator({ + regexp: HOPP_ENVIRONMENT_REGEX, + decoration: (m) => checkEnv(m[0], aggregateEnvs), + }) + +export const environmentHighlightStyle = (aggregateEnvs: HoppRESTVar[]) => { + const decorator = getMatchDecorator(aggregateEnvs) + + return ViewPlugin.define( + (view) => ({ + decorations: decorator.createDeco(view), + update(u) { + this.decorations = decorator.updateDeco(u, this.decorations) + }, + }), + { + decorations: (v) => v.decorations, + } + ) +} + +// export class HoppEnvironmentPlugin { +// private compartment = new Compartment() +// +// private envs: AggregateEnvironment[] = [] +// +// constructor( +// subscribeToStream: StreamSubscriberFunc, +// private editorView: Ref +// ) { +// this.envs = getAggregateEnvs() +// +// subscribeToStream(aggregateEnvs$, (envs) => { +// this.envs = envs +// +// this.editorView.value?.dispatch({ +// effects: this.compartment.reconfigure([ +// cursorTooltipField(this.envs), +// environmentHighlightStyle(this.envs), +// ]), +// }) +// }) +// } +// +// get extension() { +// return this.compartment.of([ +// cursorTooltipField(this.envs), +// environmentHighlightStyle(this.envs), +// ]) +// } +// } + +export class HoppReactiveVarPlugin { + private compartment = new Compartment() + + private envs: HoppRESTVar[] = [] + + constructor( + envsRef: Ref, + private editorView: Ref + ) { + watch( + envsRef, + (envs) => { + this.envs = envs + + this.editorView.value?.dispatch({ + effects: this.compartment.reconfigure([ + cursorTooltipField(this.envs), + environmentHighlightStyle(this.envs), + ]), + }) + }, + { immediate: true } + ) + } + + get extension() { + return this.compartment.of([ + cursorTooltipField(this.envs), + environmentHighlightStyle(this.envs), + ]) + } +}