Auto merge of #15081 - adenine-dev:master, r=HKalbasi

Feature: Add a memory layout viewer

**Motivation**: rustc by default doesn't enforce a particular memory layout, however it can be useful to see what it is doing under the hood, or if using a particular repr ensure it is behaving how you want it to. This command provides a way to visually explore memory layouts of structures.

**Example**:
this structure:
```rust
struct X {
    x: i32,
    y: u8,
    z: Vec<bool>,
    w: usize,
}
```
produces this output:
<img width="692" alt="image" src="https://github.com/rust-lang/rust-analyzer/assets/22418744/e0312233-18a7-4bb9-ae5b-7b52fcff158a">

**Work yet to be done**:
- tests (see below)
- html is mildly janky (see below)
- enums and unions are viewed flatly, how should they be represented?
- should niches be marked somehow?

This was written for my own use, and the jank is fine for me, but in its current state it is probably not ready to merge mostly because it is missing tests, and also because the code quality is not great. However, before I spend time fixing those things idk if this is even something wanted, if it is I am happy to clean it up, if not that's cool too.
This commit is contained in:
bors 2023-07-09 20:30:20 +00:00
commit 5f11c9a1c3
12 changed files with 848 additions and 15 deletions

View File

@ -9,7 +9,10 @@ use syntax::{
AstToken, SyntaxKind, SyntaxToken, TokenAtOffset, AstToken, SyntaxKind, SyntaxToken, TokenAtOffset,
}; };
use crate::{defs::Definition, generated, RootDatabase}; use crate::{
defs::{Definition, IdentClass},
generated, RootDatabase,
};
pub fn item_name(db: &RootDatabase, item: ItemInNs) -> Option<Name> { pub fn item_name(db: &RootDatabase, item: ItemInNs) -> Option<Name> {
match item { match item {
@ -109,3 +112,16 @@ pub fn is_editable_crate(krate: Crate, db: &RootDatabase) -> bool {
let source_root_id = db.file_source_root(root_file); let source_root_id = db.file_source_root(root_file);
!db.source_root(source_root_id).is_library !db.source_root(source_root_id).is_library
} }
pub fn get_definition(
sema: &Semantics<'_, RootDatabase>,
token: SyntaxToken,
) -> Option<Definition> {
for token in sema.descend_into_macros(token) {
let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions_no_ops);
if let Some(&[x]) = def.as_deref() {
return Some(x);
}
}
None
}

View File

@ -60,6 +60,7 @@ mod interpret_function;
mod view_item_tree; mod view_item_tree;
mod shuffle_crate_graph; mod shuffle_crate_graph;
mod fetch_crates; mod fetch_crates;
mod view_memory_layout;
use std::ffi::OsStr; use std::ffi::OsStr;
@ -74,6 +75,7 @@ use ide_db::{
}; };
use syntax::SourceFile; use syntax::SourceFile;
use triomphe::Arc; use triomphe::Arc;
use view_memory_layout::{view_memory_layout, RecursiveMemoryLayout};
use crate::navigation_target::{ToNav, TryToNav}; use crate::navigation_target::{ToNav, TryToNav};
@ -724,6 +726,13 @@ impl Analysis {
self.with_db(|db| move_item::move_item(db, range, direction)) self.with_db(|db| move_item::move_item(db, range, direction))
} }
pub fn get_recursive_memory_layout(
&self,
position: FilePosition,
) -> Cancellable<Option<RecursiveMemoryLayout>> {
self.with_db(|db| view_memory_layout(db, position))
}
/// Performs an operation on the database that may be canceled. /// Performs an operation on the database that may be canceled.
/// ///
/// rust-analyzer needs to be able to answer semantic questions about the /// rust-analyzer needs to be able to answer semantic questions about the

View File

