Skip to content
This repository was archived by the owner on Jan 6, 2024. It is now read-only.

feat: implement state graph #181

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions packages/client/components/DrawerRight.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { vResizable } from 'vue-resizables'
import 'vue-resizables/style'

const props = defineProps<{
modelValue?: boolean
navbar?: HTMLElement
@@ -39,9 +42,22 @@ export default {
<div
v-if="modelValue"
ref="el"
v-resizable="{
edge: {
left: true,
},
border: {
render: true,
style: {
headless: true,
color: '#41B883',
size: 1,
},
},
}"
border="l base"
flex="~ col gap-1"
absolute bottom-0 right-0 z-10 z-20 of-auto text-sm glass-effect
flex="~ col gap-1" absolute bottom-0 right-0 z-10 z-20 of-auto text-sm
glass-effect
:style="{ top: `${top}px` }"
v-bind="$attrs"
>
52 changes: 52 additions & 0 deletions packages/client/components/GraphDock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="tsx">
import type { FunctionalComponent } from 'vue'
import { StateGraphStateEnum, currentFullCodeAndFilename, stateGraphState } from '../logic/state-graph'

const errorBoundaryMessages: Partial<Record<StateGraphStateEnum, string>> = {
[StateGraphStateEnum.NOT_READY]: 'Is not ready yet.',
[StateGraphStateEnum.NOT_SELECT_FILE]: 'You have not selected a file yet.',
[StateGraphStateEnum.NOT_COLLECTED]: 'This file has not been collected or does\'t support.',
}

const ErrorBoundary: FunctionalComponent<{
state: StateGraphStateEnum
}> = ({ state }) =>
<div class="h-full w-full flex items-center justify-center">
<div class="rounded-lg bg-red-4 p2 text-white">
{errorBoundaryMessages[state]}
</div>
</div>

const tabs = [
{
name: 'StateGraph',
icon: 'i-carbon-data-vis-1',
},
{
name: 'Code',
icon: 'i-carbon-code',
},
] as const

const activeTab = ref<(typeof tabs)[number]['name']>(tabs[0].name)
</script>

<template>
<ErrorBoundary v-if="errorBoundaryMessages[stateGraphState]" :state="stateGraphState" />
<div v-else class="grid grid-rows-[30px_1fr] h-full w-full p2">
<nav class="h30px w-full flex items-center gap1">
<div v-for="item of tabs" :key="item.name" class="cursor-pointer p1" :class="[activeTab === item.name ? 'text-primary' : '']" @click="activeTab = item.name">
<div :class="[item.icon]" />
</div>
</nav>
<StateGraph v-if="activeTab === 'StateGraph'" />
<Suspense v-else-if="activeTab === 'Code'">
<HighlightCode class="h-full overflow-hidden" v-bind="currentFullCodeAndFilename" />
<template #fallback>
<div class="h-full w-full flex items-center justify-center">
<VDLoading class="h-30px w-30px" />
</div>
</template>
</Suspense>
</div>
</template>
32 changes: 32 additions & 0 deletions packages/client/components/HighlightCode.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script setup lang="ts">
defineProps<{
code: string
filename: string
}>()

const { highlightedCode } = await useHighlight()
</script>

<template>
<div class="overflow-x-auto overflow-y-auto">
<div v-html="highlightedCode(code, filename.split('.').at(-1)!)" />
</div>
</template>

<style scoped>
/* copied from https://github.com/shikijs/shiki/issues/3 */
:deep(code) {
counter-reset: step;
counter-increment: step 0;
}

:deep(code) .line::before {
content: counter(step);
counter-increment: step;
width: 1rem;
margin-right: 1.5rem;
display: inline-block;
text-align: right;
@apply text-gray-400;
}
</style>
4 changes: 2 additions & 2 deletions packages/client/components/SearchBox.vue
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ const { graphSettings } = useGraphSettings()
</script>

<template>
<nav flex="~ justify-between items-center" border="b base" pl-4 pr-6 font-light children:my-auto>
<nav flex="~ justify-between items-center" border="b base" z-200 pl-4 pr-6 font-light children:my-auto bg-base>
<div flex="~ gap-4 items-center" h-54px>
<div i-carbon-ibm-watson-discovery title="Vite Inspect" text-xl />
<input
@@ -35,7 +35,7 @@ const { graphSettings } = useGraphSettings()
</template>
</div>

<div>
<div class="flex items-center justify-center gap-2">
<slot name="right" />
</div>
</nav>
108 changes: 108 additions & 0 deletions packages/client/components/StateGraph.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
import type { ScanVariableDeclarationResult } from 'esm-analyzer'
import { Network } from 'vis-network'
import type { Node, Options } from 'vis-network'
import { currentSelectedFile } from '../logic/state-graph'
import { useDevToolsClient } from '~/logic/client'
import { stateGraphRawData } from '~/logic/state-graph'

const isHoveringNode = ref(false)
const container = ref<HTMLDivElement>()

const map = ref<Map</* variable name */string, ScanVariableDeclarationResult>>(new Map())
const nodes = ref<Node[]>([])

const { meta: metaKeyPressed } = useMagicKeys({
passive: true,
})

watchEffect(() => {
nodes.value = []
if (!stateGraphRawData.value) {
nodes.value = []
return map.value.clear()
}
nodes.value = []
map.value.clear()
for (const [variable] of stateGraphRawData.value) {
map.value.set(variable.name, variable)
nodes.value.push({
id: variable.name,
title: variable.name,
label: variable.name,
size: 15,
})
}
})

onMounted(() => {
const options: Options = {
nodes: {
shape: 'dot',
size: 16,
},
interaction: {
hover: true,
},
physics: {
maxVelocity: 146,
solver: 'forceAtlas2Based',
timestep: 0.35,
stabilization: {
enabled: true,
iterations: 200,
},
},
groups: {
vue: {
color: '#42b883',
},
ts: {
color: '#41b1e0',
},
js: {
color: '#d6cb2d',
},
json: {
color: '#cf8f30',
},
css: {
color: '#e6659a',
},
html: {
color: '#e34c26',
},
svelte: {
color: '#ff3e00',
},
jsx: {
color: '#7d6fe8',
},
tsx: {
color: '#7d6fe8',
},
},
}

const network = new Network(container.value!, {
nodes: nodes.value,
}, options)

const client = useDevToolsClient()

network.on('click', (params) => {
const nodeId = params.nodes?.[0]
if (metaKeyPressed.value) {
const { loc: { start: { line, column } } } = map.value.get(nodeId!)!
return client.value.openInEditor(currentSelectedFile.value!, line, column)
}
})
watch(nodes, (nodes) => {
network.setData({ nodes })
})
})
</script>

<template>
<div ref="container" flex="1" :class="[isHoveringNode ? 'cursor-pointer' : '']" />
</template>
37 changes: 37 additions & 0 deletions packages/client/composables/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Highlighter } from 'shiki'
import { getHighlighter, setCDN } from 'shiki'

