feat: support for predefined variables (#3886)

Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Kishan Jadav
2024-09-30 09:47:34 +01:00
committed by jamesgeorge007
parent db8cf229ac
commit e4d9f82a75
14 changed files with 608 additions and 17 deletions

View File

@@ -261,7 +261,7 @@ const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
1000
)
const globalVars = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
const globalEnv = useReadonlyStream(globalEnv$, {} as GlobalEnvironment)
type SelectedEnv = "variables" | "secret"
@@ -319,7 +319,7 @@ const liveEnvs = computed(() => {
}
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalVars.value.variables.map((x) => ({ ...x, source: "Global" })),
...globalEnv.value.variables.map((x) => ({ ...x, source: "Global" })),
]
})

View File

@@ -343,6 +343,7 @@ useCodemirror(
linter,
completer: null,
environmentHighlights: true,
predefinedVariablesHighlights: true,
})
)

View File

@@ -161,6 +161,7 @@ useCodemirror(
linter,
completer: null,
environmentHighlights: true,
predefinedVariablesHighlights: true,
})
)

View File

@@ -160,6 +160,7 @@ useCodemirror(
linter: langLinter,
completer: null,
environmentHighlights: true,
predefinedVariablesHighlights: true,
})
)

View File

@@ -234,6 +234,7 @@ useCodemirror(
linter,
completer: null,
environmentHighlights: true,
predefinedVariablesHighlights: true,
})
)

View File

@@ -246,6 +246,7 @@ useCodemirror(
linter,
completer: null,
environmentHighlights: true,
predefinedVariablesHighlights: true,
})
)

View File

