Merge branch 'feat/codemirror-6'

This commit is contained in:
liyasthomas
2021-11-15 16:45:20 +05:30
27 changed files with 1702 additions and 387 deletions

View File

@@ -0,0 +1,4 @@
/node_modules
package-lock.json
/dist
/src/*.d.ts

View File

@@ -0,0 +1,5 @@
/src
/test
/node_modules
rollup.config.js
tsconfig.json

View File

@@ -0,0 +1 @@
A [CodeMirror 6](https://codemirror.net/6) language plugin for GraphQL

View File

@@ -0,0 +1,32 @@
{
"name": "@hoppscotch/codemirror-lang-graphql",
"version": "0.1.0",
"description": "GraphQL language support for CodeMirror",
"scripts": {
"test": "mocha test/test.js",
"prepare": "rollup -c"
},
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"exports": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/highlight": "^0.19.0",
"@codemirror/language": "^0.19.0",
"@lezer/lr": "^0.15.0"
},
"devDependencies": {
"@lezer/generator": "^0.15.0",
"mocha": "^9.0.1",
"rollup": "^2.35.1",
"rollup-plugin-dts": "^3.0.2",
"rollup-plugin-ts": "^1.4.0",
"typescript": "^4.3.4"
},
"license": "MIT"
}

View File

@@ -0,0 +1,12 @@
import typescript from "rollup-plugin-ts"
import {lezer} from "@lezer/generator/rollup"
export default {
input: "src/index.js",
external: id => id != "tslib" && !/^(\.?\/|\w:)/.test(id),
output: [
{file: "dist/index.cjs", format: "cjs"},
{dir: "./dist", format: "es"}
],
plugins: [lezer(), typescript()]
}

View File

@@ -0,0 +1,43 @@
import {parser} from "./syntax.grammar"
import {LRLanguage, LanguageSupport, indentNodeProp, foldNodeProp, foldInside, delimitedIndent} from "@codemirror/language"
import {styleTags, tags as t} from "@codemirror/highlight"
export const GQLLanguage = LRLanguage.define({
parser: parser.configure({
props: [
indentNodeProp.add({
"SelectionSet FieldsDefinition ObjectValue SchemaDefinition RootTypeDef": delimitedIndent({ closing: "}", align: true }),
}),
foldNodeProp.add({
Application: foldInside,
"SelectionSet FieldsDefinition ObjectValue RootOperationTypeDefinition RootTypeDef": (node) => {
return {
from: node.from,
to: node.to
}
}
}),
styleTags({
Name: t.definition(t.variableName),
"OperationDefinition/Name": t.definition(t.function(t.variableName)),
OperationType: t.keyword,
BooleanValue: t.bool,
StringValue: t.string,
IntValue: t.number,
FloatValue: t.number,
NullValue: t.null,
ObjectValue: t.brace,
Comment: t.lineComment,
})
]
}),
languageData: {
commentTokens: { line: "#" },
closeBrackets: { brackets: ["(", "[", "{", '"', '"""'] }
}
})
export function GQL() {
return new LanguageSupport(GQLLanguage)
}

View File

@@ -0,0 +1,372 @@
@top SourceFile {
Document
}
@precedence {
fieldDef @right,
typeDef @right
}
Document {
Definition+
}
Definition {
ExecutableDefinition |
TypeSystemDefinition |
TypeSystemExtension
}
ExecutableDefinition {
OperationDefinition |
FragmentDefinition
}
TypeSystemDefinition {
SchemaDefinition |
TypeDefinition |
DirectiveDefinition
}
TypeSystemExtension {
SchemaExtension |
TypeExtension
}
SchemaDefinition {
Description? @specialize<Name, "schema"> Directives? RootTypeDef
}
RootTypeDef {
"{" RootOperationTypeDefinition+ "}"
}
SchemaExtension {
@specialize<Name, "extend"> @specialize<Name, "schema"> Directives? RootTypeDef
}
TypeExtension {
ScalarTypeExtension |
ObjectTypeExtension |
InterfaceTypeExtension |
UnionTypeExtension |
EnumTypeExtension |
InputObjectTypeExtension
}
ScalarTypeExtension {
@specialize<Name, "extend"> @specialize<Name, "scalar"> Name Directives
}
ObjectTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives? !typeDef FieldsDefinition |
@specialize<Name, "extend"> @specialize<Name, "type"> Name ImplementsInterfaces? Directives?
}
InterfaceTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition |
@specialize<Name, "extend"> @specialize<Name, "interface"> Name ImplementsInterfaces? Directives?
}
UnionTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives? UnionMemberTypes |
@specialize<Name, "extend"> @specialize<Name, "union"> Name Directives?
}
EnumTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition |
@specialize<Name, "extend"> @specialize<Name, "enum"> Name Directives?
}
InputObjectTypeExtension /* precedence: right 0 */ {
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives? InputFieldsDefinition+ |
@specialize<Name, "extend"> @specialize<Name, "input"> Name Directives?
}
InputFieldsDefinition {
!fieldDef "{" InputValueDefinition+ "}"
}
EnumValuesDefinition {
!fieldDef "{" EnumValueDefinition+ "}"
}
EnumValueDefinition {
Description? EnumValue Directives?
}
ImplementsInterfaces {
ImplementsInterfaces "&" NamedType |
@specialize<Name, "implements"> "&"? NamedType
}
FieldsDefinition {
!fieldDef "{" FieldDefinition+ "}"
}
FieldDefinition {
Description? Name ArgumentsDefinition? ":" Type Directives?
}
ArgumentsDefinition {
"(" InputValueDefinition+ ")"
}
InputValueDefinition {
Description? Name ":" Type DefaultValue? Directives?
}
DefaultValue {
"=" Value
}
UnionMemberTypes {
UnionMemberTypes "|" NamedType |
"=" "|"? NamedType
}
RootOperationTypeDefinition {
OperationType ":" NamedType
}
OperationDefinition {
SelectionSet |
OperationType Name? VariableDefinitions? Directives? SelectionSet
}
TypeDefinition {
ScalarTypeDefinition |
ObjectTypeDefinition |
InterfaceTypeDefinition |
UnionTypeDefinition |
EnumTypeDefinition |
InputObjectTypeDefinition
}
ScalarTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "scalar"> Name Directives?
}
ObjectTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "type"> Name ImplementsInterfaces? Directives? FieldsDefinition?
}
InterfaceTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "interface"> Name ImplementsInterfaces? Directives? FieldsDefinition?
}
UnionTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "union"> Name Directives? UnionMemberTypes?
}
EnumTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "enum"> Name Directives? !typeDef EnumValuesDefinition?
}
InputObjectTypeDefinition /* precedence: right 0 */ {
Description? @specialize<Name, "input"> Name Directives? !typeDef InputFieldsDefinition?
}
VariableDefinitions {
"(" VariableDefinition+ ")"
}
VariableDefinition {
Variable ":" Type DefaultValue? Directives? Comma?
}
SelectionSet {
"{" Selection* "}"
}
Selection {
Field |
InlineFragment |
FragmentSpread
}
Field {
Alias? Name Arguments? Directive? SelectionSet?
}
Alias {
Name ":"
}
Arguments {
"(" Argument+ ")"
}
Argument {
Name ":" Value
}
Value {
Variable |
StringValue |
IntValue |
FloatValue |
BooleanValue |
NullValue |
EnumValue |
ListValue |
ObjectValue
}
Variable {
"$" Name
}
EnumValue {
Name
}
ListValue {
"[" Value* "]"
}
ObjectValue {
"{" ObjectField* "}"
}
ObjectField {
Name ":" Value Comma?
}
FragmentSpread {
"..." FragmentName Directives?
}
FragmentDefinition {
@specialize<Name, "fragment"> FragmentName TypeCondition Directives? SelectionSet
}
FragmentName {
Name
}
InlineFragment {
"..." TypeCondition? Directives? SelectionSet
}
TypeCondition {
@specialize<Name, "on"> NamedType
}
Directives {
Directive+
}
Directive {
"@" Name Arguments?
}
DirectiveDefinition /* precedence: right 1 */ {
Description? @specialize<Name, "directive"> "@" Name ArgumentsDefinition? @specialize<Name, "repeatable"> ? @specialize<Name, "on"> DirectiveLocations
}
DirectiveLocations {
DirectiveLocations "|" DirectiveLocation |
"|"? DirectiveLocation
}
DirectiveLocation {
ExecutableDirectiveLocation |
TypeSystemDirectiveLocation
}
Type {
NamedType |
ListType |
NonNullType
}
NamedType {
Name
}
ListType {
"[" Type "]"
}
NonNullType {
NamedType "!" |
ListType "!"
}
Description {
StringValue
}
OperationType {
@specialize<Name, "query">
| @specialize<Name, "mutation">
| @specialize<Name, "subscription">
}
BooleanValue {
@specialize<Name, "true">
| @specialize<Name, "false">
}
NullValue {
@specialize<Name, "null">
}
ExecutableDirectiveLocation {
@specialize<Name, "QUERY">
| @specialize<Name, "MUTATION">
| @specialize<Name, "SUBSCRIPTION">
| @specialize<Name, "FIELD">
| @specialize<Name, "FRAGMENT_DEFINITION">
| @specialize<Name, "FRAGMENT_SPREAD">
| @specialize<Name, "INLINE_FRAGMENT">
| @specialize<Name, "VARIABLE_DEFINITION">
}
TypeSystemDirectiveLocation {
@specialize<Name, "SCHEMA">
| @specialize<Name, "SCALAR">
| @specialize<Name, "OBJECT">
| @specialize<Name, "FIELD_DEFINITION">
| @specialize<Name, "ARGUMENT_DEFINITION">
| @specialize<Name, "INTERFACE">
| @specialize<Name, "UNION">
| @specialize<Name, "ENUM">
| @specialize<Name, "ENUM_VALUE">
| @specialize<Name, "INPUT_OBJECT">
| @specialize<Name, "INPUT_FIELD_DEFINITION">
}
@skip { Whitespace | Comment }
@tokens {
Whitespace {
std.whitespace+
}
StringValue {
"\"\"\"" (!["] | "\\n" | "\"" "\""? !["])* "\"\"\"" | "\"" !["\\\n]* "\""
}
IntValue {
"-"? "0"
| "-"? std.digit+
}
FloatValue {
IntValue ("." std.digit+ | ("e" | "E") IntValue+)
}
@precedence { IntValue, FloatValue }
Name {
$[_A-Za-z] $[_0-9A-Za-z]*
}
Comment {
"#" ![\n]*
}
Comma {
","
}
}
@detectDelim

View File

@@ -0,0 +1 @@
# TODO: Write Lezer Tests

View File

@@ -0,0 +1,17 @@
import {GQLLanguage} from "../dist/index.js"
import {fileTests} from "lezer-generator/dist/test"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from 'url';
let caseDir = path.dirname(fileURLToPath(import.meta.url))
for (let file of fs.readdirSync(caseDir)) {
if (!/\.txt$/.test(file)) continue
let name = /^[^\.]*/.exec(file)[0]
describe(name, () => {
for (let {name, run} of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file))
it(name, () => run(GQLLanguage.parser))
})
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"strict": true,
"target": "es6",
"module": "es2020",
"newLine": "lf",
"declaration": true,
"moduleResolution": "node",
"allowJs": true,
},
"include": ["src/*"]
}

View File

@@ -32,8 +32,11 @@
}
::selection {
@apply bg-accent;
@apply text-accentContrast;
@apply bg-divider;
}
.cm-focused {
@apply !outline-none;
}
input::placeholder,
@@ -286,7 +289,7 @@ pre.ace_editor {
}
}
input[type="checkbox"] {
input[type="checkbox"].checkbox {
@apply hidden;
&,

View File

@@ -52,6 +52,48 @@
--editor-theme: "twilight";
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.purple.500");
--editor-name-color: theme("colors.blue.500");
--editor-operator-color: theme("colors.indigo.500");
--editor-invalid-color: theme("colors.red.500");
--editor-separator-color: theme("colors.gray.500");
--editor-meta-color: theme("colors.gray.500");
--editor-variable-color: theme("colors.green.500");
--editor-link-color: theme("colors.cyan.500");
--editor-process-color: theme("colors.gray.400");
--editor-constant-color: theme("colors.fuchsia.500");
--editor-keyword-color: theme("colors.pink.500");
}
@mixin light-editor-theme {
--editor-type-color: theme("colors.purple.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.green.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.gray.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.blue.400");
--editor-constant-color: theme("colors.fuchsia.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin green-theme {
--accent-color: theme("colors.green.500");
--accent-light-color: theme("colors.green.400");
@@ -146,18 +188,22 @@
@include base-theme;
@include dark-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
}
:root.black {
@include black-theme;
@include black-editor-theme;
}
:root[data-accent="blue"] {

View File

@@ -310,8 +310,6 @@ import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { getCurrentStrategyID } from "~/helpers/network"
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { useCodemirror } from "~/helpers/editor/codemirror"
import "codemirror/mode/javascript/javascript"
import "~/helpers/editor/modes/graphql"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"

View File

@@ -254,7 +254,6 @@ import {
setGQLURL,
setGQLVariables,
} from "~/newstore/GQLSession"
import "~/helpers/editor/modes/graphql"
function isTextFoundInGraphqlFieldObject(
text: string,

View File

@@ -30,7 +30,6 @@ import {
makeRESTRequest,
} from "~/helpers/types/HoppRESTRequest"
import { setRESTRequest } from "~/newstore/RESTSession"
import "codemirror/mode/shell/shell"
const {
$toast,

View File

@@ -177,7 +177,6 @@ import {
deleteAllRESTParams,
setRESTParams,
} from "~/newstore/RESTSession"
import "codemirror/mode/yaml/yaml"
const {
$toast,

View File

@@ -85,7 +85,6 @@
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { usePreRequestScript } from "~/newstore/RESTSession"
import snippets from "~/helpers/preRequestScriptSnippets"
import "codemirror/mode/javascript/javascript"
import { useCodemirror } from "~/helpers/editor/codemirror"
import linter from "~/helpers/editor/linting/preRequest"
import completer from "~/helpers/editor/completion/preRequest"

View File

@@ -72,11 +72,6 @@ import { useCodemirror } from "~/helpers/editor/codemirror"
import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import "codemirror/mode/yaml/yaml"
import "codemirror/mode/xml/xml"
import "codemirror/mode/css/css"
import "codemirror/mode/htmlmixed/htmlmixed"
import "codemirror/mode/javascript/javascript"
const props = defineProps<{
contentType: string

View File

@@ -85,7 +85,6 @@
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { useTestScript } from "~/newstore/RESTSession"
import testSnippets from "~/helpers/testSnippets"
import "codemirror/mode/javascript/javascript"
import { useCodemirror } from "~/helpers/editor/codemirror"
import linter from "~/helpers/editor/linting/testScript"
import completer from "~/helpers/editor/completion/testScript"

View File

@@ -67,10 +67,6 @@
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/xml/xml"
import "codemirror/mode/javascript/javascript"
import "codemirror/mode/css/css"
import "codemirror/mode/htmlmixed/htmlmixed"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
const props = defineProps<{

View File

@@ -147,7 +147,6 @@
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/javascript/javascript"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
import { getJSONOutlineAtPos } from "~/helpers/newOutline"

View File

@@ -51,7 +51,6 @@
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/xml/xml"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
const props = defineProps<{

View File

@@ -1,215 +1,324 @@
import CodeMirror from "codemirror"
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 } from "@codemirror/commands"
import { Completion, autocompletion } from "@codemirror/autocomplete"
import { linter } from "@codemirror/lint"
import "codemirror-theme-github/theme/github.css"
import "codemirror/theme/base16-dark.css"
import "codemirror/theme/tomorrow-night-bright.css"
import {
watch,
ref,
Ref,
onMounted,
onBeforeUnmount,
} from "@nuxtjs/composition-api"
import "codemirror/lib/codemirror.css"
import "codemirror/addon/lint/lint.css"
import "codemirror/addon/dialog/dialog.css"
import "codemirror/addon/hint/show-hint.css"
import "codemirror/addon/fold/foldgutter.css"
import "codemirror/addon/fold/foldgutter"
import "codemirror/addon/fold/brace-fold"
import "codemirror/addon/fold/comment-fold"
import "codemirror/addon/fold/indent-fold"
import "codemirror/addon/display/autorefresh"
import "codemirror/addon/lint/lint"
import "codemirror/addon/hint/show-hint"
import "codemirror/addon/display/placeholder"
import "codemirror/addon/edit/closebrackets"
import "codemirror/addon/search/search"
import "codemirror/addon/search/searchcursor"
import "codemirror/addon/search/jump-to-line"
import "codemirror/addon/dialog/dialog"
import "codemirror/addon/selection/active-line"
import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api"
import { LinterDefinition } from "./linting/linter"
import { javascriptLanguage } from "@codemirror/lang-javascript"
import { jsonLanguage } from "@codemirror/lang-json"
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { isJSONContentType } from "../utils/contenttypes"
import { Completer } from "./completion"
import { LinterDefinition } from "./linting/linter"
import { basicSetup, baseTheme, baseHighlightStyle } from "./themes/baseTheme"
type ExtendedEditorConfig = {
mode: string
placeholder: string
readOnly: boolean
lineWrapping: boolean
}
type CodeMirrorOptions = {
extendedEditorConfig: Omit<CodeMirror.EditorConfiguration, "value">
extendedEditorConfig: Partial<ExtendedEditorConfig>
linter: LinterDefinition | null
completer: Completer | null
}
const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
autoRefresh: true,
lineNumbers: true,
foldGutter: true,
autoCloseBrackets: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"Ctrl-Space": "autocomplete",
},
viewportMargin: Infinity,
styleActiveLine: true,
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,
}
},
],
})
}
/**
* A Vue composable to mount and use Codemirror
*
* NOTE: Make sure to import all the necessary Codemirror modules,
* as this function doesn't import any other than the core
* @param el Reference to the dom node to attach to
* @param value Reference to value to read/write to
* @param options CodeMirror options to pass
*/
const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
return linter(async (view) => {
// 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 + 1).from + result.from.ch
const endPos = view.state.doc.line(result.to.line + 1).from + result.to.ch
return {
from: startPos,
to: endPos,
message: result.message,
severity: result.severity,
}
})
})
}
const hoppLang = (
language: Language,
linter?: LinterDefinition | undefined,
completer?: Completer | undefined
) => {
const exts: Extension[] = []
if (linter) exts.push(hoppLinterExt(linter))
if (completer) exts.push(hoppCompleterExt(completer))
return new LanguageSupport(language, 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
}
// None matched, so return null
return null
}
const getEditorLanguage = (
langMime: string,
linter: LinterDefinition | undefined,
completer: Completer | undefined
): Extension =>
pipe(
O.fromNullable(getLanguage(langMime)),
O.map((lang) => hoppLang(lang, linter, completer)),
O.getOrElseW(() => [])
)
export function useCodemirror(
el: Ref<any | null>,
value: Ref<string>,
options: CodeMirrorOptions
): { cm: Ref<CodeMirror.Position | null>; cursor: Ref<CodeMirror.Position> } {
const { $colorMode } = useContext() as any
): { cursor: Ref<{ line: number; ch: number }> } {
const language = new Compartment()
const lineWrapping = new Compartment()
const placeholderConfig = new Compartment()
const cm = ref<CodeMirror.Editor | null>(null)
const cursor = ref<CodeMirror.Position>({ line: 0, ch: 0 })
const cachedCursor = ref({
line: 0,
ch: 0,
})
const cursor = ref({
line: 0,
ch: 0,
})
const updateEditorConfig = () => {
Object.keys(options.extendedEditorConfig).forEach((key) => {
// Only update options which need updating
if (
cm.value &&
cm.value?.getOption(key as any) !==
(options.extendedEditorConfig as any)[key]
) {
cm.value?.setOption(
key as any,
(options.extendedEditorConfig as any)[key]
)
}
const cachedValue = ref(value.value)
const view = ref<EditorView>()
const initView = (el: any) => {
view.value = new EditorView({
parent: el,
state: EditorState.create({
doc: value.value,
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),
],
}),
})
}
const updateLinterConfig = () => {
if (options.linter) {
cm.value?.setOption("lint", options.linter)
onMounted(() => {
if (el.value) {
if (!view.value) initView(el.value)
}
}
})
const updateCompleterConfig = () => {
if (options.completer) {
cm.value?.setOption("hintOptions", {
completeSingle: false,
hint: async (editor: CodeMirror.Editor) => {
const pos = editor.getCursor()
const text = editor.getValue()
watch(el, () => {
if (el.value) {
if (!view.value) initView(el.value)
} else {
view.value?.destroy()
view.value = undefined
}
})
const token = editor.getTokenAt(pos)
// It's not a word token, so, just increment to skip to next
if (token.string.toUpperCase() === token.string.toLowerCase())
token.start += 1
onBeforeUnmount(() => {
view.value?.destroy()
})
const result = await options.completer!(text, pos)
if (!result) return null
return <CodeMirror.Hints>{
from: { line: pos.line, ch: token.start },
to: { line: pos.line, ch: token.end },
list: result.completions
.sort((a, b) => a.score - b.score)
.map((x) => x.text),
}
watch(value, (newVal) => {
if (cachedValue.value !== newVal) {
view.value?.dispatch({
filter: false,
changes: {
from: 0,
to: view.value.state.doc.length,
insert: newVal,
},
})
}
}
const initialize = () => {
if (!el.value) return
cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
cm.value.setValue(value.value)
setTheme()
updateEditorConfig()
updateLinterConfig()
updateCompleterConfig()
cm.value.on("change", (instance) => {
// External update propagation (via watchers) should be ignored
if (instance.getValue() !== value.value) {
value.value = instance.getValue()
}
})
cm.value.on("cursorActivity", (instance) => {
cursor.value = instance.getCursor()
})
}
// Boot-up CodeMirror, set the value and listeners
onMounted(() => {
initialize()
})
// Reinitialize if the target ref updates
watch(el, () => {
if (cm.value) {
const parent = cm.value.getWrapperElement()
parent.remove()
cm.value = null
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
)
),
})
}
initialize()
})
)
const setTheme = () => {
if (cm.value) {
cm.value?.setOption("theme", getThemeName($colorMode.value))
watch(
() => options.extendedEditorConfig.lineWrapping,
(newMode) => {
view.value?.dispatch({
effects: lineWrapping.reconfigure(
newMode ? [EditorView.lineWrapping] : []
),
})
}
}
)
const getThemeName = (mode: string) => {
switch (mode) {
case "system":
return "default"
case "light":
return "github"
case "dark":
return "base16-dark"
case "black":
return "tomorrow-night-bright"
default:
return "default"
watch(
() => options.extendedEditorConfig.placeholder,
(newValue) => {
view.value?.dispatch({
effects: placeholderConfig.reconfigure(placeholder(newValue ?? "")),
})
}
}
)
// If the editor properties are reactive, watch for updates
watch(() => options.extendedEditorConfig, updateEditorConfig, {
immediate: true,
deep: true,
})
watch(() => options.linter, updateLinterConfig, { immediate: true })
watch(() => options.completer, updateCompleterConfig, { immediate: true })
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)
// Watch value updates
watch(value, (newVal) => {
// Check if we are mounted
if (cm.value) {
// Don't do anything on internal updates
if (cm.value.getValue() !== newVal) {
cm.value.setValue(newVal)
view.value?.focus()
view.value.dispatch({
scrollIntoView: true,
selection: selUpdate,
effects: EditorView.scrollTo.of(selUpdate),
})
}
}
})
// Push cursor updates
watch(cursor, (value) => {
if (value !== cm.value?.getCursor()) {
cm.value?.focus()
cm.value?.setCursor(value)
}
})
// Watch color mode updates and update theme
watch(() => $colorMode.value, setTheme)
return {
cm,
cursor,
}
}

View File

@@ -1,80 +0,0 @@
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import CodeMirror from "codemirror"
import {
LexRules,
ParseRules,
isIgnored,
onlineParser,
State,
} from "graphql-language-service-parser"
/**
* The GraphQL mode is defined as a tokenizer along with a list of rules, each
* of which is either a function or an array.
*
* * Function: Provided a token and the stream, returns an expected next step.
* * Array: A list of steps to take in order.
*
* A step is either another rule, or a terminal description of a token. If it
* is a rule, that rule is pushed onto the stack and the parsing continues from
* that point.
*
* If it is a terminal description, the token is checked against it using a
* `match` function. If the match is successful, the token is colored and the
* rule is stepped forward. If the match is unsuccessful, the remainder of the
* rule is skipped and the previous rule is advanced.
*
* This parsing algorithm allows for incremental online parsing within various
* levels of the syntax tree and results in a structured `state` linked-list
* which contains the relevant information to produce valuable typeaheads.
*/
CodeMirror.defineMode("graphql", (config) => {
const parser = onlineParser({
eatWhitespace: (stream) => stream.eatWhile(isIgnored),
lexRules: LexRules,
parseRules: ParseRules,
editorConfig: { tabSize: 2 },
})
return {
config,
startState: parser.startState,
token: parser.token as unknown as CodeMirror.Mode<any>["token"], // TODO: Check if the types are indeed compatible
indent,
electricInput: /^\s*[})\]]/,
fold: "brace",
lineComment: "#",
closeBrackets: {
pairs: '()[]{}""',
explode: "()[]{}",
},
}
})
// Seems the electricInput type in @types/codemirror is wrong (i.e it is written as electricinput instead of electricInput)
function indent(
this: CodeMirror.Mode<any> & {
electricInput?: RegExp
config?: CodeMirror.EditorConfiguration
},
state: State,
textAfter: string
) {
const levels = state.levels
// If there is no stack of levels, use the current level.
// Otherwise, use the top level, pre-emptively dedenting for close braces.
const level =
!levels || levels.length === 0
? state.indentLevel
: levels[levels.length - 1] -
(this.electricInput?.test(textAfter) ? 1 : 0)
return (level || 0) * (this.config?.indentUnit || 0)
}

View File

@@ -0,0 +1,233 @@
import {
EditorView,
keymap,
highlightSpecialChars,
highlightActiveLine,
} from "@codemirror/view"
import {
HighlightStyle,
tags as t,
defaultHighlightStyle,
} from "@codemirror/highlight"
import { foldKeymap, foldGutter } from "@codemirror/fold"
import { Extension, EditorState } from "@codemirror/state"
import { history, historyKeymap } from "@codemirror/history"
import { indentOnInput } from "@codemirror/language"
import { lineNumbers, highlightActiveLineGutter } from "@codemirror/gutter"
import { defaultKeymap } from "@codemirror/commands"
import { bracketMatching } from "@codemirror/matchbrackets"
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets"
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"
import { autocompletion, completionKeymap } from "@codemirror/autocomplete"
import { commentKeymap } from "@codemirror/comment"
import { rectangularSelection } from "@codemirror/rectangular-selection"
import { lintKeymap } from "@codemirror/lint"
export const baseTheme = EditorView.theme({
"&": {
fontSize: "var(--body-font-size)",
},
".cm-content": {
caretColor: "var(--secondary-light-color)",
fontFamily: "var(--font-mono)",
backgroundColor: "var(--primary-color)",
},
".cm-cursor": {
borderColor: "var(--secondary-color)",
},
".cm-selectionBackground, .cm-content ::selection, .cm-line ::selection": {
backgroundColor: "var(--divider-color)",
},
".cm-panels": {
backgroundColor: "var(--primary-light-color)",
color: "var(--secondary-light-color)",
},
".cm-panels.cm-panels-top": {
borderBottom: "1px solid var(--divider-light-color)",
},
".cm-panels.cm-panels-bottom": {
borderTop: "1px solid var(--divider-light-color)",
},
".cm-search": {
display: "flex",
alignItems: "center",
flexWrap: "nowrap",
flexShrink: 0,
overflow: "auto",
},
".cm-search label": {
display: "inline-flex",
alignItems: "center",
},
".cm-textfield": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-light-color)",
borderColor: "var(--divider-light-color)",
borderRadius: "3px",
},
".cm-button": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-light-color)",
backgroundImage: "none",
border: "none",
},
".cm-tooltip": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-light-color)",
border: "none",
borderRadius: "3px",
},
".cm-completionLabel": {
color: "var(--secondary-color)",
},
".cm-tooltip.cm-tooltip-autocomplete > ul": {
fontFamily: "var(--font-mono)",
},
".cm-tooltip-autocomplete ul li[aria-selected]": {
backgroundColor: "var(--accent-dark-color)",
color: "var(--accent-contrast-color)",
},
".cm-tooltip-autocomplete ul li[aria-selected] .cm-completionLabel": {
color: "var(--accent-contrast-color)",
},
".cm-activeLine": { backgroundColor: "var(--primary-light-color)" },
".cm-searchMatch": {
outline: "1px solid var(--accent-dark-color)",
},
".cm-selectionMatch": {
outline: "1px solid var(--accent-dark-color)",
},
".cm-matchingBracket, .cm-nonmatchingBracket": {
backgroundColor: "var(--divider-color)",
outline: "1px solid var(--accent-dark-color)",
},
".cm-gutters": {
fontFamily: "var(--font-mono)",
backgroundColor: "var(--primary-color)",
borderColor: "var(--divider-light-color)",
},
".cm-lineNumbers": {
minWidth: "3em",
color: "var(--secondary-light-color)",
},
".cm-foldGutter": {
minWidth: "2em",
color: "var(--secondary-light-color)",
},
".cm-foldGutter .cm-gutterElement": {
textAlign: "center",
},
".cm-line": {
paddingLeft: "0.5em",
paddingRight: "0.5em",
color: "var(--secondary-dark-color)",
},
".cm-activeLineGutter": {
backgroundColor: "var(--primary-dark-color)",
},
".cm-scroller::-webkit-scrollbar": {
display: "none",
},
})
const editorTypeColor = "var(--editor-type-color)"
const editorNameColor = "var(--editor-name-color)"
const editorOperatorColor = "var(--editor-operator-color)"
const editorInvalidColor = "var(--editor-invalid-color)"
const editorSeparatorColor = "var(--editor-separator-color)"
const editorMetaColor = "var(--editor-meta-color)"
const editorVariableColor = "var(--editor-variable-color)"
const editorLinkColor = "var(--editor-link-color)"
const editorProcessColor = "var(--editor-process-color)"
const editorConstantColor = "var(--editor-constant-color)"
const editorKeywordColor = "var(--editor-keyword-color)"
export const baseHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: editorKeywordColor },
{
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
color: editorNameColor,
},
{
tag: [t.function(t.variableName), t.labelName],
color: editorVariableColor,
},
{
tag: [t.color, t.constant(t.name), t.standard(t.name)],
color: editorConstantColor,
},
{ tag: [t.definition(t.name), t.separator], color: editorSeparatorColor },
{
tag: [
t.typeName,
t.className,
t.number,
t.changed,
t.annotation,
t.modifier,
t.self,
t.namespace,
],
color: editorTypeColor,
},
{
tag: [
t.operator,
t.operatorKeyword,
t.url,
t.escape,
t.regexp,
t.link,
t.special(t.string),
],
color: editorOperatorColor,
},
{ tag: [t.meta, t.comment], color: editorMetaColor },
{ tag: t.strong, fontWeight: "bold" },
{ tag: t.emphasis, fontStyle: "italic" },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.link, color: editorLinkColor, textDecoration: "underline" },
{ tag: t.heading, fontWeight: "bold", color: editorNameColor },
{
tag: [t.atom, t.bool, t.special(t.variableName)],
color: editorConstantColor,
},
{
tag: [t.processingInstruction, t.string, t.inserted],
color: editorProcessColor,
},
{ tag: t.invalid, color: editorInvalidColor },
])
const baseFoldStyle = foldGutter({
openText: "▾",
closedText: "▸",
})
export const basicSetup: Extension = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
baseFoldStyle,
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
defaultHighlightStyle.fallback,
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...commentKeymap,
...completionKeymap,
...lintKeymap,
]),
]

