Skip to content

Commit

Permalink
Added Codemirror 6 legacy mode support (#2035)
Browse files Browse the repository at this point in the history
Implemented the codemirror.next legacy mode support for graphql mode.
This only covers syntax highlighting. Autocomplete, jump and info extensions
still need to be implemented to have feature parity with Codemirror v5.

From #2022
  • Loading branch information
imolorhe committed Nov 23, 2021
1 parent 25fb5c5 commit d0c22c4
Show file tree
Hide file tree
Showing 15 changed files with 534 additions and 79 deletions.
6 changes: 6 additions & 0 deletions .changeset/soft-spoons-love.md
@@ -0,0 +1,6 @@
---
'codemirror-graphql': minor
'example-cm6-graphql-parcel': patch
---

Added Codemirror 6 legacy support
1 change: 1 addition & 0 deletions examples/cm6-graphql-parcel/.gitignore
@@ -0,0 +1 @@
.cache/
9 changes: 9 additions & 0 deletions examples/cm6-graphql-parcel/README.md
@@ -0,0 +1,9 @@
## Codemirror 6 Parcel Example

This example demonstrates how to transpile your own custom ES6 Codemirror 6 GraphQL implementation with parcel bundler.

### Setup

1. `yarn` and `yarn build` at the root of this repository, if you have not already.
1. `yarn start` from this folder to start parcel dev mode.
1. `yarn build` to find production ready files.
35 changes: 35 additions & 0 deletions examples/cm6-graphql-parcel/package.json
@@ -0,0 +1,35 @@
{
"name": "example-cm6-graphql-parcel",
"version": "1.1.10-alpha.8",
"license": "MIT",
"description": "GraphiQL Parcel Example",
"main": "index.js",
"private": true,
"scripts": {
"start": "parcel src/index.html -p 8080",
"build": "parcel build src/index.html --public-url /"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@codemirror/basic-setup": "^0.19.0",
"@codemirror/stream-parser": "^0.19.2",
"codemirror-graphql": "^1.1.0",
"graphql": "16.0.0-experimental-stream-defer.5",
"typescript": "^3.4.4"
},
"devDependencies": {
"parcel-bundler": "^1.12.4",
"worker-loader": "^2.0.0"
}
}
23 changes: 23 additions & 0 deletions examples/cm6-graphql-parcel/src/index.html
@@ -0,0 +1,23 @@
<html lang="en">
<head>
<style>
body {
padding: 0;
margin: 0;
min-height: 100vh;
}
#root {
height: 100vh;
}
</style>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>CM6 GraphQL Editor Example</title>
</head>

<body>
<div id="editor"></div>
<script src="./index.ts"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/cm6-graphql-parcel/src/index.ts
@@ -0,0 +1,23 @@
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup';
import { StreamLanguage } from '@codemirror/stream-parser';
import { graphql } from 'codemirror-graphql/cm6-legacy/mode';
import query from './sample-query';

const state = EditorState.create({
doc: query,
extensions: [
basicSetup,
StreamLanguage.define(graphql),
// javascript(),
],
});

const view = new EditorView({
state,
parent: document.querySelector('#editor')!,
});

// Hot Module Replacement
if (module.hot) {
module.hot.accept();
}
57 changes: 57 additions & 0 deletions examples/cm6-graphql-parcel/src/sample-query.ts
@@ -0,0 +1,57 @@
const query = /* GraphQL */ `
# 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.
query queryName($foo: TestInput, $site: TestEnum = RED) {
testAlias: hasArgs(string: "testString")
... on Test {
hasArgs(
listEnum: [RED, GREEN, BLUE]
int: 1
listFloat: [1.23, 1.3e-1, -1.35384e+3]
boolean: true
id: 123
object: $foo
enum: $site
)
}
test @include(if: true) {
union {
__typename
}
}
...frag
... @skip(if: false) {
id
}
... {
id
}
}
mutation mutationName {
setString(value: "newString")
}
subscription subscriptionName {
subscribeToTest(id: "anId") {
... on Test {
id
}
}
}
fragment frag on Test {
test @include(if: true) {
union {
__typename
}
}
}
`;

