From 50a57433d084ea08d47f5f3549dc5104768136f1 Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Fri, 10 Dec 2021 09:27:25 +0530 Subject: [PATCH 1/6] feat: init environment variables tooltip for codemirror --- .../hoppscotch-app/assets/scss/styles.scss | 65 +++++++++---------- .../helpers/editor/codemirror.ts | 2 + .../editor/extensions/environmentTooltip.ts | 55 ++++++++++++++++ packages/hoppscotch-app/package.json | 1 + 4 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts diff --git a/packages/hoppscotch-app/assets/scss/styles.scss b/packages/hoppscotch-app/assets/scss/styles.scss index 7d06cefdd..b8f4647fc 100644 --- a/packages/hoppscotch-app/assets/scss/styles.scss +++ b/packages/hoppscotch-app/assets/scss/styles.scss @@ -136,44 +136,43 @@ a { } } -.tippy-popper { - .tooltip-theme { - @apply bg-tooltip; - @apply text-primary; - @apply font-semibold; - @apply py-1 px-2; +.tooltip-theme { + @apply bg-tooltip; + @apply text-primary; + @apply font-semibold; + @apply py-1 px-2; + @apply rounded; + @apply truncate; + @apply shadow; + + font-size: 88%; + line-height: var(--body-line-height); + + kbd { + @apply inline-flex; + @apply font-sans; + @apply bg-gray-500; + @apply bg-opacity-45; + @apply text-primaryLight; + @apply rounded-sm; + @apply px-1; + @apply ml-1; @apply truncate; - @apply shadow; - - font-size: 88%; - line-height: var(--body-line-height); - - kbd { - @apply inline-flex; - @apply font-sans; - @apply bg-gray-500; - @apply bg-opacity-45; - @apply text-primaryLight; - @apply rounded-sm; - @apply px-1; - @apply ml-1; - @apply truncate; - } } +} - .popover-theme { - @apply bg-popover; - @apply text-secondary; - @apply p-2; - @apply shadow-lg; - @apply focus:outline-none; +.popover-theme { + @apply bg-popover; + @apply text-secondary; + @apply p-2; + @apply shadow-lg; + @apply focus:outline-none; - font-size: var(--body-font-size); - line-height: var(--body-line-height); + font-size: var(--body-font-size); + line-height: var(--body-line-height); - .tippy-roundarrow svg { - @apply fill-popover; - } + .tippy-roundarrow svg { + @apply fill-popover; } } diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts index 381668559..21b05319e 100644 --- a/packages/hoppscotch-app/helpers/editor/codemirror.ts +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -33,6 +33,7 @@ import { isJSONContentType } from "../utils/contenttypes" import { Completer } from "./completion" import { LinterDefinition } from "./linting/linter" import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" +import { environmentTooltip } from "./extensions/environmentTooltip" type ExtendedEditorConfig = { mode: string @@ -174,6 +175,7 @@ export function useCodemirror( basicSetup, baseTheme, baseHighlightStyle, + environmentTooltip, ViewPlugin.fromClass( class { update(update: ViewUpdate) { diff --git a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts new file mode 100644 index 000000000..083151a82 --- /dev/null +++ b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts @@ -0,0 +1,55 @@ +import { Extension } from "@codemirror/state" +import { hoverTooltip } from "@codemirror/tooltip" +import { useReadonlyStream } from "~/helpers/utils/composables" +import { aggregateEnvs$ } from "~/newstore/environments" + +const cursorTooltipField = 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)) return null + if (!/(<<\w+>>)/g.test(text.slice(start - from - 2, end - from + 2))) + return null + + const aggregateEnvs = useReadonlyStream(aggregateEnvs$, null) + const envName = getEnvName( + aggregateEnvs.value?.find( + (env: { key: string }) => env.key === text.slice(start - from, end - from) + )?.sourceEnv + ) + const envValue = getEnvValue( + aggregateEnvs.value?.find( + (env: { key: string }) => env.key === text.slice(start - from, end - from) + )?.value + ) + const 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) { + if (value) return value.replace(/"/g, """) + // it does not filter special characters before adding them to HTML. + return "not found" +} + +export const environmentTooltip: Extension = cursorTooltipField diff --git a/packages/hoppscotch-app/package.json b/packages/hoppscotch-app/package.json index 48d08086c..d841c2a34 100644 --- a/packages/hoppscotch-app/package.json +++ b/packages/hoppscotch-app/package.json @@ -51,6 +51,7 @@ "@codemirror/search": "^0.19.4", "@codemirror/state": "^0.19.6", "@codemirror/text": "^0.19.5", + "@codemirror/tooltip": "^0.19.10", "@codemirror/view": "^0.19.26", "@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0", "@hoppscotch/data": "workspace:^0.1.0", From 8a7f3927b155d60962ec9b1005f07d97244f64e6 Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Sat, 11 Dec 2021 07:26:18 +0530 Subject: [PATCH 2/6] refactor: init environment highlighter --- .../helpers/editor/codemirror.ts | 6 ++- .../editor/extensions/environmentTooltip.ts | 53 +++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts index 21b05319e..902c76caa 100644 --- a/packages/hoppscotch-app/helpers/editor/codemirror.ts +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -33,7 +33,10 @@ import { isJSONContentType } from "../utils/contenttypes" import { Completer } from "./completion" import { LinterDefinition } from "./linting/linter" import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" -import { environmentTooltip } from "./extensions/environmentTooltip" +import { + environmentHighlightStyle, + environmentTooltip, +} from "./extensions/environmentTooltip" type ExtendedEditorConfig = { mode: string @@ -176,6 +179,7 @@ export function useCodemirror( baseTheme, baseHighlightStyle, environmentTooltip, + environmentHighlightStyle, ViewPlugin.fromClass( class { update(update: ViewUpdate) { diff --git a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts index 083151a82..6d1a672dd 100644 --- a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts +++ b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts @@ -1,5 +1,14 @@ import { Extension } from "@codemirror/state" import { hoverTooltip } from "@codemirror/tooltip" +import { + Decoration, + EditorView, + MatchDecorator, + PluginField, + ViewPlugin, + ViewUpdate, + WidgetType, +} from "@codemirror/view" import { useReadonlyStream } from "~/helpers/utils/composables" import { aggregateEnvs$ } from "~/newstore/environments" @@ -11,8 +20,11 @@ const cursorTooltipField = hoverTooltip((view, pos, side) => { 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)) return null - if (!/(<<\w+>>)/g.test(text.slice(start - from - 2, end - from + 2))) + if ( + (start === pos && side < 0) || + (end === pos && side > 0) || + !/(<<\w+>>)/g.test(text.slice(start - from - 2, end - from + 2)) + ) return null const aggregateEnvs = useReadonlyStream(aggregateEnvs$, null) @@ -52,4 +64,39 @@ function getEnvValue(value: string) { return "not found" } -export const environmentTooltip: Extension = cursorTooltipField +const environmentDecorator = new MatchDecorator({ + // eslint-disable-next-line prefer-regex-literals + regexp: new RegExp(/(<<\w+>>)/g), + decoration: () => + Decoration.replace({ + widget: new (class extends WidgetType { + toDOM(_view: EditorView) { + const element = document.createElement("span") + element.textContent = "view" + return element + } + })(), + inclusive: false, + }), +}) + +export const environmentHighlightStyle = ViewPlugin.define( + (view: EditorView) => ({ + decorations: environmentDecorator.createDeco(view), + update(_update: ViewUpdate) { + this.decorations = environmentDecorator.updateDeco( + _update, + this.decorations + ) + }, + }), + { + decorations: (v) => v.decorations, + provide: PluginField.atomicRanges.from((v) => v.decorations), + } +) + +export const environmentTooltip: Extension = [ + cursorTooltipField, + environmentHighlightStyle, +] From 67002a204e8c98fa56ad388ff7029cccf635e9a9 Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Sat, 11 Dec 2021 22:19:44 +0530 Subject: [PATCH 3/6] feat: highlight environment variables in codemirror --- .../editor/extensions/environmentTooltip.ts | 55 ++++++++----------- pnpm-lock.yaml | 23 +++----- 2 files changed, 33 insertions(+), 45 deletions(-) diff --git a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts index 6d1a672dd..1e27a4ea8 100644 --- a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts +++ b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts @@ -1,14 +1,6 @@ import { Extension } from "@codemirror/state" import { hoverTooltip } from "@codemirror/tooltip" -import { - Decoration, - EditorView, - MatchDecorator, - PluginField, - ViewPlugin, - ViewUpdate, - WidgetType, -} from "@codemirror/view" +import { Decoration, MatchDecorator, ViewPlugin } from "@codemirror/view" import { useReadonlyStream } from "~/helpers/utils/composables" import { aggregateEnvs$ } from "~/newstore/environments" @@ -64,35 +56,36 @@ function getEnvValue(value: string) { return "not found" } -const environmentDecorator = new MatchDecorator({ - // eslint-disable-next-line prefer-regex-literals - regexp: new RegExp(/(<<\w+>>)/g), - decoration: () => - Decoration.replace({ - widget: new (class extends WidgetType { - toDOM(_view: EditorView) { - const element = document.createElement("span") - element.textContent = "view" - return element - } - })(), - inclusive: false, - }), +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: EditorView) => ({ - decorations: environmentDecorator.createDeco(view), - update(_update: ViewUpdate) { - this.decorations = environmentDecorator.updateDeco( - _update, - this.decorations - ) + (view) => ({ + decorations: decorator.createDeco(view), + update(u) { + this.decorations = decorator.updateDeco(u, this.decorations) }, }), { decorations: (v) => v.decorations, - provide: PluginField.atomicRanges.from((v) => v.decorations), } ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c56bdf62..3d4710a34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,7 @@ importers: '@codemirror/search': ^0.19.4 '@codemirror/state': ^0.19.6 '@codemirror/text': ^0.19.5 + '@codemirror/tooltip': ^0.19.10 '@codemirror/view': ^0.19.26 '@graphql-codegen/add': ^3.1.0 '@graphql-codegen/cli': 2.3.0 @@ -180,6 +181,7 @@ importers: '@codemirror/search': 0.19.4 '@codemirror/state': 0.19.6 '@codemirror/text': 0.19.5 + '@codemirror/tooltip': 0.19.10 '@codemirror/view': 0.19.26 '@hoppscotch/codemirror-lang-graphql': link:../codemirror-lang-graphql '@hoppscotch/data': link:../hoppscotch-data @@ -1746,7 +1748,7 @@ packages: '@codemirror/panel': 0.19.0 '@codemirror/rangeset': 0.19.2 '@codemirror/state': 0.19.6 - '@codemirror/tooltip': 0.19.8 + '@codemirror/tooltip': 0.19.10 '@codemirror/view': 0.19.26 crelt: 1.0.5 dev: false @@ -1815,13 +1817,6 @@ packages: '@codemirror/view': 0.19.26 dev: false - /@codemirror/tooltip/0.19.8: - resolution: {integrity: sha512-Xg1H50utH3z1rmyzk5l/dfE0Lko+5pkxzaVlVzAbcqHlDsG9vARDkgRX+fEEpWg/rrvR83GVQhdKwl+wNxjOAg==} - dependencies: - '@codemirror/state': 0.19.6 - '@codemirror/view': 0.19.26 - dev: false - /@codemirror/view/0.19.26: resolution: {integrity: sha512-7QfXtFLDqXY2TfdxPCQ/NvXjINGaqXQ6SAHKQmxZ+jDcTCWmhFcxaAkrDneqcfGmtp72tUPOXG9PiwCbRWgRLw==} dependencies: @@ -3506,11 +3501,11 @@ packages: ufo: 0.7.9 dev: false - /@nuxt/kit-edge/3.0.0-27307420.6a25d3e: - resolution: {integrity: sha512-JieTRigkV52VEQy+oqa6OqR/qOuL9ZmoaH9fDHNwHJXN7hLmil4HbRQ9502G7ura7hkHeAhjZTthXdQDKx1Q5Q==} + /@nuxt/kit-edge/3.0.0-27319101.3e82f0f: + resolution: {integrity: sha512-WASRt4RJp1Zdrm9AbW+diEVss6qv89cCuPbbqnqdmZMQPYepfXfB75dB/GiULeoy5sIokXJLNz7WqSCSL0INjw==} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} dependencies: - '@nuxt/schema': /@nuxt/schema-edge/3.0.0-27307420.6a25d3e + '@nuxt/schema': /@nuxt/schema-edge/3.0.0-27319101.3e82f0f consola: 2.15.3 defu: 5.0.0 dotenv: 10.0.0 @@ -3531,7 +3526,7 @@ packages: /@nuxt/kit/0.8.1-edge: resolution: {integrity: sha512-7kU+mYxRy3w9UohFK/rfrPkKXM9A4LWsTqpFN3MH7mxohy98SFBkf87B6nqE6ulXmztInK+MptS0Lr+VQa0E6w==} dependencies: - '@nuxt/kit-edge': 3.0.0-27307420.6a25d3e + '@nuxt/kit-edge': 3.0.0-27319101.3e82f0f dev: true /@nuxt/loading-screen/2.0.4: @@ -3554,8 +3549,8 @@ packages: node-fetch: 2.6.6 dev: false - /@nuxt/schema-edge/3.0.0-27307420.6a25d3e: - resolution: {integrity: sha512-QB6zMvxMQ+H5kwqd/6vZO7UAxGLIMZGV5zEc9rlYIyoilNnMO3opBJWuaUaokDLW7JpA1bGOfakLWWg8e8LGgQ==} + /@nuxt/schema-edge/3.0.0-27319101.3e82f0f: + resolution: {integrity: sha512-D5ekvoG6KV85Ko40+8j4jUtv8JifgeXoD44gY1b/dOZu++UkwH7COr+zU80UCa3hTaIcKFwQ0yRg7CTl1susTA==} engines: {node: ^14.16.0 || ^16.11.0 || ^17.0.0} dependencies: create-require: 1.1.1 From 534fe8030f743ee4d3ddb958854fd48111a5b4e9 Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Sun, 12 Dec 2021 06:19:58 +0530 Subject: [PATCH 4/6] fix: subscription streams --- .../helpers/editor/codemirror.ts | 4 +- .../editor/extensions/environmentTooltip.ts | 98 ++++++++++--------- .../helpers/utils/composables.ts | 11 ++- .../hoppscotch-app/newstore/environments.ts | 17 ++-- 4 files changed, 78 insertions(+), 52 deletions(-) diff --git a/packages/hoppscotch-app/helpers/editor/codemirror.ts b/packages/hoppscotch-app/helpers/editor/codemirror.ts index 902c76caa..cd20dee12 100644 --- a/packages/hoppscotch-app/helpers/editor/codemirror.ts +++ b/packages/hoppscotch-app/helpers/editor/codemirror.ts @@ -30,6 +30,7 @@ import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql" import { pipe } from "fp-ts/function" import * as O from "fp-ts/Option" import { isJSONContentType } from "../utils/contenttypes" +import { useStreamSubscriber } from "../utils/composables" import { Completer } from "./completion" import { LinterDefinition } from "./linting/linter" import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" @@ -152,6 +153,7 @@ export function useCodemirror( value: Ref, options: CodeMirrorOptions ): { cursor: Ref<{ line: number; ch: number }> } { + const { subscribeToStream } = useStreamSubscriber() const language = new Compartment() const lineWrapping = new Compartment() const placeholderConfig = new Compartment() @@ -178,7 +180,7 @@ export function useCodemirror( basicSetup, baseTheme, baseHighlightStyle, - environmentTooltip, + environmentTooltip(subscribeToStream), environmentHighlightStyle, ViewPlugin.fromClass( class { diff --git a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts index 1e27a4ea8..d14ba18fc 100644 --- a/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts +++ b/packages/hoppscotch-app/helpers/editor/extensions/environmentTooltip.ts @@ -1,56 +1,64 @@ import { Extension } from "@codemirror/state" import { hoverTooltip } from "@codemirror/tooltip" import { Decoration, MatchDecorator, ViewPlugin } from "@codemirror/view" -import { useReadonlyStream } from "~/helpers/utils/composables" +import { + StreamSubscriberFunc, + useReadonlyStream, +} from "~/helpers/utils/composables" import { aggregateEnvs$ } from "~/newstore/environments" -const cursorTooltipField = hoverTooltip((view, pos, side) => { - const { from, to, text } = view.state.doc.lineAt(pos) - let start = pos - let end = pos +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++ + 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 + if ( + (start === pos && side < 0) || + (end === pos && side > 0) || + !/(<<\w+>>)/g.test(text.slice(start - from - 2, end - from + 2)) + ) + return null - const aggregateEnvs = useReadonlyStream(aggregateEnvs$, null) - const envName = getEnvName( - aggregateEnvs.value?.find( - (env: { key: string }) => env.key === text.slice(start - from, end - from) - )?.sourceEnv - ) - const envValue = getEnvValue( - aggregateEnvs.value?.find( - (env: { key: string }) => env.key === text.slice(start - from, end - from) - )?.value - ) - const textContent = `${envName} ${envValue}` + 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 } - }, - } -}) + 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) { +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" @@ -63,8 +71,9 @@ function checkEnv(env: string) { 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 + aggregateEnvs.value?.find( + (k: { key: string }) => k.key === env.slice(2, -2) + )?.value === undefined ? envNotFound : envFound return Decoration.mark({ @@ -89,7 +98,8 @@ export const environmentHighlightStyle = ViewPlugin.define( } ) -export const environmentTooltip: Extension = [ - cursorTooltipField, - environmentHighlightStyle, -] +export const environmentTooltip: ( + subscribeToStream: StreamSubscriberFunc +) => Extension = (subscribeToStream: StreamSubscriberFunc) => { + return [cursorTooltipField(subscribeToStream), environmentHighlightStyle] +} diff --git a/packages/hoppscotch-app/helpers/utils/composables.ts b/packages/hoppscotch-app/helpers/utils/composables.ts index e5ebdb437..482f35c8d 100644 --- a/packages/hoppscotch-app/helpers/utils/composables.ts +++ b/packages/hoppscotch-app/helpers/utils/composables.ts @@ -100,11 +100,20 @@ export function pluckMultipleFromRef>( return Object.fromEntries(keys.map((x) => [x, pluckRef(sourceRef, x)])) as any } +export type StreamSubscriberFunc = ( + stream: Observable, + next?: ((value: T) => void) | undefined, + error?: ((e: any) => void) | undefined, + complete?: (() => void) | undefined +) => void + /** * A composable that provides the ability to run streams * and subscribe to them and respect the component lifecycle. */ -export function useStreamSubscriber() { +export function useStreamSubscriber(): { + subscribeToStream: StreamSubscriberFunc +} { const subs: Subscription[] = [] const runAndSubscribe = ( diff --git a/packages/hoppscotch-app/newstore/environments.ts b/packages/hoppscotch-app/newstore/environments.ts index fdba6d5cb..b735956b1 100644 --- a/packages/hoppscotch-app/newstore/environments.ts +++ b/packages/hoppscotch-app/newstore/environments.ts @@ -1,6 +1,6 @@ import { cloneDeep } from "lodash" import isEqual from "lodash/isEqual" -import { combineLatest } from "rxjs" +import { combineLatest, Observable } from "rxjs" import { distinctUntilChanged, map, pluck } from "rxjs/operators" import DispatchingStore, { defineDispatchers, @@ -285,17 +285,22 @@ export const currentEnvironment$ = combineLatest([ }) ) +type AggregateEnvironment = { + key: string + value: string + sourceEnv: string +} + /** * Stream returning all the environment variables accessible in * the current state (Global + The Selected Environment). * NOTE: The source environment attribute will be "Global" for Global Env as source. */ -export const aggregateEnvs$ = combineLatest([ - currentEnvironment$, - globalEnv$, -]).pipe( +export const aggregateEnvs$: Observable = combineLatest( + [currentEnvironment$, globalEnv$] +).pipe( map(([selectedEnv, globalVars]) => { - const results: { key: string; value: string; sourceEnv: string }[] = [] + const results: AggregateEnvironment[] = [] selectedEnv.variables.forEach(({ key, value }) => results.push({ key, value, sourceEnv: selectedEnv.name }) From fe5fe03b3cb43da6362aa670ab08fb76b80a2ffd Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Sun, 12 Dec 2021 20:36:49 +0530 Subject: [PATCH 5/6] 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 { From 4e0bb1a2432edcfda9d2a2eb0348ae6bf093ff7d Mon Sep 17 00:00:00 2001 From: liyasthomas Date: Sun, 12 Dec 2021 21:10:05 +0530 Subject: [PATCH 6/6] refactor: environment highlighing in editors - improve #1834 --- packages/hoppscotch-app/components/graphql/RequestOptions.vue | 3 +++ packages/hoppscotch-app/components/graphql/Response.vue | 1 + packages/hoppscotch-app/components/graphql/Sidebar.vue | 1 + packages/hoppscotch-app/components/http/CodegenModal.vue | 1 + packages/hoppscotch-app/components/http/Headers.vue | 1 + packages/hoppscotch-app/components/http/ImportCurl.vue | 1 + packages/hoppscotch-app/components/http/Parameters.vue | 1 + packages/hoppscotch-app/components/http/PreRequestScript.vue | 1 + packages/hoppscotch-app/components/http/Tests.vue | 1 + .../components/lenses/renderers/HTMLLensRenderer.vue | 1 + .../components/lenses/renderers/JSONLensRenderer.vue | 1 + .../components/lenses/renderers/RawLensRenderer.vue | 1 + .../components/lenses/renderers/XMLLensRenderer.vue | 1 + 13 files changed, 15 insertions(+) diff --git a/packages/hoppscotch-app/components/graphql/RequestOptions.vue b/packages/hoppscotch-app/components/graphql/RequestOptions.vue index 252ca3eca..1787465e0 100644 --- a/packages/hoppscotch-app/components/graphql/RequestOptions.vue +++ b/packages/hoppscotch-app/components/graphql/RequestOptions.vue @@ -307,6 +307,7 @@ useCodemirror(bulkEditor, bulkHeaders, { }, linter: null, completer: null, + environmentHighlights: false, }) const variableEditor = ref(null) @@ -318,6 +319,7 @@ useCodemirror(variableEditor, variableString, { }, linter: jsonLinter, completer: null, + environmentHighlights: false, }) const queryEditor = ref(null) @@ -330,6 +332,7 @@ useCodemirror(queryEditor, gqlQueryString, { }, linter: createGQLQueryLinter(schemaString), completer: queryCompleter(schemaString), + environmentHighlights: false, }) const copyQueryIcon = ref("copy") diff --git a/packages/hoppscotch-app/components/graphql/Response.vue b/packages/hoppscotch-app/components/graphql/Response.vue index 972c92dbe..69b60f378 100644 --- a/packages/hoppscotch-app/components/graphql/Response.vue +++ b/packages/hoppscotch-app/components/graphql/Response.vue @@ -105,6 +105,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: false, }) ) diff --git a/packages/hoppscotch-app/components/graphql/Sidebar.vue b/packages/hoppscotch-app/components/graphql/Sidebar.vue index 0087f1990..a740e7f0e 100644 --- a/packages/hoppscotch-app/components/graphql/Sidebar.vue +++ b/packages/hoppscotch-app/components/graphql/Sidebar.vue @@ -409,6 +409,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: false, }) ) diff --git a/packages/hoppscotch-app/components/http/CodegenModal.vue b/packages/hoppscotch-app/components/http/CodegenModal.vue index 6b5171a6b..d4062f379 100644 --- a/packages/hoppscotch-app/components/http/CodegenModal.vue +++ b/packages/hoppscotch-app/components/http/CodegenModal.vue @@ -109,6 +109,7 @@ useCodemirror(generatedCode, requestCode, { }, linter: null, completer: null, + environmentHighlights: false, }) watch( diff --git a/packages/hoppscotch-app/components/http/Headers.vue b/packages/hoppscotch-app/components/http/Headers.vue index ae77c61cb..df1a24d93 100644 --- a/packages/hoppscotch-app/components/http/Headers.vue +++ b/packages/hoppscotch-app/components/http/Headers.vue @@ -182,6 +182,7 @@ useCodemirror(bulkEditor, bulkHeaders, { }, linter: null, completer: null, + environmentHighlights: true, }) watch(bulkHeaders, () => { diff --git a/packages/hoppscotch-app/components/http/ImportCurl.vue b/packages/hoppscotch-app/components/http/ImportCurl.vue index 3c7efe792..153daef74 100644 --- a/packages/hoppscotch-app/components/http/ImportCurl.vue +++ b/packages/hoppscotch-app/components/http/ImportCurl.vue @@ -47,6 +47,7 @@ useCodemirror(curlEditor, curl, { }, linter: null, completer: null, + environmentHighlights: false, }) defineProps<{ show: boolean }>() diff --git a/packages/hoppscotch-app/components/http/Parameters.vue b/packages/hoppscotch-app/components/http/Parameters.vue index ede5854da..83959ed68 100644 --- a/packages/hoppscotch-app/components/http/Parameters.vue +++ b/packages/hoppscotch-app/components/http/Parameters.vue @@ -189,6 +189,7 @@ useCodemirror(bulkEditor, bulkParams, { }, linter: null, completer: null, + environmentHighlights: true, }) const params$ = useReadonlyStream(restParams$, []) diff --git a/packages/hoppscotch-app/components/http/PreRequestScript.vue b/packages/hoppscotch-app/components/http/PreRequestScript.vue index b89de57ff..86a6481b5 100644 --- a/packages/hoppscotch-app/components/http/PreRequestScript.vue +++ b/packages/hoppscotch-app/components/http/PreRequestScript.vue @@ -88,6 +88,7 @@ useCodemirror( }, linter, completer, + environmentHighlights: false, }) ) diff --git a/packages/hoppscotch-app/components/http/Tests.vue b/packages/hoppscotch-app/components/http/Tests.vue index eabd4dba0..5f757e7c2 100644 --- a/packages/hoppscotch-app/components/http/Tests.vue +++ b/packages/hoppscotch-app/components/http/Tests.vue @@ -88,6 +88,7 @@ useCodemirror( }, linter, completer, + environmentHighlights: false, }) ) diff --git a/packages/hoppscotch-app/components/lenses/renderers/HTMLLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/HTMLLensRenderer.vue index 0a621078b..654f87b33 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/HTMLLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/HTMLLensRenderer.vue @@ -102,6 +102,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: true, }) ) diff --git a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue index dd31ba3fc..7959d1089 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/JSONLensRenderer.vue @@ -193,6 +193,7 @@ const { cursor } = useCodemirror( }, linter: null, completer: null, + environmentHighlights: true, }) ) diff --git a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue index aa9bc624f..b58a70a6c 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/RawLensRenderer.vue @@ -92,6 +92,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: true, }) ) diff --git a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue index 65d1198df..fce2bd662 100644 --- a/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue +++ b/packages/hoppscotch-app/components/lenses/renderers/XMLLensRenderer.vue @@ -92,6 +92,7 @@ useCodemirror( }, linter: null, completer: null, + environmentHighlights: true, }) )