View File

@@ -34,6 +34,25 @@
},
"dependencies": {
"@apollo/client": "^3.4.17",
"@codemirror/autocomplete": "^0.19.4",
"@codemirror/closebrackets": "^0.19.0",
"@codemirror/commands": "^0.19.5",
"@codemirror/comment": "^0.19.0",
"@codemirror/fold": "^0.19.1",
"@codemirror/gutter": "^0.19.4",
"@codemirror/highlight": "^0.19.0",
"@codemirror/history": "^0.19.0",
"@codemirror/lang-javascript": "^0.19.2",
"@codemirror/lang-json": "^0.19.1",
"@codemirror/language": "^0.19.3",
"@codemirror/lint": "^0.19.2",
"@codemirror/matchbrackets": "^0.19.3",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.3",
"@codemirror/text": "^0.19.5",
"@codemirror/view": "^0.19.12",
"@hoppscotch/codemirror-lang-graphql": "workspace:^0.1.0",
"@hoppscotch/js-sandbox": "workspace:^1.0.0",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.30.0",
@@ -48,8 +67,6 @@
"acorn": "^8.5.0",
"acorn-walk": "^8.2.0",
"axios": "^0.24.0",
"codemirror": "^5.63.3",
"codemirror-theme-github": "^1.0.0",
"core-js": "^3.19.1",
"esprima": "^4.0.1",
"firebase": "^9.4.1",

732
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff