fix: reactivity issues

This commit is contained in:
Andrew Bastin
2021-12-12 20:36:49 +05:30
parent 534fe8030f
commit fe5fe03b3c
5 changed files with 235 additions and 165 deletions

View File

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

View File

@@ -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,
}),
})
}

View File

@@ -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, "&quot;")
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),
])
}
}

View File

@@ -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, "&quot;")
// 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]
}

View File

@@ -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 {