refactor: merge branch 'main' into refactor/monorepo

This commit is contained in:
Andrew Bastin
2021-09-16 22:24:21 +05:30
98 changed files with 3969 additions and 3007 deletions

View File

@@ -0,0 +1,215 @@
import CodeMirror from "codemirror"
import "codemirror-theme-github/theme/github.css"
import "codemirror/theme/base16-dark.css"
import "codemirror/theme/tomorrow-night-bright.css"
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 { Completer } from "./completion"
type CodeMirrorOptions = {
extendedEditorConfig: Omit<CodeMirror.EditorConfiguration, "value">
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,
}
/**
* 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
*/
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
const cm = ref<CodeMirror.Editor | null>(null)
const cursor = ref<CodeMirror.Position>({ 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 updateLinterConfig = () => {
if (options.linter) {
cm.value?.setOption("lint", options.linter)
}
}
const updateCompleterConfig = () => {
if (options.completer) {
cm.value?.setOption("hintOptions", {
completeSingle: false,
hint: async (editor: CodeMirror.Editor) => {
const pos = editor.getCursor()
const text = editor.getValue()
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
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),
}
},
})
}
}
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
}
initialize()
})
const setTheme = () => {
if (cm.value) {
cm.value?.setOption("theme", getThemeName($colorMode.value))
}
}
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"
}
}
// 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 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)
}
}
})
// 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

@@ -0,0 +1,27 @@
import { Ref } from "@nuxtjs/composition-api"
import { GraphQLSchema } from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { Completer, CompleterResult, CompletionEntry } from "."
const completer: (schemaRef: Ref<GraphQLSchema | null>) => Completer =
(schemaRef: Ref<GraphQLSchema | null>) => (text, completePos) => {
if (!schemaRef.value) return Promise.resolve(null)
const completions = getAutocompleteSuggestions(schemaRef.value, text, {
line: completePos.line,
character: completePos.ch,
} as any)
return Promise.resolve(<CompleterResult>{
completions: completions.map(
(x, i) =>
<CompletionEntry>{
text: x.label!,
meta: x.detail!,
score: completions.length - i,
}
),
})
}
export default completer

View File

@@ -0,0 +1,23 @@
export type CompletionEntry = {
text: string
meta: string
score: number
}
export type CompleterResult = {
/**
* List of completions to display
*/
completions: CompletionEntry[]
}
export type Completer = (
/**
* The contents of the editor
*/
text: string,
/**
* Position where the completer is fired
*/
completePos: { line: number; ch: number }
) => Promise<CompleterResult | null>

View File

@@ -0,0 +1,24 @@
import { Completer, CompletionEntry } from "."
import { getPreRequestScriptCompletions } from "~/helpers/tern"
const completer: Completer = async (text, completePos) => {
const results = await getPreRequestScriptCompletions(
text,
completePos.line,
completePos.ch
)
const completions = results.completions.map((completion: any, i: number) => {
return <CompletionEntry>{
text: completion.name,
meta: completion.isKeyword ? "keyword" : completion.type,
score: results.completions.length - i,
}
})
return {
completions,
}
}
export default completer

View File

@@ -0,0 +1,24 @@
import { Completer, CompletionEntry } from "."
import { getTestScriptCompletions } from "~/helpers/tern"
export const completer: Completer = async (text, completePos) => {
const results = await getTestScriptCompletions(
text,
completePos.line,
completePos.ch
)
const completions = results.completions.map((completion: any, i: number) => {
return <CompletionEntry>{
text: completion.name,
meta: completion.isKeyword ? "keyword" : completion.type,
score: results.completions.length - i,
}
})
return {
completions,
}
}
export default completer

View File