const extToLang = {
vue: 'vue',
js: 'javascript',
ts: 'typescript',
jsx: 'jsx',
tsx: 'tsx',
}

setCDN('https://unpkg.com/shiki/')

let highlighter: Highlighter

async function initHighlighter() {
if (highlighter)
return highlighter
return await getHighlighter({
themes: ['vitesse-dark', 'vitesse-light'],
langs: ['vue', 'javascript', 'typescript', 'jsx', 'tsx'],
}).then(h => highlighter = h)
}

export async function useHighlight() {
const isDark = useDark()
if (!highlighter)
await initHighlighter()
return {
highlightedCode: (code: string, ext: string) => highlighter.codeToHtml(code,
{
lang: extToLang[ext],
theme: isDark.value ? 'vitesse-dark' : 'vitesse-light',
},
),
}
}
101 changes: 101 additions & 0 deletions packages/client/logic/state-graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { AcceptableLang } from 'esm-analyzer'
import { Project } from 'esm-analyzer'
import { rpc } from './rpc'

export enum StateGraphStateEnum {
NOT_READY, // graph is not ready yet
READY, // graph is ready
NOT_SELECT_FILE, // graph is ready, but no file is selected
NOT_COLLECTED, // graph is ready, but the selected file is not collected
NO_STATE, // graph is ready, but the selected file has no state
HAS_STATE, // everything is ready
}

const codeData = ref<{
code: string
filename: string
}[]>([])
const rawAnalyzeData = ref<{
code: string
lang: AcceptableLang
path: string
offsetContent: string
}[]>([])
const project = shallowRef<Project>(new Project('state-analyze'))
export const stateGraphState = ref(StateGraphStateEnum.NOT_READY)

export async function initRawData() {
if (stateGraphState.value !== StateGraphStateEnum.NOT_READY)
return
const rawData = await rpc.getStateAnalyzeCollectedData() ?? []
rawData.forEach((item) => {
rawAnalyzeData.value.push({
code: item.code,
lang: item.lang as AcceptableLang,
path: item.filename,
offsetContent: item.offsetContent,
})
codeData.value.push({
code: item.fullCode,
filename: item.filename,
})
})
project.value.addFiles(rawAnalyzeData.value)
await project.value.prepare()
stateGraphState.value = StateGraphStateEnum.READY
}

export const currentSelectedFile = ref</* file name */string>()
watch(currentSelectedFile, () => {
stateGraphState.value = getState()
})

export function useStateGraph() {
const [drawerVisible, toggleDrawerVisible] = useToggle(false)

return {
drawerVisible,
toggleDrawerVisible,
enable: async () => {
toggleDrawerVisible()
await initRawData()
stateGraphState.value = getState()
},
currentSelectedFile,
}
}

function getState() {
// if not ready
if (stateGraphState.value === StateGraphStateEnum.NOT_READY)
return StateGraphStateEnum.NOT_READY
const selectFile = currentSelectedFile.value
// if not selected file
if (!selectFile)
return StateGraphStateEnum.NOT_SELECT_FILE
// if not collected
if (!project.value.getFilePaths().includes(selectFile))
return StateGraphStateEnum.NOT_COLLECTED
// if no state
const data = project.value.getAnalyzeResults(selectFile)
if (!data || !data.size)
return StateGraphStateEnum.NO_STATE
return StateGraphStateEnum.HAS_STATE
}

function getData() {
return stateGraphState.value === StateGraphStateEnum.HAS_STATE
? project.value.getAnalyzeResults(currentSelectedFile.value!)!
: null
}

export const stateGraphRawData = computed(() => {
return getData()
})

export const currentFullCodeAndFilename = computed(() => {
return codeData.value.find(item => item.filename === currentSelectedFile.value) ?? {
code: '',
filename: '',
}
})
Loading
Oops, something went wrong.