perf: remove ace editor

This commit is contained in:
liyasthomas
2021-09-10 18:52:58 +05:30
parent b4ed6fd107
commit 41a02f059d
8 changed files with 7 additions and 904 deletions

View File

@@ -1,254 +0,0 @@
<template>
<div class="opacity-0 show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as gql from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { defineComponent } from "@nuxtjs/composition-api"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import debounce from "~/helpers/utils/debounce"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
onRunGQLQuery: {
type: Function,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
validationSchema: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
defineGQLLanguageMode(ace)
const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/gql-query`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.validationSchema) {
const completions = getAutocompleteSuggestions(
this.validationSchema,
editor.getValue(),
{
line: row,
character: column,
}
)
callback(
null,
completions.map(({ label, detail }) => ({
name: label,
value: label,
score: 1.0,
meta: detail,
}))
)
} else {
callback(null, [])
}
},
}
langTools.setCompleters([completer])
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
editor.commands.addCommand({
name: "runGQLQuery",
exec: () => this.onRunGQLQuery(this.editor.getValue()),
bindKey: {
mac: "cmd-enter",
win: "ctrl-enter",
},
})
editor.commands.addCommand({
name: "prettifyGQLQuery",
exec: () => this.prettifyQuery(),
bindKey: {
mac: "cmd-p",
win: "ctrl-p",
},
})
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.parseContents(content)
this.cacheValue = content
})
this.parseContents(this.value)
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
prettifyQuery() {
try {
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
} catch (e) {
this.$toast.error(this.$t("error.gql_prettify_invalid_query"), {
icon: "error_outline",
})
}
},
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
setValidationSchema(schema) {
this.validationSchema = schema
this.parseContents(this.cacheValue)
},
parseContents: debounce(function (content) {
if (content !== "") {
try {
const doc = gql.parse(content)
if (this.validationSchema) {
this.editor.session.setAnnotations(
gql
.validate(this.validationSchema, doc)
.map(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: message,
type: "error",
}))
)
}
} catch (e) {
this.editor.session.setAnnotations([
{
row: e.locations[0].line - 1,
column: e.locations[0].column - 1,
text: e.message,
type: "error",
},
])
}
} else {
this.editor.session.setAnnotations([])
}
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,282 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
<div
v-if="provideOutline"
class="
bg-primaryLight
border-t border-divider
flex flex-nowrap flex-1
py-1
px-4
bottom-0
z-10
sticky
overflow-auto
hide-scrollbar
"
>
<div
v-for="(p, index) in currentPath"
:key="`p-${index}`"
class="
cursor-pointer
flex-grow-0 flex-shrink-0
text-secondaryLight
inline-flex
items-center
hover:text-secondary
"
>
<span @click="onBlockClick(index)">
{{ p }}
</span>
<i v-if="index + 1 !== currentPath.length" class="mx-2 material-icons">
chevron_right
</i>
<tippy
v-if="siblingDropDownIndex == index"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<SmartItem
v-for="(sibling, siblingIndex) in currentSibling"
:key="`p-${index}-sibling-${siblingIndex}`"
:label="sibling.key ? sibling.key.value : i"
@click.native="goToSibling(sibling)"
/>
</tippy>
</div>
</div>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import { defineComponent } from "@nuxtjs/composition-api"
import jsonParse from "~/helpers/jsonParse"
import debounce from "~/helpers/utils/debounce"
import outline from "~/helpers/outline"
export default defineComponent({
props: {
provideOutline: {
type: Boolean,
default: false,
required: false,
},
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
lang: {
type: String,
default: "json",
},
lint: {
type: Boolean,
default: true,
required: false,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
outline: outline(),
currentPath: [],
currentSibling: [],
siblingDropDownIndex: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
lang(value) {
this.editor.getSession().setMode(`ace/mode/${value}`)
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/${this.lang}`,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
if (this.lang === "json" && this.provideOutline)
this.initOutline(this.value)
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.cacheValue = content
if (this.provideOutline) debounce(this.initOutline(content), 500)
if (this.lint) this.provideLinting(content)
})
if (this.lang === "json" && this.provideOutline) {
editor.session.selection.on("changeCursor", () => {
const index = editor.session.doc.positionToIndex(
editor.selection.getCursor(),
0
)
const path = this.outline.genPath(index)
if (path.success) {
this.currentPath = path.res
}
})
}
// Disable linting, if lint prop is false
if (this.lint) this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
if (this.lang === "json") {
try {
jsonParse(code)
this.editor.session.setAnnotations([])
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.start, 0)
this.editor.session.setAnnotations([
{
row: pos.row,
column: pos.column,
text: e.message,
type: "error",
},
])
}
}
}, 2000),
onBlockClick(index) {
if (this.siblingDropDownIndex === index) {
this.clearSiblingList()
} else {
this.currentSibling = this.outline.getSiblings(index)
if (this.currentSibling.length) this.siblingDropDownIndex = index
}
},
clearSiblingList() {
this.currentSibling = []
this.siblingDropDownIndex = null
},
goToSibling(obj) {
this.clearSiblingList()
if (obj.start) {
const pos = this.editor.session.doc.indexToPosition(obj.start, 0)
if (pos) {
this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
this.editor.session.selection.clearSelection()
this.editor.scrollToLine(pos.row, false, true, null)
}
}
},
initOutline: debounce(function (content) {
if (this.lang === "json") {
try {
this.outline.init(content)
if (content[0] === "[") this.currentPath.push("[]")
else this.currentPath.push("{}")
} catch (e) {
console.log("Outline error: ", e)
}
}
}),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -1,53 +0,0 @@
<template>
<div ref="editor" class="w-full block"></div>
</template>
<script setup lang="ts">
import "codemirror/mode/javascript/javascript"
import { ref, watch } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { LinterDefinition } from "~/helpers/editor/linting/linter"
import { Completer } from "~/helpers/editor/completion"
const props = withDefaults(
defineProps<{
value: string
mode: string
placeholder?: string
wrap?: boolean
linter: LinterDefinition | null
completer: Completer | null
}>(),
{
placeholder: "",
wrap: true,
linter: null as any,
completer: null as any,
}
)
const emit = defineEmits<{
(e: "input", value: string): void
}>()
const value = ref(props.value)
watch(
() => props.value,
(val) => (value.value = val)
)
watch(value, (val) => emit("input", val))
const editor = ref<any | null>(null)
useCodemirror(editor, value, {
extendedEditorConfig: {
mode: props.mode,
placeholder: props.placeholder,
lineWrapping: props.wrap,
},
linter: props.linter,
completer: props.completer,
})
</script>

View File

@@ -1,292 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as esprima from "esprima"
import { defineComponent } from "@nuxtjs/composition-api"
import debounce from "~/helpers/utils/debounce"
import {
getPreRequestScriptCompletions,
getTestScriptCompletions,
performPreRequestLinting,
performTestLinting,
} from "~/helpers/tern"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
completeMode: {
type: String,
required: true,
default: "none",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTick shouldn't really ever throw but still
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
// const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/javascript`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTIck shouldn't really ever throw but still
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.completeMode === "pre") {
getPreRequestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
} else if (this.completeMode === "test") {
getTestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
}
},
}
editor.completers = [completer]
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.cacheValue = content
this.provideLinting(content)
})
this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
let results = []
const lintFunc =
this.completeMode === "pre"
? performPreRequestLinting
: performTestLinting
lintFunc(code)
.then((semanticLints) => {
results = results.concat(
semanticLints.map((lint) => ({
row: lint.from.line,
column: lint.from.ch,
text: `[semantic] ${lint.message}`,
type: "error",
}))
)
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
.catch(() => {
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>