feat: fix outline

This commit is contained in:
Andrew Bastin
2021-09-14 21:30:58 +05:30
parent a07cc7e560
commit 1dee098ca2
6 changed files with 344 additions and 33 deletions

View File

@@ -465,9 +465,9 @@ input[type="checkbox"] {
.CodeMirror {
@apply !h-auto;
&:not(.CodeMirror-focused) .CodeMirror-activeline-background {
background: transparent !important;
}
// &:not(.CodeMirror-focused) .CodeMirror-activeline-background {
// background: transparent !important;
// }
.CodeMirror-dialog-top {
@apply bg-primaryLight;

View File

@@ -13,9 +13,9 @@
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<label class="font-semibold text-secondaryLight">{{
$t("response.body")
}}</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
@@ -44,15 +44,89 @@
</div>
</div>
<div ref="jsonResponse"></div>
<div
v-if="outlinePath"
class="
h-32
bg-primaryLight
border-t border-dividerLight
flex flex-nowrap flex-1
py-2
px-4
bottom-0
z-10
sticky
overflow-auto
hide-scrollbar
"
>
<div v-for="(item, index) in outlinePath" :key="`item-${index}`">
<tippy ref="options" interactive trigger="click" theme="popover" arrow>
<template #trigger>
<ButtonSecondary
v-if="item.kind === 'RootObject'"
:label="item.kind"
/>
<ButtonSecondary
v-if="item.kind === 'RootArray'"
:label="item.kind"
/>
<ButtonSecondary
v-if="item.kind === 'ArrayMember'"
:label="item.index.toString()"
/>
<ButtonSecondary
v-if="item.kind === 'ObjectMember'"
:label="item.name"
/>
</template>
<div
v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
>
<div v-if="item.kind === 'ArrayMember'">
<ButtonSecondary
v-for="(ast, astIndex) in item.astParent.values"
:key="`ast-${astIndex}`"
:label="astIndex.toString()"
@click.native="jumpCursor(ast)"
/>
</div>
<div v-if="item.kind === 'ObjectMember'">
<ButtonSecondary
v-for="(ast, astIndex) in item.astParent.members"
:key="`ast-${astIndex}`"
:label="ast.key.value"
@click.native="jumpCursor(ast)"
/>
</div>
</div>
</tippy>
<i v-if="index + 1 !== outlinePath.length" class="mx-2 material-icons"
>chevron_right</i
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import {
computed,
ref,
watchEffect,
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"
import {
convertIndexToLineCh,
convertLineChToIndex,
} from "~/helpers/editor/utils"
const props = defineProps<{
response: HoppRESTResponse
@@ -90,10 +164,18 @@ const jsonBodyText = computed(() => {
}
})
const ast = computed(() => {
try {
return jsonParse(jsonBodyText.value)
} catch (_: any) {
return null
}
})
const jsonResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
const { cursor } = useCodemirror(
jsonResponse,
jsonBodyText,
reactive({
@@ -107,6 +189,13 @@ useCodemirror(
})
)
const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
console.log(jsonBodyText.value)
console.log(ast.start)
console.log(convertIndexToLineCh(jsonBodyText.value, ast.start))
cursor.value = convertIndexToLineCh(jsonBodyText.value, ast.start)
}
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "application/json" })
@@ -128,6 +217,19 @@ const downloadResponse = () => {
}, 1000)
}
const outlinePath = computed(() => {
if (ast.value) {
return getJSONOutlineAtPos(
ast.value,
convertLineChToIndex(jsonBodyText.value, cursor.value)
)
} else return null
})
watchEffect(() => {
console.log(outlinePath.value)
})
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"

View File

@@ -61,10 +61,11 @@ 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) => {
@@ -135,6 +136,10 @@ export function useCodemirror(
value.value = instance.getValue()
}
})
cm.value.on("cursorActivity", (instance) => {
cursor.value = instance.getCursor()
})
}
// Boot-up CodeMirror, set the value and listeners
@@ -192,10 +197,20 @@ export function useCodemirror(
}
})
// Push cursor updates
watch(cursor, (value) => {
if (value !== cm.value?.getCursor()) {
cm.value?.focus()
console.log(value)
cm.value?.setCursor(value)
}
})
// Watch color mode updates and update theme
watch(() => $colorMode.value, setTheme)
return {
cm,
cursor,
}
}

View File