@ -3,13 +3,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use hir::{db::HirDatabase, Crate, Module, Semantics}; use hir::{db::HirDatabase, Crate, Module};
use ide_db::helpers::get_definition;
use ide_db::{ use ide_db::{
base_db::{FileId, FileRange, SourceDatabaseExt}, base_db::{FileId, FileRange, SourceDatabaseExt},
defs::{Definition, IdentClass}, defs::Definition,
FxHashSet, RootDatabase, FxHashSet, RootDatabase,
}; };
use syntax::{AstNode, SyntaxKind::*, SyntaxToken, TextRange, T}; use syntax::{AstNode, SyntaxKind::*, TextRange, T};
use crate::{ use crate::{
hover::hover_for_definition, hover::hover_for_definition,
@ -214,16 +215,6 @@ impl StaticIndex<'_> {
} }
} }
fn get_definition(sema: &Semantics<'_, RootDatabase>, token: SyntaxToken) -> Option<Definition> {
for token in sema.descend_into_macros(token) {
let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions_no_ops);
if let Some(&[it]) = def.as_deref() {
return Some(it);
}
}
None
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{fixture, StaticIndex}; use crate::{fixture, StaticIndex};

View File

@ -0,0 +1,409 @@
use std::fmt;
use hir::{Field, HirDisplay, Layout, Semantics, Type};
use ide_db::{
defs::Definition,
helpers::{get_definition, pick_best_token},
RootDatabase,
};
use syntax::{AstNode, SyntaxKind};
use crate::FilePosition;
pub struct MemoryLayoutNode {
pub item_name: String,
pub typename: String,
pub size: u64,
pub alignment: u64,
pub offset: u64,
pub parent_idx: i64,
pub children_start: i64,
pub children_len: u64,
}
pub struct RecursiveMemoryLayout {
pub nodes: Vec<MemoryLayoutNode>,
}
// NOTE: this is currently strictly for testing and so isn't super useful as a visualization tool, however it could be adapted to become one?
impl fmt::Display for RecursiveMemoryLayout {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fn process(
fmt: &mut fmt::Formatter<'_>,
nodes: &Vec<MemoryLayoutNode>,
idx: usize,
depth: usize,
) -> fmt::Result {
let mut out = "\t".repeat(depth);
let node = &nodes[idx];
out += &format!(
"{}: {} (size: {}, align: {}, field offset: {})\n",
node.item_name, node.typename, node.size, node.alignment, node.offset
);
write!(fmt, "{}", out)?;
if node.children_start != -1 {
for j in nodes[idx].children_start
..(nodes[idx].children_start + nodes[idx].children_len as i64)
{
process(fmt, nodes, j as usize, depth + 1)?;
}
}
Ok(())
}
process(fmt, &self.nodes, 0, 0)
}
}
enum FieldOrTupleIdx {
Field(Field),
TupleIdx(usize),
}
impl FieldOrTupleIdx {
fn name(&self, db: &RootDatabase) -> String {
match *self {
FieldOrTupleIdx::Field(f) => f
.name(db)
.as_str()
.map(|s| s.to_owned())
.unwrap_or_else(|| format!(".{}", f.name(db).as_tuple_index().unwrap())),
FieldOrTupleIdx::TupleIdx(i) => format!(".{i}").to_owned(),
}
}
fn index(&self) -> usize {
match *self {
FieldOrTupleIdx::Field(f) => f.index(),
FieldOrTupleIdx::TupleIdx(i) => i,
}
}
}
// Feature: View Memory Layout
//
// Displays the recursive memory layout of a datatype.
//
// |===
// | Editor | Action Name
//
// | VS Code | **rust-analyzer: View Memory Layout**
// |===
pub(crate) fn view_memory_layout(
db: &RootDatabase,
position: FilePosition,
) -> Option<RecursiveMemoryLayout> {
let sema = Semantics::new(db);
let file = sema.parse(position.file_id);
let token =
pick_best_token(file.syntax().token_at_offset(position.offset), |kind| match kind {
SyntaxKind::IDENT => 3,
_ => 0,
})?;
let def = get_definition(&sema, token)?;
let ty = match def {
Definition::Adt(it) => it.ty(db),
Definition::TypeAlias(it) => it.ty(db),
Definition::BuiltinType(it) => it.ty(db),
Definition::SelfType(it) => it.self_ty(db),
Definition::Local(it) => it.ty(db),
Definition::Field(it) => it.ty(db),
Definition::Const(it) => it.ty(db),
Definition::Static(it) => it.ty(db),
_ => return None,
};
fn read_layout(
nodes: &mut Vec<MemoryLayoutNode>,
db: &RootDatabase,
ty: &Type,
layout: &Layout,
parent_idx: usize,
) {
let mut fields = ty
.fields(db)
.into_iter()
.map(|(f, ty)| (FieldOrTupleIdx::Field(f), ty))
.chain(
ty.tuple_fields(db)
.into_iter()
.enumerate()
.map(|(i, ty)| (FieldOrTupleIdx::TupleIdx(i), ty)),
)
.collect::<Vec<_>>();
if fields.len() == 0 {
return;
}
fields.sort_by_key(|(f, _)| layout.field_offset(f.index()).unwrap());
let children_start = nodes.len();
nodes[parent_idx].children_start = children_start as i64;
nodes[parent_idx].children_len = fields.len() as u64;
for (field, child_ty) in fields.iter() {
if let Ok(child_layout) = child_ty.layout(db) {
nodes.push(MemoryLayoutNode {
item_name: field.name(db),
typename: child_ty.display(db).to_string(),
size: child_layout.size(),
alignment: child_layout.align(),
offset: layout.field_offset(field.index()).unwrap_or(0),
parent_idx: parent_idx as i64,
children_start: -1,
children_len: 0,
});
} else {
nodes.push(MemoryLayoutNode {
item_name: field.name(db)
+ format!("(no layout data: {:?})", child_ty.layout(db).unwrap_err())
.as_ref(),
typename: child_ty.display(db).to_string(),
size: 0,
offset: 0,
alignment: 0,
parent_idx: parent_idx as i64,
children_start: -1,
children_len: 0,
});
}
}
for (i, (_, child_ty)) in fields.iter().enumerate() {
if let Ok(child_layout) = child_ty.layout(db) {
read_layout(nodes, db, &child_ty, &child_layout, children_start + i);
}
}
}
ty.layout(db)
.map(|layout| {
let item_name = match def {
// def is a datatype
Definition::Adt(_)
| Definition::TypeAlias(_)
| Definition::BuiltinType(_)
| Definition::SelfType(_) => "[ROOT]".to_owned(),
// def is an item
def => def
.name(db)
.map(|n| {
n.as_str()
.map(|s| s.to_owned())
.unwrap_or_else(|| format!(".{}", n.as_tuple_index().unwrap()))
})
.unwrap_or("[ROOT]".to_owned()),
};
let typename = ty.display(db).to_string();
let mut nodes = vec![MemoryLayoutNode {
item_name,
typename: typename.clone(),
size: layout.size(),
offset: 0,
alignment: layout.align(),
parent_idx: -1,
children_start: -1,
children_len: 0,
}];
read_layout(&mut nodes, db, &ty, &layout, 0);
RecursiveMemoryLayout { nodes }
})
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixture;
use expect_test::expect;
fn make_memory_layout(ra_fixture: &str) -> Option<RecursiveMemoryLayout> {
let (analysis, position, _) = fixture::annotations(ra_fixture);
view_memory_layout(&analysis.db, position)
}
#[test]
fn view_memory_layout_none() {
assert!(make_memory_layout(r#"$0"#).is_none());
assert!(make_memory_layout(r#"stru$0ct Blah {}"#).is_none());
}
#[test]
fn view_memory_layout_primitive() {
expect![[r#"
foo: i32 (size: 4, align: 4, field offset: 0)
"#]]
.assert_eq(
&make_memory_layout(
r#"
fn main() {
let foo$0 = 109; // default i32
}
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_constant() {
expect![[r#"
BLAH: bool (size: 1, align: 1, field offset: 0)
"#]]
.assert_eq(
&make_memory_layout(
r#"
const BLAH$0: bool = 0;
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_static() {
expect![[r#"
BLAH: bool (size: 1, align: 1, field offset: 0)
"#]]
.assert_eq(
&make_memory_layout(
r#"
static BLAH$0: bool = 0;
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_tuple() {
expect![[r#"
x: (f64, u8, i64) (size: 24, align: 8, field offset: 0)
.0: f64 (size: 8, align: 8, field offset: 0)
.1: u8 (size: 1, align: 1, field offset: 8)
.2: i64 (size: 8, align: 8, field offset: 16)
"#]]
.assert_eq(
&make_memory_layout(
r#"
fn main() {
let x$0 = (101.0, 111u8, 119i64);
}
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_c_struct() {
expect![[r#"
[ROOT]: Blah (size: 16, align: 4, field offset: 0)
a: u32 (size: 4, align: 4, field offset: 0)
b: (i32, u8) (size: 8, align: 4, field offset: 4)
.0: i32 (size: 4, align: 4, field offset: 0)
.1: u8 (size: 1, align: 1, field offset: 4)
c: i8 (size: 1, align: 1, field offset: 12)
"#]]
.assert_eq(
&make_memory_layout(
r#"
#[repr(C)]
struct Blah$0 {
a: u32,
b: (i32, u8),
c: i8,
}
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_struct() {
expect![[r#"
[ROOT]: Blah (size: 16, align: 4, field offset: 0)
b: (i32, u8) (size: 8, align: 4, field offset: 0)
.0: i32 (size: 4, align: 4, field offset: 0)
.1: u8 (size: 1, align: 1, field offset: 4)
a: u32 (size: 4, align: 4, field offset: 8)
c: i8 (size: 1, align: 1, field offset: 12)
"#]]
.assert_eq(
&make_memory_layout(
r#"
struct Blah$0 {
a: u32,
b: (i32, u8),
c: i8,
}
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_member() {
expect![[r#"
a: bool (size: 1, align: 1, field offset: 0)
"#]]
.assert_eq(
&make_memory_layout(
r#"
#[repr(C)]
struct Oof {
a$0: bool,
}
"#,
)
.unwrap()
.to_string(),
);
}
#[test]
fn view_memory_layout_alias() {
let ml_a = make_memory_layout(
r#"
struct X {
a: u32,
b: i8,
c: (f32, f32),
}
type Foo$0 = X;
"#,
)
.unwrap();
let ml_b = make_memory_layout(
r#"
struct X$0 {
a: u32,
b: i8,
c: (f32, f32),
}
"#,
)
.unwrap();
assert_eq!(ml_a.to_string(), ml_b.to_string());
}
}

View File

@ -1689,6 +1689,34 @@ pub(crate) fn handle_move_item(
} }
} }
pub(crate) fn handle_view_recursive_memory_layout(
snap: GlobalStateSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> anyhow::Result<Option<lsp_ext::RecursiveMemoryLayout>> {
let _p = profile::span("view_recursive_memory_layout");
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
let line_index = snap.file_line_index(file_id)?;
let offset = from_proto::offset(&line_index, params.position)?;
let res = snap.analysis.get_recursive_memory_layout(FilePosition { file_id, offset })?;
Ok(res.map(|it| lsp_ext::RecursiveMemoryLayout {
nodes: it
.nodes
.iter()
.map(|n| lsp_ext::MemoryLayoutNode {
item_name: n.item_name.clone(),
typename: n.typename.clone(),
size: n.size,
offset: n.offset,
alignment: n.alignment,
parent_idx: n.parent_idx,
children_start: n.children_start,
children_len: n.children_len,
})
.collect(),
}))
}
fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink { fn to_command_link(command: lsp_types::Command, tooltip: String) -> lsp_ext::CommandLink {
lsp_ext::CommandLink { tooltip: Some(tooltip), command } lsp_ext::CommandLink { tooltip: Some(tooltip), command }
} }

View File

@ -182,6 +182,33 @@ pub struct ExpandedMacro {
pub expansion: String, pub expansion: String,
} }
pub enum ViewRecursiveMemoryLayout {}
impl Request for ViewRecursiveMemoryLayout {
type Params = lsp_types::TextDocumentPositionParams;
type Result = Option<RecursiveMemoryLayout>;
const METHOD: &'static str = "rust-analyzer/viewRecursiveMemoryLayout";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RecursiveMemoryLayout {
pub nodes: Vec<MemoryLayoutNode>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MemoryLayoutNode {
pub item_name: String,
pub typename: String,
pub size: u64,
pub offset: u64,
pub alignment: u64,
pub parent_idx: i64,
pub children_start: i64,
pub children_len: u64,
}
pub enum CancelFlycheck {} pub enum CancelFlycheck {}
impl Notification for CancelFlycheck { impl Notification for CancelFlycheck {

View File

@ -753,6 +753,7 @@ impl GlobalState {
) )
.on::<lsp_types::request::WillRenameFiles>(handlers::handle_will_rename_files) .on::<lsp_types::request::WillRenameFiles>(handlers::handle_will_rename_files)
.on::<lsp_ext::Ssr>(handlers::handle_ssr) .on::<lsp_ext::Ssr>(handlers::handle_ssr)
.on::<lsp_ext::ViewRecursiveMemoryLayout>(handlers::handle_view_recursive_memory_layout)
.finish(); .finish();
} }

View File

@ -1,5 +1,5 @@
<!--- <!---
lsp_ext.rs hash: 2d60bbffe70ae198 lsp_ext.rs hash: 149a5be3c5e469d1
If you need to change the above hash to make the test pass, please check if you If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue: need to adjust this doc as well and ping this issue:
@ -886,3 +886,48 @@ export interface FetchDependencyListResult {
} }
``` ```
Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree. Returns all crates from this workspace, so it can be used create a viewTree to help navigate the dependency tree.
## View Recursive Memory Layout
**Method:** `rust-analyzer/viewRecursiveMemoryLayout`
**Request:** `TextDocumentPositionParams`
**Response:**
```typescript
export interface RecursiveMemoryLayoutNode = {
/// Name of the item, or [ROOT], `.n` for tuples
item_name: string;
/// Full name of the type (type aliases are ignored)
typename: string;
/// Size of the type in bytes
size: number;
/// Alignment of the type in bytes
alignment: number;
/// Offset of the type relative to its parent (or 0 if its the root)
offset: number;
/// Index of the node's parent (or -1 if its the root)
parent_idx: number;
/// Index of the node's children (or -1 if it does not have children)
children_start: number;
/// Number of child nodes (unspecified it does not have children)
children_len: number;
};
export interface RecursiveMemoryLayout = {
nodes: RecursiveMemoryLayoutNode[];
};
```
Returns a vector of nodes representing items in the datatype as a tree, `RecursiveMemoryLayout::nodes[0]` is the root node.
If `RecursiveMemoryLayout::nodes::length == 0` we could not find a suitable type.
Generic Types do not give anything because they are incomplete. Fully specified generic types do not give anything if they are selected directly but do work when a child of other types [this is consistent with other behavior](https://github.com/rust-lang/rust-analyzer/issues/15010).
### Unresolved questions:
- How should enums/unions be represented? currently they do not produce any children because they have multiple distinct sets of children.
- Should niches be represented? currently they are not reported.
- A visual representation of the memory layout is not specified, see the provided implementation for an example, however it may not translate well to terminal based editors or other such things.

View File

@ -284,6 +284,11 @@
"command": "rust-analyzer.revealDependency", "command": "rust-analyzer.revealDependency",
"title": "Reveal File", "title": "Reveal File",
"category": "rust-analyzer" "category": "rust-analyzer"
},
{
"command": "rust-analyzer.viewMemoryLayout",
"title": "View Memory Layout",
"category": "rust-analyzer"
} }
], ],
"keybindings": [ "keybindings": [
@ -2067,6 +2072,10 @@
{ {
"command": "rust-analyzer.openCargoToml", "command": "rust-analyzer.openCargoToml",
"when": "inRustProject" "when": "inRustProject"
},
{
"command": "rust-analyzer.viewMemoryLayout",
"when": "inRustProject"
} }
], ],
"editor/context": [ "editor/context": [

View File

@ -1129,3 +1129,281 @@ export function linkToCommand(_: Ctx): Cmd {
} }
}; };
} }
export function viewMemoryLayout(ctx: CtxInit): Cmd {
return async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const client = ctx.client;
const position = editor.selection.active;
const expanded = await client.sendRequest(ra.viewRecursiveMemoryLayout, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
position,
});
const document = vscode.window.createWebviewPanel(
"memory_layout",
"[Memory Layout]",
vscode.ViewColumn.Two,
{ enableScripts: true }
);
document.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
min-height: 100%;
height: 100vh;
padding: 32px;
position: relative;
display: block;
background-color: var(--vscode-editor-background);
font-family: var(--vscode-editor-font-family);
font-size: var(--vscode-editor-font-size);
color: var(--vscode-editor-foreground);
}
.container {
position: relative;
}
.trans {
transition: all 0.2s ease-in-out;
}
.grid {
height: 100%;
position: relative;
color: var(--vscode-commandCenter-activeBorder);
pointer-events: none;
}
.grid-line {
position: absolute;
width: 100%;
height: 1px;
background-color: var(--vscode-commandCenter-activeBorder);
}
#tooltip {
position: fixed;
display: none;
z-index: 1;
pointer-events: none;
padding: 4px 8px;
z-index: 2;
color: var(--vscode-editorHoverWidget-foreground);
background-color: var(--vscode-editorHoverWidget-background);
border: 1px solid var(--vscode-editorHoverWidget-border);
}
#tooltip b {
color: var(--vscode-editorInlayHint-typeForeground);
}
#tooltip ul {
margin-left: 0;
padding-left: 20px;
}
table {
position: absolute;
transform: rotateZ(90deg) rotateX(180deg);
transform-origin: top left;
border-collapse: collapse;
table-layout: fixed;
left: 48px;
top: 0;
max-height: calc(100vw - 64px - 48px);
z-index: 1;
}
td {
border: 1px solid var(--vscode-focusBorder);
writing-mode: vertical-rl;
text-orientation: sideways-right;
height: 80px;
}
td p {
height: calc(100% - 16px);
width: calc(100% - 8px);
margin: 8px 4px;
display: inline-block;
transform: rotateY(180deg);
pointer-events: none;
overflow: hidden;
}
td p * {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
height: 100%;
}
td p b {
color: var(--vscode-editorInlayHint-typeForeground);
}
td:hover {
background-color: var(--vscode-editor-hoverHighlightBackground);
}
td:empty {
visibility: hidden;
border: 0;
}
</style>
</head>
<body>
<div id="tooltip"></div>
</body>
<script>(function() {
const data = ${JSON.stringify(expanded)}
if (!(data && data.nodes.length)) {
document.body.innerText = "Not Available"
return
}
data.nodes.map(n => {
n.typename = n.typename.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', ' & quot; ').replaceAll("'", '&#039;')
return n
})
let height = window.innerHeight - 64
addEventListener("resize", e => {
const new_height = window.innerHeight - 64
height = new_height
container.classList.remove("trans")
table.classList.remove("trans")
locate()
setTimeout(() => { // give delay to redraw, annoying but needed
container.classList.add("trans")
table.classList.add("trans")
}, 0)
})
const container = document.createElement("div")
container.classList.add("container")
container.classList.add("trans")
document.body.appendChild(container)
const tooltip = document.getElementById("tooltip")
let y = 0
let zoom = 1.0
const table = document.createElement("table")
table.classList.add("trans")
container.appendChild(table)
const rows = []
function node_t(idx, depth, offset) {
if (!rows[depth]) {
rows[depth] = { el: document.createElement("tr"), offset: 0 }
}
if (rows[depth].offset < offset) {
const pad = document.createElement("td")
pad.colSpan = offset - rows[depth].offset
rows[depth].el.appendChild(pad)
rows[depth].offset += offset - rows[depth].offset
}
const td = document.createElement("td")
td.innerHTML = '<p><span>' + data.nodes[idx].itemName + ':</span> <b>' + data.nodes[idx].typename + '</b></p>'
td.colSpan = data.nodes[idx].size
td.addEventListener("mouseover", e => {
const node = data.nodes[idx]
tooltip.innerHTML = node.itemName + ": <b>" + node.typename + "</b><br/>"
+ "<ul>"
+ "<li>size = " + node.size + "</li>"
+ "<li>align = " + node.alignment + "</li>"
+ "<li>field offset = " + node.offset + "</li>"
+ "</ul>"
+ "<i>double click to focus</i>"
tooltip.style.display = "block"
})
td.addEventListener("mouseleave", _ => tooltip.style.display = "none")
const total_offset = rows[depth].offset
td.addEventListener("dblclick", e => {
const node = data.nodes[idx]
zoom = data.nodes[0].size / node.size
y = -(total_offset) / data.nodes[0].size * zoom
x = 0
locate()
})
rows[depth].el.appendChild(td)
rows[depth].offset += data.nodes[idx].size
if (data.nodes[idx].childrenStart != -1) {
for (let i = 0; i < data.nodes[idx].childrenLen; i++) {
if (data.nodes[data.nodes[idx].childrenStart + i].size) {
node_t(data.nodes[idx].childrenStart + i, depth + 1, offset + data.nodes[data.nodes[idx].childrenStart + i].offset)
}
}
}
}
node_t(0, 0, 0)
for (const row of rows) table.appendChild(row.el)
const grid = document.createElement("div")
grid.classList.add("grid")
container.appendChild(grid)
for (let i = 0; i < data.nodes[0].size / 8 + 1; i++) {
const el = document.createElement("div")
el.classList.add("grid-line")
el.style.top = (i / (data.nodes[0].size / 8) * 100) + "%"
el.innerText = i * 8
grid.appendChild(el)
}
addEventListener("mousemove", e => {
tooltip.style.top = e.clientY + 10 + "px"
tooltip.style.left = e.clientX + 10 + "px"
})
function locate() {
container.style.top = height * y + "px"
container.style.height = (height * zoom) + "px"
table.style.width = container.style.height
}
locate()
})()
</script>
</html>`;
ctx.pushExtCleanup(document);
};
}

View File

@ -150,6 +150,11 @@ export const serverStatus = new lc.NotificationType<ServerStatusParams>(
"experimental/serverStatus" "experimental/serverStatus"
); );
export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>("experimental/ssr"); export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>("experimental/ssr");
export const viewRecursiveMemoryLayout = new lc.RequestType<
lc.TextDocumentPositionParams,
RecursiveMemoryLayout | null,
void
>("rust-analyzer/viewRecursiveMemoryLayout");
export type JoinLinesParams = { export type JoinLinesParams = {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;
@ -197,3 +202,17 @@ export type SsrParams = {
position: lc.Position; position: lc.Position;
selections: readonly lc.Range[]; selections: readonly lc.Range[];
}; };
export type RecursiveMemoryLayoutNode = {
item_name: string;
typename: string;
size: number;
alignment: number;
offset: number;
parent_idx: number;
children_start: number;
children_len: number;
};
export type RecursiveMemoryLayout = {
nodes: RecursiveMemoryLayoutNode[];
};

View File

@ -179,6 +179,7 @@ function createCommands(): Record<string, CommandFactory> {
runFlycheck: { enabled: commands.runFlycheck }, runFlycheck: { enabled: commands.runFlycheck },
ssr: { enabled: commands.ssr }, ssr: { enabled: commands.ssr },
serverVersion: { enabled: commands.serverVersion }, serverVersion: { enabled: commands.serverVersion },
viewMemoryLayout: { enabled: commands.viewMemoryLayout },
// Internal commands which are invoked by the server. // Internal commands which are invoked by the server.
applyActionGroup: { enabled: commands.applyActionGroup }, applyActionGroup: { enabled: commands.applyActionGroup },
applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand }, applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand },