codemirror.ts (hoppscotch-2.2.1) | : | codemirror.ts (hoppscotch-3.0.0) | ||
---|---|---|---|---|
skipping to change at line 14 | skipping to change at line 14 | |||
ViewPlugin, | ViewPlugin, | |||
ViewUpdate, | ViewUpdate, | |||
placeholder, | placeholder, | |||
} from "@codemirror/view" | } from "@codemirror/view" | |||
import { | import { | |||
Extension, | Extension, | |||
EditorState, | EditorState, | |||
Compartment, | Compartment, | |||
EditorSelection, | EditorSelection, | |||
} from "@codemirror/state" | } from "@codemirror/state" | |||
import { Language, LanguageSupport } from "@codemirror/language" | import { | |||
import { defaultKeymap } from "@codemirror/commands" | Language, | |||
LanguageSupport, | ||||
StreamLanguage, | ||||
syntaxHighlighting, | ||||
} from "@codemirror/language" | ||||
import { defaultKeymap, indentLess, insertTab } from "@codemirror/commands" | ||||
import { Completion, autocompletion } from "@codemirror/autocomplete" | import { Completion, autocompletion } from "@codemirror/autocomplete" | |||
import { linter } from "@codemirror/lint" | import { linter } from "@codemirror/lint" | |||
import { watch, ref, Ref, onMounted, onBeforeUnmount } from "vue" | ||||
import { | ||||
watch, | ||||
ref, | ||||
Ref, | ||||
onMounted, | ||||
onBeforeUnmount, | ||||
} from "@nuxtjs/composition-api" | ||||
import { javascriptLanguage } from "@codemirror/lang-javascript" | import { javascriptLanguage } from "@codemirror/lang-javascript" | |||
import { xmlLanguage } from "@codemirror/lang-xml" | ||||
import { jsonLanguage } from "@codemirror/lang-json" | import { jsonLanguage } from "@codemirror/lang-json" | |||
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql" | import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql" | |||
import { pipe } from "fp-ts/function" | ||||
import * as O from "fp-ts/Option" | ||||
import { StreamLanguage } from "@codemirror/stream-parser" | ||||
import { html } from "@codemirror/legacy-modes/mode/xml" | import { html } from "@codemirror/legacy-modes/mode/xml" | |||
import { shell } from "@codemirror/legacy-modes/mode/shell" | import { shell } from "@codemirror/legacy-modes/mode/shell" | |||
import { yaml } from "@codemirror/legacy-modes/mode/yaml" | import { yaml } from "@codemirror/legacy-modes/mode/yaml" | |||
import { isJSONContentType } from "../utils/contenttypes" | import { isJSONContentType } from "@helpers/utils/contenttypes" | |||
import { useStreamSubscriber } from "../utils/composables" | import { useStreamSubscriber } from "@composables/stream" | |||
import { Completer } from "./completion" | import { Completer } from "@helpers/editor/completion" | |||
import { LinterDefinition } from "./linting/linter" | import { LinterDefinition } from "@helpers/editor/linting/linter" | |||
import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme" | import { | |||
import { HoppEnvironmentPlugin } from "./extensions/HoppEnvironment" | basicSetup, | |||
baseTheme, | ||||
baseHighlightStyle, | ||||
} from "@helpers/editor/themes/baseTheme" | ||||
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironmen | ||||
t" | ||||
// TODO: Migrate from legacy mode | // TODO: Migrate from legacy mode | |||
type ExtendedEditorConfig = { | type ExtendedEditorConfig = { | |||
mode: string | mode: string | |||
placeholder: string | placeholder: string | |||
readOnly: boolean | readOnly: boolean | |||
lineWrapping: boolean | lineWrapping: boolean | |||
} | } | |||
type CodeMirrorOptions = { | type CodeMirrorOptions = { | |||
skipping to change at line 97 | skipping to change at line 96 | |||
return { | return { | |||
from: context.state.wordAt(context.pos)?.from ?? context.pos, | from: context.state.wordAt(context.pos)?.from ?? context.pos, | |||
options: completions, | options: completions, | |||
} | } | |||
}, | }, | |||
], | ], | |||
}) | }) | |||
} | } | |||
const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => { | const hoppLinterExt = (hoppLinter: LinterDefinition | undefined): Extension => { | |||
return linter(async (view) => { | return linter(async (view) => { | |||
if (!hoppLinter) return [] | ||||
// Requires full document scan, hence expensive on big files, force disable on big files ? | // Requires full document scan, hence expensive on big files, force disable on big files ? | |||
const linterResult = await hoppLinter( | const linterResult = await hoppLinter( | |||
view.state.doc.toJSON().join(view.state.lineBreak) | view.state.doc.toJSON().join(view.state.lineBreak) | |||
) | ) | |||
return linterResult.map((result) => { | return linterResult.map((result) => { | |||
const startPos = | const startPos = | |||
view.state.doc.line(result.from.line).from + result.from.ch - 1 | 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 | const endPos = view.state.doc.line(result.to.line).from + result.to.ch - 1 | |||
skipping to change at line 120 | skipping to change at line 121 | |||
from: startPos < 0 ? 0 : startPos, | from: startPos < 0 ? 0 : startPos, | |||
to: endPos > view.state.doc.length ? view.state.doc.length : endPos, | to: endPos > view.state.doc.length ? view.state.doc.length : endPos, | |||
message: result.message, | message: result.message, | |||
severity: result.severity, | severity: result.severity, | |||
} | } | |||
}) | }) | |||
}) | }) | |||
} | } | |||
const hoppLang = ( | const hoppLang = ( | |||
language: Language, | language: Language | undefined, | |||
linter?: LinterDefinition | undefined, | linter?: LinterDefinition | undefined, | |||
completer?: Completer | undefined | completer?: Completer | undefined | |||
) => { | ): Extension | LanguageSupport => { | |||
const exts: Extension[] = [] | const exts: Extension[] = [] | |||
if (linter) exts.push(hoppLinterExt(linter)) | exts.push(hoppLinterExt(linter)) | |||
if (completer) exts.push(hoppCompleterExt(completer)) | if (completer) exts.push(hoppCompleterExt(completer)) | |||
return new LanguageSupport(language, exts) | return language ? new LanguageSupport(language, exts) : exts | |||
} | } | |||
const getLanguage = (langMime: string): Language | null => { | const getLanguage = (langMime: string): Language | null => { | |||
if (isJSONContentType(langMime)) { | if (isJSONContentType(langMime)) { | |||
return jsonLanguage | return jsonLanguage | |||
} else if (langMime === "application/javascript") { | } else if (langMime === "application/javascript") { | |||
return javascriptLanguage | return javascriptLanguage | |||
} else if (langMime === "graphql") { | } else if (langMime === "graphql") { | |||
return GQLLanguage | return GQLLanguage | |||
} else if (langMime === "application/xml") { | ||||
return xmlLanguage | ||||
} else if (langMime === "htmlmixed") { | } else if (langMime === "htmlmixed") { | |||
return StreamLanguage.define(html) | return StreamLanguage.define(html) | |||
} else if (langMime === "application/x-sh") { | } else if (langMime === "application/x-sh") { | |||
return StreamLanguage.define(shell) | return StreamLanguage.define(shell) | |||
} else if (langMime === "text/x-yaml") { | } else if (langMime === "text/x-yaml") { | |||
return StreamLanguage.define(yaml) | return StreamLanguage.define(yaml) | |||
} | } | |||
// None matched, so return null | // None matched, so return null | |||
return null | return null | |||
} | } | |||
const getEditorLanguage = ( | const getEditorLanguage = ( | |||
langMime: string, | langMime: string, | |||
linter: LinterDefinition | undefined, | linter: LinterDefinition | undefined, | |||
completer: Completer | undefined | completer: Completer | undefined | |||
): Extension => | ): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer) | |||
pipe( | ||||
O.fromNullable(getLanguage(langMime)), | ||||
O.map((lang) => hoppLang(lang, linter, completer)), | ||||
O.getOrElseW(() => []) | ||||
) | ||||
export function useCodemirror( | export function useCodemirror( | |||
el: Ref<any | null>, | el: Ref<any | null>, | |||
value: Ref<string>, | value: Ref<string>, | |||
options: CodeMirrorOptions | options: CodeMirrorOptions | |||
): { cursor: Ref<{ line: number; ch: number }> } { | ): { cursor: Ref<{ line: number; ch: number }> } { | |||
const { subscribeToStream } = useStreamSubscriber() | const { subscribeToStream } = useStreamSubscriber() | |||
const language = new Compartment() | const language = new Compartment() | |||
const lineWrapping = new Compartment() | const lineWrapping = new Compartment() | |||
skipping to change at line 194 | skipping to change at line 192 | |||
const view = ref<EditorView>() | const view = ref<EditorView>() | |||
const environmentTooltip = options.environmentHighlights | const environmentTooltip = options.environmentHighlights | |||
? new HoppEnvironmentPlugin(subscribeToStream, view) | ? new HoppEnvironmentPlugin(subscribeToStream, view) | |||
: null | : null | |||
const initView = (el: any) => { | const initView = (el: any) => { | |||
const extensions = [ | const extensions = [ | |||
basicSetup, | basicSetup, | |||
baseTheme, | baseTheme, | |||
baseHighlightStyle, | syntaxHighlighting(baseHighlightStyle, { fallback: true }), | |||
ViewPlugin.fromClass( | ViewPlugin.fromClass( | |||
class { | class { | |||
update(update: ViewUpdate) { | update(update: ViewUpdate) { | |||
if (update.selectionSet) { | if (update.selectionSet) { | |||
const cursorPos = update.state.selection.main.head | const cursorPos = update.state.selection.main.head | |||
const line = update.state.doc.lineAt(cursorPos) | const line = update.state.doc.lineAt(cursorPos) | |||
cachedCursor.value = { | cachedCursor.value = { | |||
line: line.number - 1, | line: line.number - 1, | |||
skipping to change at line 224 | skipping to change at line 222 | |||
// Expensive on big files ? | // Expensive on big files ? | |||
cachedValue.value = update.state.doc | cachedValue.value = update.state.doc | |||
.toJSON() | .toJSON() | |||
.join(update.state.lineBreak) | .join(update.state.lineBreak) | |||
if (!options.extendedEditorConfig.readOnly) | if (!options.extendedEditorConfig.readOnly) | |||
value.value = cachedValue.value | value.value = cachedValue.value | |||
} | } | |||
} | } | |||
} | } | |||
), | ), | |||
EditorView.updateListener.of((update) => { | ||||
if (options.extendedEditorConfig.readOnly) { | ||||
update.view.contentDOM.inputMode = "none" | ||||
} | ||||
}), | ||||
EditorState.changeFilter.of(() => !options.extendedEditorConfig.readOnly), | EditorState.changeFilter.of(() => !options.extendedEditorConfig.readOnly), | |||
placeholderConfig.of( | placeholderConfig.of( | |||
placeholder(options.extendedEditorConfig.placeholder ?? "") | placeholder(options.extendedEditorConfig.placeholder ?? "") | |||
), | ), | |||
language.of( | language.of( | |||
getEditorLanguage( | getEditorLanguage( | |||
options.extendedEditorConfig.mode ?? "", | options.extendedEditorConfig.mode ?? "", | |||
options.linter ?? undefined, | options.linter ?? undefined, | |||
options.completer ?? undefined | options.completer ?? undefined | |||
) | ) | |||
), | ), | |||
lineWrapping.of( | lineWrapping.of( | |||
options.extendedEditorConfig.lineWrapping | options.extendedEditorConfig.lineWrapping | |||
? [EditorView.lineWrapping] | ? [EditorView.lineWrapping] | |||
: [] | : [] | |||
), | ), | |||
keymap.of(defaultKeymap), | keymap.of([ | |||
...defaultKeymap, | ||||
{ | ||||
key: "Tab", | ||||
preventDefault: true, | ||||
run: insertTab, | ||||
}, | ||||
{ | ||||
key: "Shift-Tab", | ||||
preventDefault: true, | ||||
run: indentLess, | ||||
}, | ||||
]), | ||||
] | ] | |||
if (environmentTooltip) extensions.push(environmentTooltip.extension) | if (environmentTooltip) extensions.push(environmentTooltip.extension) | |||
view.value = new EditorView({ | view.value = new EditorView({ | |||
parent: el, | parent: el, | |||
state: EditorState.create({ | state: EditorState.create({ | |||
doc: value.value, | doc: value.value, | |||
extensions, | extensions, | |||
}), | }), | |||
skipping to change at line 262 | skipping to change at line 277 | |||
} | } | |||
onMounted(() => { | onMounted(() => { | |||
if (el.value) { | if (el.value) { | |||
if (!view.value) initView(el.value) | if (!view.value) initView(el.value) | |||
} | } | |||
}) | }) | |||
watch(el, () => { | watch(el, () => { | |||
if (el.value) { | if (el.value) { | |||
if (!view.value) initView(el.value) | if (view.value) view.value.destroy() | |||
initView(el.value) | ||||
} else { | } else { | |||
view.value?.destroy() | view.value?.destroy() | |||
view.value = undefined | view.value = undefined | |||
} | } | |||
}) | }) | |||
onBeforeUnmount(() => { | onBeforeUnmount(() => { | |||
view.value?.destroy() | view.value?.destroy() | |||
}) | }) | |||
skipping to change at line 340 | skipping to change at line 356 | |||
cachedCursor.value.ch !== newPos.ch | cachedCursor.value.ch !== newPos.ch | |||
) { | ) { | |||
const line = view.value.state.doc.line(newPos.line + 1) | const line = view.value.state.doc.line(newPos.line + 1) | |||
const selUpdate = EditorSelection.cursor(line.from + newPos.ch - 1) | const selUpdate = EditorSelection.cursor(line.from + newPos.ch - 1) | |||
view.value?.focus() | view.value?.focus() | |||
view.value.dispatch({ | view.value.dispatch({ | |||
scrollIntoView: true, | scrollIntoView: true, | |||
selection: selUpdate, | selection: selUpdate, | |||
effects: EditorView.scrollTo.of(selUpdate), | effects: EditorView.scrollIntoView(selUpdate), | |||
}) | }) | |||
} | } | |||
} | } | |||
}) | }) | |||
return { | return { | |||
cursor, | cursor, | |||
} | } | |||
} | } | |||
End of changes. 18 change blocks. | ||||
35 lines changed or deleted | 52 lines changed or added |