feat(playground): visualize symbol (#1886)

close: https://github.com/oxc-project/oxc/issues/1048
This commit is contained in:
Dunqing 2024-01-04 15:36:31 +08:00 committed by GitHub
parent 45156443ed
commit f1b433b126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 189 additions and 52 deletions

7
Cargo.lock generated
View file

@ -169,6 +169,9 @@ name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
dependencies = [
"serde",
]
[[package]]
name = "block-buffer"
@ -975,6 +978,9 @@ name = "index_vec"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74086667896a940438f2118212f313abba4aff3831fef6f4b17d02add5c8bb60"
dependencies = [
"serde",
]
[[package]]
name = "indexmap"
@ -1932,6 +1938,7 @@ dependencies = [
"oxc_syntax",
"phf",
"rustc-hash",
"serde",
]
[[package]]

View file

@ -34,7 +34,7 @@ oxc_minifier = { workspace = true, optional = true }
oxc_codegen = { workspace = true, optional = true }
[features]
serde = ["oxc_ast/serde"]
serde = ["oxc_ast/serde", "oxc_semantic/serde"]
semantic = ["oxc_semantic"]
formatter = ["oxc_formatter"]
transformer = ["oxc_transformer"]

View file

@ -21,3 +21,7 @@ doctest = false
[dependencies]
index_vec = { workspace = true }
static_assertions = { workspace = true }
[features]
default = []
serde = ["index_vec/serde"]

View file

@ -29,8 +29,13 @@ rustc-hash = { workspace = true }
phf = { workspace = true, features = ["macros"] }
indexmap = { workspace = true }
itertools = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
[dev-dependencies]
oxc_parser = { workspace = true }
oxc_allocator = { workspace = true }
miette = { workspace = true, features = ["fancy-no-backtrace"] }
[features]
default = []
serde = ["dep:serde", "oxc_span/serde", "oxc_syntax/serde", "oxc_index/serde"]

View file

@ -1,10 +1,13 @@
use oxc_span::{Atom, Span};
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::{symbol::SymbolId, AstNodeId};
pub use oxc_syntax::reference::{ReferenceFlag, ReferenceId};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
pub struct Reference {
span: Span,
/// The name of the identifier that was referred to

View file

@ -11,10 +11,14 @@ use crate::{
reference::{Reference, ReferenceId},
};
#[cfg(feature = "serde")]
use serde::Serialize;
/// Symbol Table
///
/// `SoA` (Struct of Arrays) for memory efficiency.
#[derive(Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
pub struct SymbolTable {
pub spans: IndexVec<SymbolId, Span>,
pub names: IndexVec<SymbolId, Atom>,

View file

@ -32,4 +32,4 @@ phf = { workspace = true, features = ["macros"] }
[features]
default = []
serde = ["dep:serde"]
serde = ["dep:serde", "bitflags/serde"]

View file

@ -1,5 +1,7 @@
use bitflags::bitflags;
use oxc_index::define_index_type;
#[cfg(feature = "serde")]
use serde::Serialize;
define_index_type! {
pub struct ReferenceId = u32;
@ -7,6 +9,7 @@ define_index_type! {
bitflags! {
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct ReferenceFlag: u8 {
const None = 0;
const Read = 1 << 0;

View file

@ -1,12 +1,16 @@
use bitflags::bitflags;
use oxc_index::define_index_type;
#[cfg(feature = "serde")]
use serde::Serialize;
define_index_type! {
pub struct SymbolId = u32;
}
bitflags! {
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub struct SymbolFlags: u32 {
const None = 0;
/// Variable (var) or parameter

View file

@ -44,6 +44,7 @@ pub struct Oxc {
ast: JsValue,
ir: JsValue,
symbols: JsValue,
scope_text: String,
codegen_text: String,
@ -120,6 +121,11 @@ impl Oxc {
self.scope_text.clone()
}
#[wasm_bindgen(getter = symbols)]
pub fn symbols(&self) -> JsValue {
self.symbols.clone()
}
/// Returns Array of String
/// # Errors
/// # Panics
@ -274,9 +280,13 @@ impl Oxc {
}
}
if run_options.scope() {
if run_options.scope() || run_options.symbol() {
let semantic = SemanticBuilder::new(source_text, source_type).build(program).semantic;
self.scope_text = Self::get_scope_text(&semantic);
if run_options.scope() {
self.scope_text = Self::get_scope_text(&semantic);
} else if run_options.symbol() {
self.symbols = semantic.symbols().serialize(&self.serializer)?;
}
}
let program = allocator.alloc(program);

View file

@ -11,6 +11,7 @@ pub struct OxcRunOptions {
transform: bool,
type_check: bool,
scope: bool,
symbol: bool,
}
#[wasm_bindgen]
@ -99,6 +100,16 @@ impl OxcRunOptions {
pub fn set_scope(&mut self, yes: bool) {
self.scope = yes;
}
#[wasm_bindgen(getter)]
pub fn symbol(self) -> bool {
self.symbol
}
#[wasm_bindgen(setter)]
pub fn set_symbol(&mut self, yes: bool) {
self.symbol = yes;
}
}
#[wasm_bindgen]

View file

@ -0,0 +1,32 @@
// Go down and find the `start` and `end` keys
export function getStartAndEnd(view, cursor) {
let start, end;
while (true) {
if (
!start &&
this.getTextFromView(view, cursor.from, cursor.to) == '"start"'
) {
cursor.next();
start = this.getTextFromView(view, cursor.from, cursor.to);
}
if (
!end &&
this.getTextFromView(view, cursor.from, cursor.to) == '"end"'
) {
cursor.next();
end = this.getTextFromView(view, cursor.from, cursor.to);
}
if (start && end) {
break;
}
if (!cursor.next()) {
break;
}
}
return [start, end]
}
export const convertToUtf8 = (sourceTextUtf8, d) => {
return new TextDecoder().decode(sourceTextUtf8.slice(0, d)).length;
}

View file

@ -66,6 +66,7 @@
<button type="button" id="prettier">Format (Prettier)</button>
<button type="button" id="query">Query Playground</button>
<button type="button" id="scope">Scope</button>
<button type="button" id="symbol">Symbol</button>
<button type="button" id="query-args-or-outputs" class="query-button-red">Show Query Arguments</button>
<button type="button" id="ir-copy">Copy IR to clipboard</button>
</div>

View file

@ -8,6 +8,7 @@ import {
Compartment,
RangeSet,
} from "@codemirror/state";
import { convertToUtf8, getStartAndEnd } from './editor.js'
import { findMostInnerNodeForPosition } from './traverseJson.js'
import { parser } from '@lezer/json'
import { javascript, javascriptLanguage } from "@codemirror/lang-javascript";
@ -37,6 +38,7 @@ import initWasm, {
OxcTypeCheckingOptions,
graphql_schema_text,
} from "@oxc/wasm-web";
import { getSymbolAndReferencesSpan, renderSymbols } from "./symbols.js";
const placeholderText = `
import React, { useEffect, useRef } from 'react'
@ -477,6 +479,7 @@ class Playground {
case "ir":
return "rust";
case "ast":
case "symbol":
return "json";
case "query":
return "graphql";
@ -525,6 +528,12 @@ class Playground {
this.runOptions.scope = false
text = this.oxc.scopeText;
break;
case "symbol":
this.runOptions.symbol = true;
this.run();
this.runOptions.symbol = false
text = renderSymbols(this.oxc.symbols)
break;
case "codegen":
this.run();
text = this.oxc.codegenText;
@ -621,7 +630,9 @@ query {
}
highlightEditorRange(view, range) {
if (range.from === 0 && range.to === 0) {
let ranges = Array.isArray(range) ? range : [range];
ranges = ranges.filter((range) => range.from !== 0 || range.to !== 0);
if (ranges.length === 0) {
return;
}
const addHighlight = StateEffect.define({
@ -647,7 +658,7 @@ query {
},
provide: (f) => EditorView.decorations.from(f),
});
const effects = [addHighlight.of(range)];
const effects = ranges.map((range) => addHighlight.of(range));
if (!view.state.field(highlightField, false)) {
effects.push(
StateEffect.appendConfig.of([highlightField, Playground.highlightTheme])
@ -667,56 +678,44 @@ query {
// Highlight the editor by searching for `start` and `end` values.
highlightEditorFromViewer(e, view) {
if (this.currentLanguage() != "json") {
return;
if (this.currentView === 'symbol') {
const pos = view.posAtCoords(e);
const tree = syntaxTree(view.state);
let cursor = tree.cursorAt(pos);
let [start, end] = getStartAndEnd.call(this, view, cursor)
// if we didn't find a start or an end, return early
if (start == undefined || end == undefined) return;
let ranges = getSymbolAndReferencesSpan(start, end)
this.highlightEditorRange(
this.editor,
ranges.map(range => EditorSelection.range(convertToUtf8(this.sourceTextUtf8, range.start), convertToUtf8(this.sourceTextUtf8, range.end)))
);
}
const pos = view.posAtCoords(e);
const tree = syntaxTree(view.state);
let cursor = tree.cursorAt(pos);
// Go up and find the `type` key
while (true) {
if (view.state.doc.sliceString(cursor.from, cursor.to) == '"type"') {
break;
else if (this.currentView === "ast") {
const pos = view.posAtCoords(e);
const tree = syntaxTree(view.state);
let cursor = tree.cursorAt(pos);
// Go up and find the `type` key
while (true) {
if (view.state.doc.sliceString(cursor.from, cursor.to) == '"type"') {
break;
}
if (!cursor.prev()) {
break;
}
}
if (!cursor.prev()) {
break;
}
}
// Go down and find the `start` and `end` keys
let start, end;
while (true) {
if (
!start &&
this.getTextFromView(view, cursor.from, cursor.to) == '"start"'
) {
cursor.next();
start = this.getTextFromView(view, cursor.from, cursor.to);
}
if (
!end &&
this.getTextFromView(view, cursor.from, cursor.to) == '"end"'
) {
cursor.next();
end = this.getTextFromView(view, cursor.from, cursor.to);
}
if (start && end) {
break;
}
if (!cursor.next()) {
break;
}
}
// if we didn't find a start or an end, return early
if (start == undefined || end == undefined) return;
let [start, end] = getStartAndEnd.call(this, view, cursor)
// if we didn't find a start or an end, return early
if (start == undefined || end == undefined) return;
// convert utf8 to utf16 span so they show correctly in the editor
start = new TextDecoder().decode(this.sourceTextUtf8.slice(0, start)).length;
end = new TextDecoder().decode(this.sourceTextUtf8.slice(0, end)).length;
start = convertToUtf8(this.sourceTextUtf8, start)
end = convertToUtf8(this.sourceTextUtf8, end)
this.highlightEditorRange(
this.editor,
EditorSelection.range(start, end)
);
this.highlightEditorRange(
this.editor,
EditorSelection.range(start, end)
);
}
}
}
@ -803,6 +802,10 @@ async function main() {
playground.updateView("scope");
};
document.getElementById("symbol").onclick = () => {
playground.updateView("symbol");
};
document.getElementById("codegen").onclick = () => {
playground.updateView("codegen");
};

View file

@ -0,0 +1,50 @@
/**
* @typedef {Object} SymbolTable
* @property {Array<{start: number, end: number}>} spans - The spans of the symbols.
* @property {string[]} names - The names of the symbols.
* @property {string[]} flags - The flags of the symbols.
* @property {number[]} scopeIds - The scope IDs of the symbols.
* @property {number[]} declarations - The declarations of the symbols.
* @property {Array<number[]>} resolvedReferences - The resolved references of the symbols.
* @property {Array<{span: {start: number, end: number}, name: string, node_id: number, symbol_id: number|null, flag: string}>} references - The references of the symbols.
*/
/**
* @type {Array<any>}
*/
let cacheSymbols = null
/**
*
* @param {SymbolTable} symbols
* @returns
*/
export const renderSymbols = (symbols) => {
const target = []
symbols.declarations.forEach((nodeId, index) => {
target.push({
name: symbols.names[index],
flag: symbols.flags[index],
symbolId: index,
nodeId,
span: symbols.spans[index],
references: symbols.resolvedReferences[index].map((id) => symbols.references[id]),
})
})
cacheSymbols = target
return JSON.stringify(target, null, 2)
}
export const getSymbolAndReferencesSpan = (start, end) => {
if (!cacheSymbols) {
return [{ start, end }]
}
const symbol = cacheSymbols.find((symbol) => {
return symbol.span.start == start && symbol.span.end == end
})
if (!symbol) {
return [{ start, end }]
}
return [symbol.span, ...symbol.references.map((reference) => reference.span)]
}