diff --git a/editors/code/.prettierrc.js b/editors/code/.prettierrc.js index cafb12f0e6dd..b38dc40bdb73 100644 --- a/editors/code/.prettierrc.js +++ b/editors/code/.prettierrc.js @@ -2,4 +2,5 @@ module.exports = { // use 100 because it's Rustfmt's default // https://rust-lang.github.io/rustfmt/?version=v1.4.38&search=#max_width printWidth: 100, + tabWidth: 4, }; diff --git a/editors/code/.vscodeignore b/editors/code/.vscodeignore index 09dc27056b37..05954c4fd385 100644 --- a/editors/code/.vscodeignore +++ b/editors/code/.vscodeignore @@ -7,6 +7,7 @@ !node_modules/d3-graphviz/build/d3-graphviz.min.js !node_modules/d3/dist/d3.min.js !out/main.js +!out/webview/ !package-lock.json !package.json !ra_syntax_tree.tmGrammar.json diff --git a/editors/code/README.md b/editors/code/README.md index 36ab98188220..eeb6b65653ee 100644 --- a/editors/code/README.md +++ b/editors/code/README.md @@ -5,15 +5,15 @@ It is recommended over and replaces `rust-lang.rust`. ## Features -- [code completion] with [imports insertion] -- go to [definition], [implementation], [type definition] -- [find all references], [workspace symbol search], [symbol renaming] -- [types and documentation on hover] -- [inlay hints] for types and parameter names -- [semantic syntax highlighting] -- a lot of [assists (code actions)] -- apply suggestions from errors -- ... and many more, check out the [manual] to see them all +- [code completion] with [imports insertion] +- go to [definition], [implementation], [type definition] +- [find all references], [workspace symbol search], [symbol renaming] +- [types and documentation on hover] +- [inlay hints] for types and parameter names +- [semantic syntax highlighting] +- a lot of [assists (code actions)] +- apply suggestions from errors +- ... and many more, check out the [manual] to see them all [code completion]: https://rust-analyzer.github.io/manual.html#magic-completions [imports insertion]: https://rust-analyzer.github.io/manual.html#completion-with-autoimport diff --git a/editors/code/build.mjs b/editors/code/build.mjs new file mode 100644 index 000000000000..0c4133cfe437 --- /dev/null +++ b/editors/code/build.mjs @@ -0,0 +1,94 @@ +import * as path from "node:path"; +import { parseArgs } from "node:util"; +import * as esbuild from "esbuild"; + +function parseCliOptions() { + const { values } = parseArgs({ + options: { + minify: { + type: "boolean", + default: false, + }, + sourcemap: { + type: "boolean", + default: false, + }, + watch: { + type: "boolean", + default: false, + }, + }, + strict: true, + }); + return { + shouldMinify: !!values.minify, + shouldEmitSourceMap: !!values.sourcemap, + isWatchMode: !!values.watch, + }; +} + +const { shouldMinify, shouldEmitSourceMap, isWatchMode } = parseCliOptions(); + +const OUT_DIR = "./out"; +const OUT_WEBVIEW_DIR = path.resolve(OUT_DIR, "webview"); + +/** @type {esbuild.BuildOptions} */ +const BASE_OPTIONS = { + minify: shouldMinify, + sourcemap: shouldEmitSourceMap ? "external" : false, + bundle: true, +}; + +function createBuildOption(entryPoints) { + /** @type {esbuild.BuildOptions} */ + const options = { + ...BASE_OPTIONS, + entryPoints, + external: ["vscode"], + format: "cjs", + platform: "node", + target: "node16", + outdir: OUT_DIR, + }; + return options; +} + +function createBuildOptionForWebView(entryPoints) { + /** @type {esbuild.BuildOptions} */ + const options = { + ...BASE_OPTIONS, + entryPoints, + format: "esm", + platform: "browser", + // VSCode v1.78 (Electron 22) uses Chromium 108. + // https://code.visualstudio.com/updates/v1_78 + target: "chrome108", + outdir: OUT_WEBVIEW_DIR, + }; + return options; +} + +async function bundleSource(options) { + if (!isWatchMode) { + return esbuild.build(options); + } + + const ctx = await esbuild.context(options); + return ctx.watch(); +} + +await Promise.all([ + bundleSource(createBuildOption(["src/main.ts"])), + bundleSource( + createBuildOptionForWebView([ + "src/webview/show_crate_graph.ts", + "src/webview/show_crate_graph.css", + ]), + ), + bundleSource( + createBuildOptionForWebView([ + "src/webview/view_memory_layout.ts", + "src/webview/view_memory_layout.css", + ]), + ), +]); diff --git a/editors/code/package.json b/editors/code/package.json index 76d7e91f3810..637fe3246ce5 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -32,7 +32,7 @@ "scripts": { "vscode:prepublish": "npm run build-base -- --minify", "package": "vsce package -o rust-analyzer.vsix", - "build-base": "esbuild ./src/main.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node --target=node16", + "build-base": "node ./build.mjs", "build": "npm run build-base -- --sourcemap", "watch": "npm run build-base -- --sourcemap --watch", "format": "prettier --write .", diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index e21f536f26aa..b78a8b916c6d 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -21,6 +21,7 @@ import type { LanguageClient } from "vscode-languageclient/node"; import { LINKED_COMMANDS } from "./client"; import type { DependencyId } from "./dependencies_provider"; import { unwrapUndefinable } from "./undefinable"; +import { getNodeModulePath, getWebViewModulePath } from "./uri"; export * from "./ast_inspector"; export * from "./run"; @@ -736,7 +737,8 @@ export function viewItemTree(ctx: CtxInit): Cmd { function crateGraph(ctx: CtxInit, full: boolean): Cmd { return async () => { - const nodeModulesPath = vscode.Uri.file(path.join(ctx.extensionPath, "node_modules")); + const nodeModulesPath = getNodeModulePath(ctx); + const webviewModulePath = getWebViewModulePath(ctx); const panel = vscode.window.createWebviewPanel( "rust-analyzer.crate-graph", @@ -745,7 +747,7 @@ function crateGraph(ctx: CtxInit, full: boolean): Cmd { { enableScripts: true, retainContextWhenHidden: true, - localResourceRoots: [nodeModulesPath], + localResourceRoots: [nodeModulesPath, webviewModulePath], }, ); const params = { @@ -753,47 +755,25 @@ function crateGraph(ctx: CtxInit, full: boolean): Cmd { }; const client = ctx.client; const dot = await client.sendRequest(ra.viewCrateGraph, params); - const uri = panel.webview.asWebviewUri(nodeModulesPath); + + const nodeModuleUri = panel.webview.asWebviewUri(nodeModulesPath); + const webviewModuleUri = panel.webview.asWebviewUri(webviewModulePath); const html = ` - + - - - + + +
- `; @@ -1142,265 +1122,32 @@ export function viewMemoryLayout(ctx: CtxInit): Cmd { position, }); + const webviewModulePath = getWebViewModulePath(ctx); + const document = vscode.window.createWebviewPanel( "memory_layout", "[Memory Layout]", vscode.ViewColumn.Two, - { enableScripts: true }, + { enableScripts: true, localResourceRoots: [webviewModulePath] }, ); + const uri = document.webview.asWebviewUri(webviewModulePath); + document.webview.html = ` - - Document - +
- `; diff --git a/editors/code/src/uri.ts b/editors/code/src/uri.ts new file mode 100644 index 000000000000..0cd6bea1fc6e --- /dev/null +++ b/editors/code/src/uri.ts @@ -0,0 +1,16 @@ +import * as path from "node:path"; +import { Uri } from "vscode"; +import type { CtxInit } from "./ctx"; + +function getBundledAssetsUri(ctx: CtxInit, pathname: string): Uri { + const resolved = path.join(ctx.extensionPath, pathname); + return Uri.file(resolved); +} + +export function getWebViewModulePath(ctx: CtxInit) { + return getBundledAssetsUri(ctx, "out/webview"); +} + +export function getNodeModulePath(ctx: CtxInit) { + return getBundledAssetsUri(ctx, "node_modules"); +} diff --git a/editors/code/src/webview/show_crate_graph.css b/editors/code/src/webview/show_crate_graph.css new file mode 100644 index 000000000000..14a06d3ec1c9 --- /dev/null +++ b/editors/code/src/webview/show_crate_graph.css @@ -0,0 +1,27 @@ +/* Fill the entire view */ +html, +body { + margin: 0; + padding: 0; + overflow: hidden; +} +svg { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; +} + +/* Disable the graphviz background and fill the polygons */ +.graph > polygon { + display: none; +} +:is(.node, .edge) polygon { + fill: white; +} + +/* Invert the line colours for dark themes */ +body:not(.vscode-light) .edge path { + stroke: white; +} diff --git a/editors/code/src/webview/show_crate_graph.ts b/editors/code/src/webview/show_crate_graph.ts new file mode 100644 index 000000000000..a05b1dac4695 --- /dev/null +++ b/editors/code/src/webview/show_crate_graph.ts @@ -0,0 +1,19 @@ +// @ts-nocheck +export function showCrateDependencyGraph(dot): void { + const graph = d3 + .select("#graph") + .graphviz({ useWorker: false, useSharedWorker: false }) + .fit(true) + .zoomScaleExtent([0.1, Infinity]) + .renderDot(dot); + + d3.select(window).on("click", (event) => { + if (event.ctrlKey) { + graph.resetZoom(d3.transition().duration(100)); + } + }); + d3.select(window).on("copy", (event) => { + event.clipboardData.setData("text/plain", dot); + event.preventDefault(); + }); +} diff --git a/editors/code/src/webview/view_memory_layout.css b/editors/code/src/webview/view_memory_layout.css new file mode 100644 index 000000000000..a5df65dee252 --- /dev/null +++ b/editors/code/src/webview/view_memory_layout.css @@ -0,0 +1,113 @@ +* { + 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; +} diff --git a/editors/code/src/webview/view_memory_layout.ts b/editors/code/src/webview/view_memory_layout.ts new file mode 100644 index 000000000000..60f5f4630c0e --- /dev/null +++ b/editors/code/src/webview/view_memory_layout.ts @@ -0,0 +1,149 @@ +// @ts-nocheck + +export function showMemoryLayout(data): void { + if (!(data && data.nodes.length)) { + document.body.innerText = "Not Available"; + return; + } + + data.nodes.map((n) => { + n.typename = n.typename + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', " & quot; ") + .replaceAll("'", "'"); + return n; + }); + + let height = window.innerHeight - 64; + + window.addEventListener("resize", (e) => { + const newHeight = window.innerHeight - 64; + height = newHeight; + 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 nodeT(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 = + "

" + + data.nodes[idx].itemName + + ": " + + data.nodes[idx].typename + + "

"; + + td.colSpan = data.nodes[idx].size; + + td.addEventListener("mouseover", (e) => { + const node = data.nodes[idx]; + tooltip.innerHTML = + node.itemName + + ": " + + node.typename + + "
" + + "" + + "double click to focus"; + + tooltip.style.display = "block"; + }); + td.addEventListener("mouseleave", (_) => (tooltip.style.display = "none")); + + const totalOffset = rows[depth].offset; + td.addEventListener("dblclick", (e) => { + const node = data.nodes[idx]; + zoom = data.nodes[0].size / node.size; + y = (-totalOffset / 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) { + nodeT( + data.nodes[idx].childrenStart + i, + depth + 1, + offset + data.nodes[data.nodes[idx].childrenStart + i].offset, + ); + } + } + } + } + + nodeT(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); + } + + window.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(); +} diff --git a/editors/code/tsconfig.eslint.json b/editors/code/tsconfig.eslint.json index 5e2b33ca39f8..21f38c40c853 100644 --- a/editors/code/tsconfig.eslint.json +++ b/editors/code/tsconfig.eslint.json @@ -6,6 +6,7 @@ "src", "tests", // these are the eslint-only inclusions - ".eslintrc.js" + ".eslintrc.js", + "./build.mjs" ] } diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json index ee353c28dd67..f06a9d9111cc 100644 --- a/editors/code/tsconfig.json +++ b/editors/code/tsconfig.json @@ -6,7 +6,6 @@ "moduleResolution": "node16", "target": "es2021", "outDir": "out", - "lib": ["es2021"], "sourceMap": true, "rootDir": ".", "newLine": "LF",