@@ -0,0 +1,58 @@
import { Ref } from "@nuxtjs/composition-api"
import {
GraphQLError,
GraphQLSchema,
parse as gqlParse,
validate as gqlValidate,
} from "graphql"
import { LinterDefinition, LinterResult } from "./linter"
/**
* Creates a Linter function that can lint a GQL query against a given
* schema
*/
export const createGQLQueryLinter: (
schema: Ref<GraphQLSchema | null>
) => LinterDefinition = (schema: Ref<GraphQLSchema | null>) => (text) => {
if (text === "") return Promise.resolve([])
if (!schema.value) return Promise.resolve([])
try {
const doc = gqlParse(text)
const results = gqlValidate(schema.value, doc).map(
({ locations, message }) =>
<LinterResult>{
from: {
line: locations![0].line - 1,
ch: locations![0].column - 1,
},
to: {
line: locations![0].line - 1,
ch: locations![0].column,
},
message,
severity: "error",
}
)
return Promise.resolve(results)
} catch (e) {
const err = e as GraphQLError
return Promise.resolve([
<LinterResult>{
from: {
line: err.locations![0].line - 1,
ch: err.locations![0].column - 1,
},
to: {
line: err.locations![0].line - 1,
ch: err.locations![0].column,
},
message: err.message,
severity: "error",
},
])
}
}

View File

@@ -0,0 +1,21 @@
import { convertIndexToLineCh } from "../utils"
import { LinterDefinition, LinterResult } from "./linter"
import jsonParse from "~/helpers/jsonParse"
const linter: LinterDefinition = (text) => {
try {
jsonParse(text)
return Promise.resolve([])
} catch (e: any) {
return Promise.resolve([
<LinterResult>{
from: convertIndexToLineCh(text, e.start),
to: convertIndexToLineCh(text, e.end),
message: e.message,
severity: "error",
},
])
}
}
export default linter

View File

@@ -0,0 +1,7 @@
export type LinterResult = {
message: string
severity: "warning" | "error"
from: { line: number; ch: number }
to: { line: number; ch: number }
}
export type LinterDefinition = (text: string) => Promise<LinterResult[]>

View File

@@ -0,0 +1,69 @@
import * as esprima from "esprima"
import { LinterDefinition, LinterResult } from "./linter"
import { performPreRequestLinting } from "~/helpers/tern"
const linter: LinterDefinition = async (text) => {
let results: LinterResult[] = []
// Semantic linting
const semanticLints = await performPreRequestLinting(text)
results = results.concat(
semanticLints.map((lint: any) => ({
from: lint.from,
to: lint.to,
severity: "error",
message: `[semantic] ${lint.message}`,
}))
)
// Syntax linting
try {
const res: any = esprima.parseScript(text, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err: any) => {
const fromPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column - 1,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column - 1,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column,
}
results = results.concat([
<LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${e.description}`,
severity: "error",
},
])
}
return results
}
export default linter

View File

@@ -0,0 +1,69 @@
import * as esprima from "esprima"
import { LinterDefinition, LinterResult } from "./linter"
import { performTestLinting } from "~/helpers/tern"
const linter: LinterDefinition = async (text) => {
let results: LinterResult[] = []
// Semantic linting
const semanticLints = await performTestLinting(text)
results = results.concat(
semanticLints.map((lint: any) => ({
from: lint.from,
to: lint.to,
severity: "error",
message: `[semantic] ${lint.message}`,
}))
)
// Syntax linting
try {
const res: any = esprima.parseScript(text, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err: any) => {
const fromPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column - 1,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column - 1,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column,
}
results = results.concat([
<LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${e.description}`,
severity: "error",
},
])
}
return results
}
export default linter

View File

@@ -0,0 +1,80 @@
/**
* 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,38 @@
export function convertIndexToLineCh(
text: string,
i: number
): { line: number; ch: number } {
const lines = text.split("\n")
let line = 0
let counter = 0
while (line < lines.length) {
if (i > lines[line].length + counter) {
counter += lines[line].length + 1
line++
} else {
return {
line: line + 1,
ch: i - counter + 1,
}
}
}
throw new Error("Invalid input")
}
export function convertLineChToIndex(
text: string,
lineCh: { line: number; ch: number }
): number {
const textSplit = text.split("\n")
if (textSplit.length < lineCh.line) throw new Error("Invalid position")
const tillLineIndex = textSplit
.slice(0, lineCh.line)
.reduce((acc, line) => acc + line.length + 1, 0)
return tillLineIndex + lineCh.ch
}