mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(playground): visualize symbol (#1886)
close: https://github.com/oxc-project/oxc/issues/1048
This commit is contained in:
parent
45156443ed
commit
f1b433b126
15 changed files with 189 additions and 52 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -21,3 +21,7 @@ doctest = false
|
|||
[dependencies]
|
||||
index_vec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["index_vec/serde"]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -32,4 +32,4 @@ phf = { workspace = true, features = ["macros"] }
|
|||
|
||||
[features]
|
||||
default = []
|
||||
serde = ["dep:serde"]
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
32
website/playground/editor.js
Normal file
32
website/playground/editor.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
|
|||
50
website/playground/symbols.js
Normal file
50
website/playground/symbols.js
Normal 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)]
|
||||
}
|
||||
Loading…
Reference in a new issue