chore: split app to commons and web (squash commit)

This commit is contained in:
Andrew Bastin
2022-12-02 02:57:46 -05:00
parent fb827e3586
commit 3d004f2322
535 changed files with 1487 additions and 501 deletions

View File

@@ -0,0 +1,27 @@
import { Ref } from "vue"
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,226 @@
import { watch, Ref } from "vue"
import { Compartment } from "@codemirror/state"
import {
Decoration,
EditorView,
MatchDecorator,
ViewPlugin,
hoverTooltip,
} from "@codemirror/view"
import * as E from "fp-ts/Either"
import { parseTemplateStringE } from "@hoppscotch/data"
import { StreamSubscriberFunc } from "@composables/stream"
import {
AggregateEnvironment,
aggregateEnvs$,
getAggregateEnvs,
getSelectedEnvironmentType,
} from "~/newstore/environments"
import { invokeAction } from "~/helpers/actions"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_ENV_HIGHLIGHT_FOUND =
"bg-accentDark text-accentContrast hover:bg-accent"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND =
"bg-red-500 text-accentContrast hover:bg-red-600"
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
hoverTooltip(
(view, pos, side) => {
const { from, to, text } = view.state.doc.lineAt(pos)
// TODO: When Codemirror 6 allows this to work (not make the
// popups appear half of the time) use this implementation
// const wordSelection = view.state.wordAt(pos)
// if (!wordSelection) return null
// const word = view.state.doc.sliceString(
// wordSelection.from - 2,
// wordSelection.to + 2
// )
// if (!HOPP_ENVIRONMENT_REGEX.test(word)) return null
// Tracking the start and the end of the words
let start = pos
let end = pos
while (start > from && /[a-zA-Z0-9-_]+/.test(text[start - from - 1]))
start--
while (end < to && /[a-zA-Z0-9-_]+/.test(text[end - from])) end++
if (
(start === pos && side < 0) ||
(end === pos && side > 0) ||
!HOPP_ENVIRONMENT_REGEX.test(
text.slice(start - from - 2, end - from + 2)
)
)
return null
const parsedEnvKey = text.slice(start - from, end - from)
const tooltipEnv = aggregateEnvs.find((env) => env.key === parsedEnvKey)
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
const envValue = tooltipEnv?.value ?? "Not found"
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
const selectedEnvType = getSelectedEnvironmentType()
const envTypeIcon = `<i class="inline-flex -my-1 -mx-0.5 opacity-65 items-center text-base material-icons border-secondary">${
selectedEnvType === "TEAM_ENV" ? "people" : "person"
}</i>`
const appendEditAction = (tooltip: HTMLElement) => {
const editIcon = document.createElement("span")
editIcon.className =
"ml-2 cursor-pointer env-icon text-accent hover:text-accentDark"
editIcon.addEventListener("click", () => {
const isPersonalEnv =
envName === "Global" || selectedEnvType !== "TEAM_ENV"
const action = isPersonalEnv ? "my" : "team"
invokeAction(`modals.${action}.environment.edit`, {
envName,
variableName: parsedEnvKey,
})
})
editIcon.innerHTML = `<i class="inline-flex items-center px-1 -mx-1 -my-1 text-base material-icons border-secondary">drive_file_rename_outline</i>`
tooltip.appendChild(editIcon)
}
return {
pos: start,
end: to,
above: true,
arrow: true,
create() {
const dom = document.createElement("span")
const tooltipContainer = document.createElement("span")
const kbd = document.createElement("kbd")
const icon = document.createElement("span")
icon.innerHTML = envTypeIcon
icon.className = "mr-2 env-icon"
kbd.textContent = finalEnv
tooltipContainer.appendChild(icon)
tooltipContainer.appendChild(document.createTextNode(`${envName} `))
tooltipContainer.appendChild(kbd)
if (tooltipEnv) appendEditAction(tooltipContainer)
tooltipContainer.className = "tippy-content"
dom.className = "tippy-box"
dom.dataset.theme = "tooltip"
dom.appendChild(tooltipContainer)
return { dom }
},
}
},
// HACK: This is a hack to fix hover tooltip not coming half of the time
// https://github.com/codemirror/tooltip/blob/765c463fc1d5afcc3ec93cee47d72606bed27e1d/src/tooltip.ts#L622
// Still doesn't fix the not showing up some of the time issue, but this is atleast more consistent
{ hoverTime: 1 } as any
)
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
const className = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
return Decoration.mark({
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
})
}
const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
new MatchDecorator({
regexp: HOPP_ENVIRONMENT_REGEX,
decoration: (m) => checkEnv(m[0], aggregateEnvs),
})
export const environmentHighlightStyle = (
aggregateEnvs: AggregateEnvironment[]
) => {
const decorator = getMatchDecorator(aggregateEnvs)
return ViewPlugin.define(
(view) => ({
decorations: decorator.createDeco(view),
update(u) {
this.decorations = decorator.updateDeco(u, this.decorations)
},
}),
{
decorations: (v) => v.decorations,
}
)
}
export class HoppEnvironmentPlugin {
private compartment = new Compartment()
private envs: AggregateEnvironment[] = []
constructor(
subscribeToStream: StreamSubscriberFunc,
private editorView: Ref<EditorView | undefined>
) {
this.envs = getAggregateEnvs()
subscribeToStream(aggregateEnvs$, (envs) => {
this.envs = envs
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
})
}
get extension() {
return this.compartment.of([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
])
}
}
export class HoppReactiveEnvPlugin {
private compartment = new Compartment()
private envs: AggregateEnvironment[] = []
constructor(
envsRef: Ref<AggregateEnvironment[]>,
private editorView: Ref<EditorView | undefined>
) {
watch(
envsRef,
(envs) => {
this.envs = envs
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true }
)
}
get extension() {
return this.compartment.of([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
])
}
}

