diff --git a/Cargo.lock b/Cargo.lock index 742c7ee2f..a14bfed58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/crates/oxc/Cargo.toml b/crates/oxc/Cargo.toml index 56e79895f..4d76361dd 100644 --- a/crates/oxc/Cargo.toml +++ b/crates/oxc/Cargo.toml @@ -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"] diff --git a/crates/oxc_index/Cargo.toml b/crates/oxc_index/Cargo.toml index 276f1b479..ef3453ec2 100644 --- a/crates/oxc_index/Cargo.toml +++ b/crates/oxc_index/Cargo.toml @@ -21,3 +21,7 @@ doctest = false [dependencies] index_vec = { workspace = true } static_assertions = { workspace = true } + +[features] +default = [] +serde = ["index_vec/serde"] diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index d1c5a6086..cf3232365 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -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"] diff --git a/crates/oxc_semantic/src/reference.rs b/crates/oxc_semantic/src/reference.rs index aa407f9df..38d385f5c 100644 --- a/crates/oxc_semantic/src/reference.rs +++ b/crates/oxc_semantic/src/reference.rs @@ -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 diff --git a/crates/oxc_semantic/src/symbol.rs b/crates/oxc_semantic/src/symbol.rs index 76ae6c650..287147668 100644 --- a/crates/oxc_semantic/src/symbol.rs +++ b/crates/oxc_semantic/src/symbol.rs @@ -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, pub names: IndexVec, diff --git a/crates/oxc_syntax/Cargo.toml b/crates/oxc_syntax/Cargo.toml index be06ce758..e8d883f02 100644 --- a/crates/oxc_syntax/Cargo.toml +++ b/crates/oxc_syntax/Cargo.toml @@ -32,4 +32,4 @@ phf = { workspace = true, features = ["macros"] } [features] default = [] -serde = ["dep:serde"] +serde = ["dep:serde", "bitflags/serde"] diff --git a/crates/oxc_syntax/src/reference.rs b/crates/oxc_syntax/src/reference.rs index b53c3692b..ad16137b6 100644 --- a/crates/oxc_syntax/src/reference.rs +++ b/crates/oxc_syntax/src/reference.rs @@ -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; diff --git a/crates/oxc_syntax/src/symbol.rs b/crates/oxc_syntax/src/symbol.rs index 2f6715734..abb1012a3 100644 --- a/crates/oxc_syntax/src/symbol.rs +++ b/crates/oxc_syntax/src/symbol.rs @@ -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 diff --git a/crates/oxc_wasm/src/lib.rs b/crates/oxc_wasm/src/lib.rs index 3e65b2521..a8b6f5ea1 100644 --- a/crates/oxc_wasm/src/lib.rs +++ b/crates/oxc_wasm/src/lib.rs @@ -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); diff --git a/crates/oxc_wasm/src/options.rs b/crates/oxc_wasm/src/options.rs index a2ed074aa..6eb37ceaa 100644 --- a/crates/oxc_wasm/src/options.rs +++ b/crates/oxc_wasm/src/options.rs @@ -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] diff --git a/website/playground/editor.js b/website/playground/editor.js new file mode 100644 index 000000000..333b8aea2 --- /dev/null +++ b/website/playground/editor.js @@ -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; +} \ No newline at end of file diff --git a/website/playground/index.html b/website/playground/index.html index 7da9d1ef7..d3db1c19a 100644 --- a/website/playground/index.html +++ b/website/playground/index.html @@ -66,6 +66,7 @@ + diff --git a/website/playground/index.js b/website/playground/index.js index 1a2a99d2a..6d018da70 100644 --- a/website/playground/index.js +++ b/website/playground/index.js @@ -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"); }; diff --git a/website/playground/symbols.js b/website/playground/symbols.js new file mode 100644 index 000000000..eae626b56 --- /dev/null +++ b/website/playground/symbols.js @@ -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} 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} + */ +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)] +} \ No newline at end of file