From fe5fe03b3cb43da6362aa670ab08fb76b80a2ffd Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Sun, 12 Dec 2021 20:36:49 +0530 Subject: [PATCH] fix: reactivity issues --- .../components/http/RawBody.vue | 1 + .../helpers/editor/codemirror.ts | 123 ++++++++------- .../editor/extensions/HoppEnvironment.ts | 146 ++++++++++++++++++ .../editor/extensions/environmentTooltip.ts | 105 ------------- .../hoppscotch-app/newstore/environments.ts | 25 ++- 5 files changed, 235 insertions(+), 165 deletions(-) create mode 100644 packages/hoppscotch-app/helpers/editor/extensions/HoppEnvironment.ts delete mode 100644 packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts diff --git a/packages/hoppscotch-app/components/http/RawBody.vue b/packages/hoppscotch-app/components/http/RawBody.vue index 58d62343f..64bc45da3 100644 --- a/packages/hoppscotch-app/components/http/RawBody.vue +++ b/packages/hoppscotch-app/components/http/RawBody.vue @@ -91,6 +91,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: true, }) ) diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts index cd20dee12..b2a1e1744 100644 --- a/packages/hoppscotch-app/helpers/editor/codemirror.ts +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -34,10 +34,7 @@ import { useStreamSubscriber } from "../utils/composables" import { Completer } from "./completion" import { LinterDefinition } from "./linting/linter" import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" -import { - environmentHighlightStyle, - environmentTooltip, -} from "./extensions/environmentTooltip" +import { HoppEnvironmentPlugin } from "./extensions/HoppEnvironment" type ExtendedEditorConfig = { mode: string @@ -50,6 +47,9 @@ type CodeMirrorOptions = { extendedEditorConfig: Partial linter: LinterDefinition | null completer: Completer | null + + // NOTE: This property is not reactive + environmentHighlights: boolean } const hoppCompleterExt = (completer: Completer): Extension => { @@ -154,6 +154,7 @@ export function useCodemirror( options: CodeMirrorOptions ): { cursor: Ref<{ line: number; ch: number }> } { const { subscribeToStream } = useStreamSubscriber() + const language = new Compartment() const lineWrapping = new Compartment() const placeholderConfig = new Compartment() @@ -171,66 +172,70 @@ export function useCodemirror( const view = ref() + const environmentTooltip = options.environmentHighlights + ? new HoppEnvironmentPlugin(subscribeToStream, view) + : null + const initView = (el: any) => { + const extensions = [ + basicSetup, + baseTheme, + baseHighlightStyle, + ViewPlugin.fromClass( + class { + update(update: ViewUpdate) { + if (update.selectionSet) { + const cursorPos = update.state.selection.main.head + + const line = update.state.doc.lineAt(cursorPos) + + cachedCursor.value = { + line: line.number - 1, + ch: cursorPos - line.from, + } + + cursor.value = { + line: cachedCursor.value.line, + ch: cachedCursor.value.ch, + } + } + if (update.docChanged) { + // Expensive on big files ? + cachedValue.value = update.state.doc + .toJSON() + .join(update.state.lineBreak) + if (!options.extendedEditorConfig.readOnly) + value.value = cachedValue.value + } + } + } + ), + EditorState.changeFilter.of(() => !options.extendedEditorConfig.readOnly), + placeholderConfig.of( + placeholder(options.extendedEditorConfig.placeholder ?? "") + ), + language.of( + getEditorLanguage( + options.extendedEditorConfig.mode ?? "", + options.linter ?? undefined, + options.completer ?? undefined + ) + ), + lineWrapping.of( + options.extendedEditorConfig.lineWrapping + ? [EditorView.lineWrapping] + : [] + ), + keymap.of(defaultKeymap), + ] + + if (environmentTooltip) extensions.push(environmentTooltip.extension) + view.value = new EditorView({ parent: el, state: EditorState.create({ doc: value.value, - extensions: [ - basicSetup, - baseTheme, - baseHighlightStyle, - environmentTooltip(subscribeToStream), - environmentHighlightStyle, - ViewPlugin.fromClass( - class { - update(update: ViewUpdate) { - if (update.selectionSet) { - const cursorPos = update.state.selection.main.head - - const line = update.state.doc.lineAt(cursorPos) - - cachedCursor.value = { - line: line.number - 1, - ch: cursorPos - line.from, - } - - cursor.value = { - line: cachedCursor.value.line, - ch: cachedCursor.value.ch, - } - } - if (update.docChanged) { - // Expensive on big files ? - cachedValue.value = update.state.doc - .toJSON() - .join(update.state.lineBreak) - if (!options.extendedEditorConfig.readOnly) - value.value = cachedValue.value - } - } - } - ), - EditorState.changeFilter.of( - () => !options.extendedEditorConfig.readOnly - ), - placeholderConfig.of( - placeholder(options.extendedEditorConfig.placeholder ?? "") - ), - language.of( - getEditorLanguage( - options.extendedEditorConfig.mode ?? "", - options.linter ?? undefined, - options.completer ?? undefined - ) - ), - lineWrapping.of( - options.extendedEditorConfig.lineWrapping - ? [EditorView.lineWrapping] - : [] - ), - keymap.of(defaultKeymap), - ], + extensions, }), }) } diff --git a/packages/hoppscotch-app/helpers/editor/extensions/HoppEnvironment.ts b/packages/hoppscotch-app/helpers/editor/extensions/HoppEnvironment.ts new file mode 100644 index 000000000..2dbb28635 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/extensions/HoppEnvironment.ts @@ -0,0 +1,146 @@ +import { Compartment } from "@codemirror/state" +import { hoverTooltip } from "@codemirror/tooltip" +import { + Decoration, + EditorView, + MatchDecorator, + ViewPlugin, +} from "@codemirror/view" +import { Ref } from "@nuxtjs/composition-api" +import { StreamSubscriberFunc } from "~/helpers/utils/composables" +import { + AggregateEnvironment, + aggregateEnvs$, + getAggregateEnvs, +} from "~/newstore/environments" + +const HOPP_ENVIRONMENT_REGEX = /(<<\w+>>)/g + +const HOPP_ENV_HIGHLIGHT = + "cursor-help transition rounded px-1 focus:outline-none mx-0.5" +const HOPP_ENV_HIGHLIGHT_FOUND = + "bg-accentDark text-accentContrast hover:bg-accent" +const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "bg-red-400 text-red-50 hover:bg-red-600" + +const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) => + 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 envName = + aggregateEnvs.find( + (env) => env.key === text.slice(start - from, end - from) + // env.key === word.slice(wordSelection.from + 2, wordSelection.to - 2) + )?.sourceEnv ?? "choose an environment" + + 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" + ).replace(/"/g, """) + + const textContent = `${envName} ${envValue}` + + return { + pos: start, + end: to, + above: true, + create() { + const dom = document.createElement("span") + dom.innerHTML = textContent + dom.className = "tooltip-theme" + return { dom } + }, + } + }) + +function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) { + 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: AggregateEnvironment[]) => + new MatchDecorator({ + regexp: HOPP_ENVIRONMENT_REGEX, + decoration: (m) => checkEnv(m[0], aggregateEnvs), + }) + +export const environmentHighlightStyle = ( + aggregateEnvs: AggregateEnvironment[] +) => { + 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), + ]) + } +} diff --git a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts deleted file mode 100644 index d14ba18fc..000000000 --- a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Extension } from "@codemirror/state" -import { hoverTooltip } from "@codemirror/tooltip" -import { Decoration, MatchDecorator, ViewPlugin } from "@codemirror/view" -import { - StreamSubscriberFunc, - useReadonlyStream, -} from "~/helpers/utils/composables" -import { aggregateEnvs$ } from "~/newstore/environments" - -const cursorTooltipField = (subscribeToStream: StreamSubscriberFunc) => - hoverTooltip((view, pos, side) => { - const { from, to, text } = view.state.doc.lineAt(pos) - 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) || - !/(<<\w+>>)/g.test(text.slice(start - from - 2, end - from + 2)) - ) - return null - - let textContent: string - subscribeToStream(aggregateEnvs$, (envs) => { - const envName = getEnvName( - envs.find( - (env: { key: string }) => - env.key === text.slice(start - from, end - from) - )?.sourceEnv - ) - const envValue = getEnvValue( - envs.find( - (env: { key: string }) => - env.key === text.slice(start - from, end - from) - )?.value - ) - textContent = `${envName} ${envValue}` - }) - - return { - pos: start, - end, - above: true, - create() { - const dom = document.createElement("span") - dom.innerHTML = textContent - dom.className = "tooltip-theme" - return { dom } - }, - } - }) - -function getEnvName(name: any) { - if (name) return name - return "choose an environment" -} - -function getEnvValue(value: string | undefined) { - if (value) return value.replace(/"/g, """) - // it does not filter special characters before adding them to HTML. - return "not found" -} - -function checkEnv(env: string) { - const envHighlight = - "cursor-help transition rounded px-1 focus:outline-none mx-0.5" - const envFound = "bg-accentDark text-accentContrast hover:bg-accent" - const envNotFound = "bg-red-400 text-red-50 hover:bg-red-600" - const aggregateEnvs = useReadonlyStream(aggregateEnvs$, null) - const className = - aggregateEnvs.value?.find( - (k: { key: string }) => k.key === env.slice(2, -2) - )?.value === undefined - ? envNotFound - : envFound - return Decoration.mark({ - class: `${envHighlight} ${className}`, - }) -} - -const decorator = new MatchDecorator({ - regexp: /(<<\w+>>)/g, - decoration: (m) => checkEnv(m[0]), -}) - -export const environmentHighlightStyle = ViewPlugin.define( - (view) => ({ - decorations: decorator.createDeco(view), - update(u) { - this.decorations = decorator.updateDeco(u, this.decorations) - }, - }), - { - decorations: (v) => v.decorations, - } -) - -export const environmentTooltip: ( - subscribeToStream: StreamSubscriberFunc -) => Extension = (subscribeToStream: StreamSubscriberFunc) => { - return [cursorTooltipField(subscribeToStream), environmentHighlightStyle] -} diff --git a/packages/hoppscotch-app/newstore/environments.ts b/packages/hoppscotch-app/newstore/environments.ts index b735956b1..a1e23fb84 100644 --- a/packages/hoppscotch-app/newstore/environments.ts +++ b/packages/hoppscotch-app/newstore/environments.ts @@ -285,7 +285,7 @@ export const currentEnvironment$ = combineLatest([ }) ) -type AggregateEnvironment = { +export type AggregateEnvironment = { key: string value: string sourceEnv: string @@ -314,6 +314,29 @@ export const aggregateEnvs$: Observable = combineLatest( distinctUntilChanged(isEqual) ) +export function getAggregateEnvs() { + const currentEnv = getCurrentEnvironment() + + return [ + ...currentEnv.variables.map( + (x) => + { + key: x.key, + value: x.value, + sourceEnv: currentEnv.name, + } + ), + ...getGlobalVariables().map( + (x) => + { + key: x.key, + value: x.value, + sourceEnv: "Global", + } + ), + ] +} + export function getCurrentEnvironment(): Environment { if (environmentsStore.value.currentEnvironmentIndex === -1) { return {