View File

@@ -0,0 +1,58 @@
import { Ref } from "vue"
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,
ch: locations![0].column - 1,
},
to: {
line: locations![0].line,
ch: locations![0].column - 1,
},
message,
severity: "error",
}
)
return Promise.resolve(results)
} catch (e) {
const err = e as GraphQLError
return Promise.resolve([
<LinterResult>{
from: {
line: err.locations![0].line,
ch: err.locations![0].column - 1,
},
to: {
line: err.locations![0].line,
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,75 @@
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: {
ch: lint.from.ch + 1,
line: lint.from.line + 1,
},
to: {
ch: lint.from.ch + 1,
line: lint.to.line + 1,
},
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,
ch: err.column,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e: any) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber,
ch: e.column,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber,
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,24 @@
import * as E from "fp-ts/Either"
import { strictParseRawKeyValueEntriesE } from "@hoppscotch/data"
import { convertIndexToLineCh } from "../utils"
import { LinterDefinition, LinterResult } from "./linter"
const linter: LinterDefinition = (text) => {
const result = strictParseRawKeyValueEntriesE(text)
if (E.isLeft(result)) {
const pos = convertIndexToLineCh(text, result.left.pos)
return Promise.resolve([
<LinterResult>{
from: pos,
to: pos,
message: result.left.message,
severity: "error",
},
])
} else {
return Promise.resolve([])
}
}
export default linter

View File

@@ -0,0 +1,75 @@
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: {
ch: lint.from.ch + 1,
line: lint.from.line + 1,
},
to: {
ch: lint.from.ch + 1,
line: lint.to.line + 1,
},
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,
ch: err.column,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e: any) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber,
ch: e.column,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber,
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,413 @@
import {
EditorView,
keymap,
highlightSpecialChars,
highlightActiveLine,
drawSelection,
dropCursor,
lineNumbers,
highlightActiveLineGutter,
rectangularSelection,
crosshairCursor,
} from "@codemirror/view"
import {
HighlightStyle,
defaultHighlightStyle,
foldKeymap,
foldGutter,
indentOnInput,
bracketMatching,
syntaxHighlighting,
} from "@codemirror/language"
import { tags as t } from "@lezer/highlight"
import { Extension, EditorState } from "@codemirror/state"
import { history, historyKeymap, defaultKeymap } from "@codemirror/commands"
import {
closeBrackets,
closeBracketsKeymap,
autocompletion,
completionKeymap,
} from "@codemirror/autocomplete"
import {
searchKeymap,
highlightSelectionMatches,
search,
} from "@codemirror/search"
import { lintKeymap } from "@codemirror/lint"
export const baseTheme = EditorView.theme({
"&": {
fontSize: "var(--font-size-body)",
height: "100%",
width: "100%",
flex: "1",
},
".cm-content": {
caretColor: "var(--secondary-dark-color)",
fontFamily: "var(--font-mono)",
color: "var(--secondary-dark-color)",
backgroundColor: "transparent",
},
".cm-cursor": {
borderColor: "var(--secondary-color)",
},
".cm-widgetBuffer": {
position: "absolute",
},
".cm-selectionBackground": {
backgroundColor: "var(--accent-dark-color)",
color: "var(--accent-contrast-color)",
borderRadius: "2px",
},
".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-dark-color)",
borderColor: "var(--divider-light-color)",
borderRadius: "4px",
},
".cm-button": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-dark-color)",
backgroundImage: "none",
border: "none",
borderRadius: "4px",
},
".cm-completionLabel": {
color: "var(--secondary-color)",
},
".cm-tooltip": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-light-color)",
border: "none",
borderRadius: "4px",
},
".cm-tooltip-arrow": {
color: "var(--tooltip-color)",
},
".cm-tooltip-arrow:after": {
borderTopColor: "inherit !important",
},
".cm-tooltip-arrow:before": {
borderTopColor: "inherit !important",
},
".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: "transparent" },
".cm-searchMatch": {
outline: "1px solid var(--accent-dark-color)",
backgroundColor: "var(--divider-dark-color)",
borderRadius: "2px",
},
".cm-selectionMatch": {
outline: "1px solid var(--accent-dark-color)",
backgroundColor: "var(--divider-light-color)",
borderRadius: "2px",
},
".cm-matchingBracket, .cm-nonmatchingBracket": {
backgroundColor: "var(--divider-color)",
outline: "1px solid var(--accent-dark-color)",
borderRadius: "2px",
},
".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",
},
".cm-activeLineGutter": {
backgroundColor: "transparent",
},
".cm-scroller::-webkit-scrollbar": {
display: "none",
},
".cm-foldPlaceholder": {
backgroundColor: "var(--divider-light-color)",
color: "var(--secondary-dark-color)",
borderColor: "var(--divider-dark-color)",
},
})
export const inputTheme = EditorView.theme({
"&": {
fontSize: "var(--font-size-body)",
height: "100%",
width: "100%",
flex: "1",
},
".cm-content": {
caretColor: "var(--secondary-dark-color)",
fontFamily: "var(--font-sans)",
color: "var(--secondary-dark-color)",
backgroundColor: "transparent",
},
".cm-cursor": {
borderColor: "var(--secondary-color)",
},
".cm-widgetBuffer": {
position: "absolute",
},
".cm-selectionBackground": {
backgroundColor: "var(--accent-dark-color)",
color: "var(--accent-contrast-color)",
borderRadius: "2px",
},
".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-dark-color)",
borderColor: "var(--divider-light-color)",
borderRadius: "4px",
},
".cm-button": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-dark-color)",
backgroundImage: "none",
border: "none",
borderRadius: "4px",
},
".cm-completionLabel": {
color: "var(--secondary-color)",
},
".cm-tooltip": {
backgroundColor: "var(--primary-dark-color)",
color: "var(--secondary-light-color)",
border: "none",
borderRadius: "4px",
},
".cm-tooltip-arrow": {
color: "var(--tooltip-color)",
},
".cm-tooltip-arrow:after": {
borderTopColor: "currentColor !important",
},
".cm-tooltip-arrow:before": {
borderTopColor: "currentColor !important",
},
".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: "transparent" },
".cm-searchMatch": {
outline: "1px solid var(--accent-dark-color)",
backgroundColor: "var(--divider-dark-color)",
borderRadius: "2px",
},
".cm-selectionMatch": {
outline: "1px solid var(--accent-dark-color)",
backgroundColor: "var(--divider-light-color)",
borderRadius: "2px",
},
".cm-matchingBracket, .cm-nonmatchingBracket": {
backgroundColor: "var(--divider-color)",
outline: "1px solid var(--accent-dark-color)",
borderRadius: "2px",
},
".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: "1rem",
paddingRight: "1rem",
paddingTop: "0.2rem",
paddingBottom: "0.2rem",
},
".cm-activeLineGutter": {
backgroundColor: "transparent",
},
".cm-scroller::-webkit-scrollbar": {
display: "none",
},
".cm-foldPlaceholder": {
backgroundColor: "var(--divider-light-color)",
color: "var(--secondary-dark-color)",
borderColor: "var(--divider-dark-color)",
},
})
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 },
])
export const basicSetup: Extension = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter({
openText: "▾",
closedText: "▸",
}),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(baseHighlightStyle),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
]),
search({
top: true,
}),
]

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
}