fix: reactivity issues
This commit is contained in:
@@ -91,6 +91,7 @@ useCodemirror(
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: true,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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<ExtendedEditorConfig>
|
||||
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<EditorView>()
|
||||
|
||||
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,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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} <kbd>${envValue}</kbd>`
|
||||
|
||||
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<EditorView | undefined>
|
||||
) {
|
||||
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),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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} <kbd>${envValue}</kbd>`
|
||||
})
|
||||
|
||||
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]
|
||||
}
|
||||
@@ -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<AggregateEnvironment[]> = combineLatest(
|
||||
distinctUntilChanged(isEqual)
|
||||
)
|
||||
|
||||
export function getAggregateEnvs() {
|
||||
const currentEnv = getCurrentEnvironment()
|
||||
|
||||
return [
|
||||
...currentEnv.variables.map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: currentEnv.name,
|
||||
}
|
||||
),
|
||||
...getGlobalVariables().map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: "Global",
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
export function getCurrentEnvironment(): Environment {
|
||||
if (environmentsStore.value.currentEnvironmentIndex === -1) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user