@@ -78,6 +78,7 @@ import { clone } from "lodash-es"
import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { HoppPredefinedVariablesPlugin } from "~/helpers/editor/extensions/HoppPredefinedVariables"
import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
@@ -103,6 +104,7 @@ const props = withDefaults(
focus?: boolean
selectTextOnMount?: boolean
environmentHighlights?: boolean
predefinedVariablesHighlights?: boolean
readonly?: boolean
autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined
@@ -118,6 +120,7 @@ const props = withDefaults(
focus: false,
readonly: false,
environmentHighlights: true,
predefinedVariablesHighlights: true,
autoCompleteSource: undefined,
inspectionResult: undefined,
inspectionResults: undefined,
@@ -396,20 +399,22 @@ function envAutoCompletion(context: CompletionContext) {
info: env?.value ?? "",
apply: env?.key ? `<<${env.key}>>` : "",
}))
.filter((x) => x)
.filter(Boolean)
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
const tagBefore = /<<\w*$/.exec(textBefore)
const tagBefore = /<<\$?\w*$/.exec(textBefore) // Update regex to match <<$ as well
if (!tagBefore && !context.explicit) return null
return {
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
options: options,
validFor: /^(<<\w*)?$/,
validFor: /^(<<\$?\w*)?$/,
}
}
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
const predefinedVariablePlugin = new HoppPredefinedVariablesPlugin()
function handleTextSelection() {
const selection = view.value?.state.selection.main
@@ -490,6 +495,7 @@ const getExtensions = (readonly: boolean): Extension => {
position: "absolute",
}),
props.environmentHighlights ? envTooltipPlugin : [],
props.predefinedVariablesHighlights ? predefinedVariablePlugin : [],
placeholderExt(props.placeholder),
EditorView.domEventHandlers({
paste(ev) {

View File

@@ -47,6 +47,7 @@ import { useDebounceFn } from "@vueuse/core"
// TODO: Migrate from legacy mode
import * as E from "fp-ts/Either"
import { HoppPredefinedVariablesPlugin } from "~/helpers/editor/extensions/HoppPredefinedVariables"
type ExtendedEditorConfig = {
mode: string
@@ -63,6 +64,12 @@ type CodeMirrorOptions = {
// NOTE: This property is not reactive
environmentHighlights: boolean
/**
* Whether or not to highlight predefined variables, such as: `<<$guid>>`.
* - These are special variables that starts with a dolar sign.
*/
predefinedVariablesHighlights?: boolean
additionalExts?: Extension[]
contextMenuEnabled?: boolean
@@ -251,6 +258,10 @@ export function useCodemirror(
text: null,
})
}
const predefinedVariable: HoppPredefinedVariablesPlugin | null =
options.predefinedVariablesHighlights
? new HoppPredefinedVariablesPlugin()
: null
function handleTextSelection() {
const selection = view.value?.state.selection.main
@@ -396,6 +407,7 @@ export function useCodemirror(
]
if (environmentTooltip) extensions.push(environmentTooltip.extension)
if (predefinedVariable) extensions.push(predefinedVariable.extension)
view.value = new EditorView({
parent: el,

View File

@@ -0,0 +1,142 @@
import { Compartment } from "@codemirror/state"
import {
Decoration,
MatchDecorator,
ViewPlugin,
hoverTooltip,
} from "@codemirror/view"
import IconSquareAsterisk from "~icons/lucide/square-asterisk?raw"
import { HOPP_SUPPORTED_PREDEFINED_VARIABLES } from "@hoppscotch/data"
const HOPP_PREDEFINED_VARIABLES_REGEX = /(<<\$[a-zA-Z0-9-_]+>>)/g
const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 predefined-variable-highlight"
const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_VALID = "predefined-variable-valid"
const HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_INVALID = "predefined-variable-invalid"
const getMatchDecorator = () => {
return new MatchDecorator({
regexp: HOPP_PREDEFINED_VARIABLES_REGEX,
decoration: (m) => checkPredefinedVariable(m[0]),
})
}
const cursorTooltipField = () =>
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 - 3,
// wordSelection.to + 2
// )
// if (!HOPP_PREDEFINED_VARIABLES_REGEX.test(word)) return null
// Tracking the start and the end of the words
let start = pos
let end = pos
while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||
(end === pos && side > 0) ||
!HOPP_PREDEFINED_VARIABLES_REGEX.test(
text.slice(start - from - 3, end - from + 2)
)
) {
return null
}
const variableName = text.slice(start - from - 1, end - from)
const variable = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find(
(VARIABLE) => VARIABLE.key === variableName
)
const variableIcon = `<span class="inline-flex items-center justify-center my-1">${IconSquareAsterisk}</span>`
const variableDescription =
variable !== undefined
? `${variableName} - ${variable.description}`
: `${variableName} is not a valid predefined variable.`
return {
pos: start,
end: to,
above: true,
arrow: true,
create() {
const dom = document.createElement("div")
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
const icon = document.createElement("span")
icon.innerHTML = variableIcon
icon.className = "mr-2"
const tooltipContainer = document.createElement("span")
tooltipContainer.className = "tippy-content"
tooltipContainer.appendChild(icon)
tooltipContainer.appendChild(
document.createTextNode(variableDescription)
)
dom.appendChild(tooltipContainer)
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
)
const checkPredefinedVariable = (variable: string) => {
const inputVariableKey = variable.slice(2, -2)
const className = HOPP_SUPPORTED_PREDEFINED_VARIABLES.find((v) => {
return v.key === inputVariableKey
})
? HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_VALID
: HOPP_PREDEFINED_VARIABLE_HIGHLIGHT_INVALID
return Decoration.mark({
class: `${HOPP_PREDEFINED_VARIABLE_HIGHLIGHT} ${className}`,
})
}
export const predefinedVariableHighlightStyle = () => {
const decorator = getMatchDecorator()
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
{
decorations: (v) => v.decorations,
}
)
}
export class HoppPredefinedVariablesPlugin {
private compartment = new Compartment()
get extension() {
return this.compartment.of([
cursorTooltipField(),
predefinedVariableHighlightStyle(),
])
}
}

View File

@@ -2,6 +2,7 @@ import {
Environment,
GlobalEnvironment,
GlobalEnvironmentVariable,
HOPP_SUPPORTED_PREDEFINED_VARIABLES,
} from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es"
import { combineLatest, Observable } from "rxjs"
@@ -407,24 +408,45 @@ export type AggregateEnvironment = {
export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
[currentEnvironment$, globalEnv$]
).pipe(
map(([selectedEnv, globalVars]) => {
const results: AggregateEnvironment[] = []
map(([selectedEnv, globalEnv]) => {
const effectiveAggregateEnvs: AggregateEnvironment[] = []
// Ensure pre-defined variables are prioritised over other environment variables with the same name
HOPP_SUPPORTED_PREDEFINED_VARIABLES.forEach(({ key, getValue }) => {
effectiveAggregateEnvs.push({
key,
value: getValue(),
secret: false,
sourceEnv: selectedEnv?.name ?? "Global",
})
})
const aggregateEnvKeys = effectiveAggregateEnvs.map(({ key }) => key)
selectedEnv?.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
results.push({ key, value, secret, sourceEnv: selectedEnv.name })
if (!aggregateEnvKeys.includes(key)) {
effectiveAggregateEnvs.push({
key,
value,
secret,
sourceEnv: selectedEnv.name,
})
}
})
globalVars.variables.forEach((variable) => {
globalEnv.variables.forEach((variable) => {
const { key, secret } = variable
const value = "value" in variable ? variable.value : ""
results.push({ key, value, secret, sourceEnv: "Global" })
if (!aggregateEnvKeys.includes(key)) {
effectiveAggregateEnvs.push({ key, value, secret, sourceEnv: "Global" })
}
})
return results
return effectiveAggregateEnvs
}),
distinctUntilChanged(isEqual)
)
@@ -503,7 +525,7 @@ export function getAggregateEnvsWithSecrets() {
export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
combineLatest([currentEnvironment$, globalEnv$]).pipe(
map(([selectedEnv, globalVars]) => {
map(([selectedEnv, globalEnv]) => {
const results: AggregateEnvironment[] = []
selectedEnv?.variables.map((x, index) => {
let value
@@ -523,7 +545,7 @@ export const aggregateEnvsWithSecrets$: Observable<AggregateEnvironment[]> =
})
})
globalVars.variables.map((x, index) => {
globalEnv.variables.map((x, index) => {
let value
if (x.secret) {
value = secretEnvironmentService.getSecretEnvironmentVariableValue(