123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- import {
- keymap,
- EditorView,
- ViewPlugin,
- ViewUpdate,
- placeholder,
- } from "@codemirror/view"
- import {
- Extension,
- EditorState,
- Compartment,
- EditorSelection,
- } from "@codemirror/state"
- import { Language, LanguageSupport } from "@codemirror/language"
- import { defaultKeymap, indentLess, insertTab } from "@codemirror/commands"
- import { Completion, autocompletion } from "@codemirror/autocomplete"
- import { linter } from "@codemirror/lint"
- import {
- watch,
- ref,
- Ref,
- onMounted,
- onBeforeUnmount,
- } from "@nuxtjs/composition-api"
- import { javascriptLanguage } from "@codemirror/lang-javascript"
- import { xmlLanguage } from "@codemirror/lang-xml"
- import { jsonLanguage } from "@codemirror/lang-json"
- import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
- import { StreamLanguage } from "@codemirror/stream-parser"
- import { html } from "@codemirror/legacy-modes/mode/xml"
- import { shell } from "@codemirror/legacy-modes/mode/shell"
- import { yaml } from "@codemirror/legacy-modes/mode/yaml"
- 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"
- import { HoppEnvironmentPlugin } from "./extensions/HoppEnvironment"
- // TODO: Migrate from legacy mode
- type ExtendedEditorConfig = {
- mode: string
- placeholder: string
- readOnly: boolean
- lineWrapping: boolean
- }
- type CodeMirrorOptions = {
- extendedEditorConfig: Partial<ExtendedEditorConfig>
- linter: LinterDefinition | null
- completer: Completer | null
- // NOTE: This property is not reactive
- environmentHighlights: boolean
- }
- const hoppCompleterExt = (completer: Completer): Extension => {
- return autocompletion({
- override: [
- async (context) => {
- // Expensive operation! Disable on bigger files ?
- const text = context.state.doc.toJSON().join(context.state.lineBreak)
- const line = context.state.doc.lineAt(context.pos)
- const lineStart = line.from
- const lineNo = line.number - 1
- const ch = context.pos - lineStart
- // Only do trigger on type when typing a word token, else stop (unless explicit)
- if (!context.matchBefore(/\w+/) && !context.explicit)
- return {
- from: context.pos,
- options: [],
- }
- const result = await completer(text, { line: lineNo, ch })
- // Use more completion features ?
- const completions =
- result?.completions.map<Completion>((comp) => ({
- label: comp.text,
- detail: comp.meta,
- })) ?? []
- return {
- from: context.state.wordAt(context.pos)?.from ?? context.pos,
- options: completions,
- }
- },
- ],
- })
- }
- const hoppLinterExt = (hoppLinter: LinterDefinition | undefined): Extension => {
- return linter(async (view) => {
- if (!hoppLinter) return []
- // Requires full document scan, hence expensive on big files, force disable on big files ?
- const linterResult = await hoppLinter(
- view.state.doc.toJSON().join(view.state.lineBreak)
- )
- return linterResult.map((result) => {
- const startPos =
- view.state.doc.line(result.from.line).from + result.from.ch - 1
- const endPos = view.state.doc.line(result.to.line).from + result.to.ch - 1
- return {
- from: startPos < 0 ? 0 : startPos,
- to: endPos > view.state.doc.length ? view.state.doc.length : endPos,
- message: result.message,
- severity: result.severity,
- }
- })
- })
- }
- const hoppLang = (
- language: Language | undefined,
- linter?: LinterDefinition | undefined,
- completer?: Completer | undefined
- ): Extension | LanguageSupport => {
- const exts: Extension[] = []
- exts.push(hoppLinterExt(linter))
- if (completer) exts.push(hoppCompleterExt(completer))
- return language ? new LanguageSupport(language, exts) : exts
- }
- const getLanguage = (langMime: string): Language | null => {
- if (isJSONContentType(langMime)) {
- return jsonLanguage
- } else if (langMime === "application/javascript") {
- return javascriptLanguage
- } else if (langMime === "graphql") {
- return GQLLanguage
- } else if (langMime === "application/xml") {
- return xmlLanguage
- } else if (langMime === "htmlmixed") {
- return StreamLanguage.define(html)
- } else if (langMime === "application/x-sh") {
- return StreamLanguage.define(shell)
- } else if (langMime === "text/x-yaml") {
- return StreamLanguage.define(yaml)
- }
- // None matched, so return null
- return null
- }
- const getEditorLanguage = (
- langMime: string,
- linter: LinterDefinition | undefined,
- completer: Completer | undefined
- ): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer)
- export function useCodemirror(
- el: Ref<any | null>,
- value: Ref<string>,
- options: CodeMirrorOptions
- ): { cursor: Ref<{ line: number; ch: number }> } {
- const { subscribeToStream } = useStreamSubscriber()
- const language = new Compartment()
- const lineWrapping = new Compartment()
- const placeholderConfig = new Compartment()
- const cachedCursor = ref({
- line: 0,
- ch: 0,
- })
- const cursor = ref({
- line: 0,
- ch: 0,
- })
- const cachedValue = ref(value.value)
- 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
- }
- }
- }
- ),
- EditorView.updateListener.of((update) => {
- if (options.extendedEditorConfig.readOnly) {
- update.view.contentDOM.inputMode = "none"
- }
- }),
- 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,
- {
- key: "Tab",
- preventDefault: true,
- run: insertTab,
- },
- {
- key: "Shift-Tab",
- preventDefault: true,
- run: indentLess,
- },
- ]),
- ]
- if (environmentTooltip) extensions.push(environmentTooltip.extension)
- view.value = new EditorView({
- parent: el,
- state: EditorState.create({
- doc: value.value,
- extensions,
- }),
- })
- }
- onMounted(() => {
- if (el.value) {
- if (!view.value) initView(el.value)
- }
- })
- watch(el, () => {
- if (el.value) {
- if (view.value) view.value.destroy()
- initView(el.value)
- } else {
- view.value?.destroy()
- view.value = undefined
- }
- })
- onBeforeUnmount(() => {
- view.value?.destroy()
- })
- watch(value, (newVal) => {
- if (cachedValue.value !== newVal) {
- view.value?.dispatch({
- filter: false,
- changes: {
- from: 0,
- to: view.value.state.doc.length,
- insert: newVal,
- },
- })
- }
- cachedValue.value = newVal
- })
- watch(
- () => [
- options.extendedEditorConfig.mode,
- options.linter,
- options.completer,
- ],
- () => {
- view.value?.dispatch({
- effects: language.reconfigure(
- getEditorLanguage(
- (options.extendedEditorConfig.mode as any) ?? "",
- options.linter ?? undefined,
- options.completer ?? undefined
- )
- ),
- })
- }
- )
- watch(
- () => options.extendedEditorConfig.lineWrapping,
- (newMode) => {
- view.value?.dispatch({
- effects: lineWrapping.reconfigure(
- newMode ? [EditorView.lineWrapping] : []
- ),
- })
- }
- )
- watch(
- () => options.extendedEditorConfig.placeholder,
- (newValue) => {
- view.value?.dispatch({
- effects: placeholderConfig.reconfigure(placeholder(newValue ?? "")),
- })
- }
- )
- watch(cursor, (newPos) => {
- if (view.value) {
- if (
- cachedCursor.value.line !== newPos.line ||
- cachedCursor.value.ch !== newPos.ch
- ) {
- const line = view.value.state.doc.line(newPos.line + 1)
- const selUpdate = EditorSelection.cursor(line.from + newPos.ch - 1)
- view.value?.focus()
- view.value.dispatch({
- scrollIntoView: true,
- selection: selUpdate,
- effects: EditorView.scrollTo.of(selUpdate),
- })
- }
- }
- })
- return {
- cursor,
- }
- }
|