@@ -2,7 +2,7 @@ export function convertIndexToLineCh(
text: string,
i: number
): { line: number; ch: number } {
const lines = text.split("/n")
const lines = text.split("\n")
let line = 0
let counter = 0
@@ -21,3 +21,18 @@ export function convertIndexToLineCh(
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
}

View File

@@ -19,7 +19,75 @@
* - end: int - the end exclusive offset of the syntax error
*
*/
export default function jsonParse(str) {
type JSONEOFValue = {
kind: "EOF"
start: number
end: number
}
type JSONNullValue = {
kind: "Null"
start: number
end: number
}
type JSONNumberValue = {
kind: "Number"
start: number
end: number
value: number
}
type JSONStringValue = {
kind: "String"
start: number
end: number
value: string
}
type JSONBooleanValue = {
kind: "Boolean"
start: number
end: number
value: boolean
}
type JSONPrimitiveValue =
| JSONNullValue
| JSONEOFValue
| JSONStringValue
| JSONNumberValue
| JSONBooleanValue
export type JSONObjectValue = {
kind: "Object"
start: number
end: number
// eslint-disable-next-line no-use-before-define
members: JSONObjectMember[]
}
export type JSONArrayValue = {
kind: "Array"
start: number
end: number
// eslint-disable-next-line no-use-before-define
values: JSONValue[]
}
export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
export type JSONObjectMember = {
kind: "Member"
start: number
end: number
key: JSONStringValue
value: JSONValue
}
export default function jsonParse(
str: string
): JSONObjectValue | JSONArrayValue {
string = str
strLen = str.length
start = end = lastEnd = -1
@@ -37,15 +105,15 @@ export default function jsonParse(str) {
}
}
let string
let strLen
let start
let end
let lastEnd
let code
let kind
let string: string
let strLen: number
let start: number
let end: number
let lastEnd: number
let code: number
let kind: string
function parseObj() {
function parseObj(): JSONObjectValue {
const nodeStart = start
const members = []
expect("{")
@@ -63,9 +131,9 @@ function parseObj() {
}
}
function parseMember() {
function parseMember(): JSONObjectMember {
const nodeStart = start
const key = kind === "String" ? curToken() : null
const key = kind === "String" ? (curToken() as JSONStringValue) : null
expect("String")
expect(":")
const value = parseVal()
@@ -73,14 +141,14 @@ function parseMember() {
kind: "Member",
start: nodeStart,
end: lastEnd,
key,
key: key!,
value,
}
}
function parseArr() {
function parseArr(): JSONArrayValue {
const nodeStart = start
const values = []
const values: JSONValue[] = []
expect("[")
if (!skip("]")) {
do {
@@ -96,7 +164,7 @@ function parseArr() {
}
}
function parseVal() {
function parseVal(): JSONValue {
switch (kind) {
case "[":
return parseArr()
@@ -111,14 +179,19 @@ function parseVal() {
lex()
return token
}
return expect("Value")
return expect("Value") as never
}
function curToken() {
return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
function curToken(): JSONPrimitiveValue {
return {
kind: kind as any,
start,
end,
value: JSON.parse(string.slice(start, end)),
}
}
function expect(str) {
function expect(str: string) {
if (kind === str) {
lex()
return
@@ -137,11 +210,17 @@ function expect(str) {
throw syntaxError(`Expected ${str} but found ${found}.`)
}
function syntaxError(message) {
type SyntaxError = {
message: string
start: number
end: number
}
function syntaxError(message: string): SyntaxError {
return { message, start, end }
}
function skip(k) {
function skip(k: string) {
if (kind === k) {
lex()
return true
@@ -227,7 +306,7 @@ function lex() {
function readString() {
ch()
while (code !== 34 && code > 31) {
if (code === 92) {
if (code === (92 as any)) {
// \
ch()
switch (code) {
@@ -299,7 +378,7 @@ function readNumber() {
if (code === 69 || code === 101) {
// E e
ch()
if (code === 43 || code === 45) {
if (code === (43 as any) || code === (45 as any)) {
// + -
ch()
}

100
helpers/newOutline.ts Normal file
View File

@@ -0,0 +1,100 @@
import {
JSONArrayValue,
JSONObjectMember,
JSONObjectValue,
JSONValue,
} from "./jsonParse"
type RootEntry =
| {
kind: "RootObject"
astValue: JSONObjectValue
}
| {
kind: "RootArray"
astValue: JSONArrayValue
}
type ObjectMemberEntry = {
kind: "ObjectMember"
name: string
astValue: JSONObjectMember
astParent: JSONObjectValue
}
type ArrayMemberEntry = {
kind: "ArrayMember"
index: number
astValue: JSONValue
astParent: JSONArrayValue
}
type PathEntry = RootEntry | ObjectMemberEntry | ArrayMemberEntry
export function getJSONOutlineAtPos(
jsonRootAst: JSONObjectValue | JSONArrayValue,
posIndex: number
): PathEntry[] | null {
try {
const rootObj = jsonRootAst
if (posIndex > rootObj.end || posIndex < rootObj.start)
throw new Error("Invalid position")
let current: JSONValue = rootObj
const path: PathEntry[] = []
if (rootObj.kind === "Object") {
path.push({
kind: "RootObject",
astValue: rootObj,
})
} else {
path.push({
kind: "RootArray",
astValue: rootObj,
})
}
while (current.kind === "Object" || current.kind === "Array") {
if (current.kind === "Object") {
const next: JSONObjectMember | undefined = current.members.find(
(member) => member.start <= posIndex && member.end >= posIndex
)
if (!next) throw new Error("Couldn't find child")
path.push({
kind: "ObjectMember",
name: next.key.value,
astValue: next,
astParent: current,
})
current = next.value
} else {
const nextIndex = current.values.findIndex(
(value) => value.start <= posIndex && value.end >= posIndex
)
if (nextIndex < 0) throw new Error("Couldn't find child")
const next: JSONValue = current.values[nextIndex]
path.push({
kind: "ArrayMember",
index: nextIndex,
astValue: next,
astParent: current,
})
current = next
}
}
return path
} catch (e: any) {
return null
}
}