export default query;
20 changes: 20 additions & 0 deletions examples/cm6-graphql-parcel/tsconfig.json
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["src"]
}
1 change: 1 addition & 0 deletions packages/codemirror-graphql/.gitignore
Expand Up @@ -10,4 +10,5 @@ coverage/
/utils
/variables
/results
/cm6-legacy
/__tests__
3 changes: 2 additions & 1 deletion packages/codemirror-graphql/package.json
Expand Up @@ -32,7 +32,7 @@
"build": "yarn build-clean && yarn build-js && yarn build-esm && yarn build-flow .",
"build-js": "yarn tsc",
"build-esm": "cross-env ESM=true yarn tsc --project tsconfig.esm.json && node ../../resources/renameFileExtensions.js './esm/{**,!**/__tests__/}/*.js' . .esm.js",
"build-clean": "rimraf {mode,hint,info,jump,lint}.{js,esm.js,js.flow,js.map,d.ts,d.ts.map} && rimraf esm results utils variables coverage __tests__",
"build-clean": "rimraf {mode,hint,info,jump,lint}.{js,esm.js,js.flow,js.map,d.ts,d.ts.map} && rimraf esm results utils variables coverage cm6-legacy __tests__",
"build-flow": "node ../../resources/buildFlow.js",
"watch": "babel --optional runtime resources/watch.js | node",
"test": "jest",
Expand All @@ -43,6 +43,7 @@
"graphql": ">= 15.5.0 <= 16.0.0-experimental-stream-defer.5"
},
"dependencies": {
"@codemirror/stream-parser": "^0.19.2",
"graphql-language-service-interface": "^2.9.0",
"graphql-language-service-parser": "^1.10.0"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/codemirror-graphql/src/cm6-legacy/mode.ts
@@ -0,0 +1,6 @@
import type { StreamParser } from '@codemirror/stream-parser';
import graphqlModeFactory from '../utils/mode-factory';

// Types of property 'token' are incompatible.
// Type '((stream: StringStream, state: any) => string | null) | undefined' is not comparable to type '(stream: StringStream, state: any) => string | null'.
export const graphql = (graphqlModeFactory({}) as unknown) as StreamParser<any>;
71 changes: 2 additions & 69 deletions packages/codemirror-graphql/src/mode.ts
Expand Up @@ -8,73 +8,6 @@
*/

import CodeMirror from 'codemirror';
import {
LexRules,
ParseRules,
isIgnored,
onlineParser,
State,
} from 'graphql-language-service-parser';
import modeFactory from './utils/mode-factory';

/**
* 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: config.tabSize },
});

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);
}
CodeMirror.defineMode('graphql', modeFactory);
64 changes: 64 additions & 0 deletions packages/codemirror-graphql/src/utils/mode-factory.ts
@@ -0,0 +1,64 @@
/**
* 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,
} from 'graphql-language-service-parser';
import indent from './mode-indent';

/**
* 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.
*/
const graphqlModeFactory: CodeMirror.ModeFactory<any> = config => {
const parser = onlineParser({
eatWhitespace: stream => stream.eatWhile(isIgnored),
lexRules: LexRules,
parseRules: ParseRules,
editorConfig: { tabSize: config.tabSize },
});

return {
config,
startState: parser.startState,
token: (parser.token as unknown) as NonNullable<
CodeMirror.Mode<any>['token']
>, // TODO: Check if the types are indeed compatible
indent,
electricInput: /^\s*[})\]]/,
fold: 'brace',
lineComment: '#',
closeBrackets: {
pairs: '()[]{}""',
explode: '()[]{}',
},
};
};

export default graphqlModeFactory;
31 changes: 31 additions & 0 deletions packages/codemirror-graphql/src/utils/mode-indent.ts
@@ -0,0 +1,31 @@
/**
* 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 { State } from 'graphql-language-service-parser';

// Seems the electricInput type in @types/codemirror is wrong (i.e it is written as electricinput instead of electricInput)
export default 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);
}

2 comments on commit d0c22c4

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.