Skip to content

Commit

Permalink
[explorer] Implement go-to-definition within modules (MystenLabs#3930)
Browse files Browse the repository at this point in the history
* Implement go-to-definition within explorer

* Implement same-module go-to-definition

* Add utils

* lint + format
  • Loading branch information
Jordan-Mysten authored Aug 12, 2022
1 parent c5cb2e6 commit 340f8a4
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 20 deletions.
138 changes: 129 additions & 9 deletions explorer/client/src/components/module/ModuleView.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,105 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { useQuery } from '@tanstack/react-query';
import cl from 'classnames';
import Highlight, { defaultProps, Prism } from 'prism-react-renderer';
import 'prism-themes/themes/prism-one-light.css';
import { useContext } from 'react';
import { Link } from 'react-router-dom';

import { NetworkContext } from '../../context';
import codestyle from '../../styles/bytecode.module.css';
import { DefaultRpcClient as rpc } from '../../utils/api/DefaultRpcClient';
import { normalizeSuiAddress } from './util';

import type { SuiMoveNormalizedType } from '@mysten/sui.js';
import type { Language } from 'prism-react-renderer';

import styles from './ModuleView.module.css';

// inclue Rust language.
// @ts-ignore
(typeof global !== 'undefined' ? global : window).Prism = Prism;
require('prismjs/components/prism-rust');

function ModuleView({ itm }: { itm: any }) {
interface Props {
id?: string;
name: string;
code: string;
}

interface TypeReference {
address: string;
module: string;
name: string;
type_arguments: SuiMoveNormalizedType[];
}

/** Takes a normalized move type and returns the address information contained within it */
function unwrapTypeReference(
type: SuiMoveNormalizedType
): null | TypeReference {
if (typeof type === 'object') {
if ('Struct' in type) {
return type.Struct;
}
if ('Reference' in type) {
return unwrapTypeReference(type.Reference);
}
if ('MutableReference' in type) {
return unwrapTypeReference(type.MutableReference);
}
if ('Vector' in type) {
return unwrapTypeReference(type.Vector);
}
}
return null;
}

function ModuleView({ id, name, code }: Props) {
const [network] = useContext(NetworkContext);
const { data: normalizedModuleReferences } = useQuery(
['normalized-module', id, name],
async () => {
const normalizedModule = await rpc(network).getNormalizedMoveModule(
id!,
name
);

const typeReferences: Record<string, TypeReference> = {};

Object.values(normalizedModule.exposed_functions).forEach(
(exposedFunction) => {
exposedFunction.parameters.forEach((param) => {
const unwrappedType = unwrapTypeReference(param);
if (!unwrappedType) return;
typeReferences[unwrappedType.name] = unwrappedType;

unwrappedType.type_arguments.forEach((typeArg) => {
const unwrappedTypeArg =
unwrapTypeReference(typeArg);
if (!unwrappedTypeArg) return;
typeReferences[unwrappedTypeArg.name] =
unwrappedTypeArg;
});
});
}
);

return typeReferences;
},
{
enabled: !!id,
}
);

return (
<section className={styles.modulewrapper}>
<div className={styles.moduletitle}>{itm[0]}</div>
<div className={styles.moduletitle}>{name}</div>
<div className={cl(codestyle.code, styles.codeview)}>
<Highlight
{...defaultProps}
code={itm[1]}
code={code}
language={'rust' as Language}
theme={undefined}
>
Expand All @@ -42,12 +120,54 @@ function ModuleView({ itm }: { itm: any }) {
<div className={styles.codelinenumbers}>
{i + 1}
</div>
{line.map((token, key) => (
<span
{...getTokenProps({ token, key })}
key={key}
/>
))}

{line.map((token, key) => {
const reference =
normalizedModuleReferences?.[
token.content
];

if (
(token.types.includes(
'class-name'
) ||
token.types.includes(
'constant'
)) &&
reference
) {
const href = `/objects/${reference.address}?module=${reference.module}`;

return (
<Link
key={key}
{...getTokenProps({
token,
key,
})}
to={href}
target={
normalizeSuiAddress(
reference.address
) ===
normalizeSuiAddress(id!)
? undefined
: '_blank'
}
/>
);
}

return (
<span
{...getTokenProps({
token,
key,
})}
key={key}
/>
);
})}
</div>
))}
</pre>
Expand Down
34 changes: 24 additions & 10 deletions explorer/client/src/components/module/ModulesWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useMemo, useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

import Pagination from '../../components/pagination/Pagination';
import ModuleView from './ModuleView';
Expand All @@ -10,20 +11,33 @@ import styles from './ModuleView.module.css';

type Modules = {
title: string;
content: any[];
content: [moduleName: string, code: string][];
};

interface Props {
id?: string;
data: Modules;
}

const MODULES_PER_PAGE = 3;
// TODO: Include Pagination for now use viewMore and viewLess
function ModuleViewWrapper({ data }: { data: Modules }) {
const moduleData = useMemo(() => data, [data]);
function ModuleViewWrapper({ id, data }: Props) {
const [searchParams] = useSearchParams();
const [modulesPageNumber, setModulesPageNumber] = useState(1);
const totalModulesCount = moduleData.content.length;
const totalModulesCount = data.content.length;
const numOfMudulesToShow = MODULES_PER_PAGE;

useEffect(() => {
setModulesPageNumber(modulesPageNumber);
}, [modulesPageNumber]);
if (searchParams.get('module')) {
const moduleIndex = data.content.findIndex(([moduleName]) => {
return moduleName === searchParams.get('module');
});

setModulesPageNumber(
Math.floor(moduleIndex / MODULES_PER_PAGE) + 1
);
}
}, [searchParams, data.content]);

const stats = {
stats_text: 'total modules',
Expand All @@ -34,15 +48,15 @@ function ModuleViewWrapper({ data }: { data: Modules }) {
<div className={styles.modulewraper}>
<h3 className={styles.title}>{data.title}</h3>
<div className={styles.module}>
{moduleData.content
{data.content
.filter(
(_, index) =>
index >=
(modulesPageNumber - 1) * numOfMudulesToShow &&
index < modulesPageNumber * numOfMudulesToShow
)
.map((item, idx) => (
<ModuleView itm={item} key={idx} />
.map(([name, code], idx) => (
<ModuleView key={idx} id={id} name={name} code={code} />
))}
</div>
{totalModulesCount > numOfMudulesToShow && (
Expand Down
27 changes: 27 additions & 0 deletions explorer/client/src/components/module/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { SuiAddress } from '@mysten/sui.js';

export const SUI_ADDRESS_LENGTH = 20;

// TODO: Use version of this function from the SDK when it is exposed.
export function normalizeSuiAddress(
value: string,
forceAdd0x: boolean = false
): SuiAddress {
let address = value.toLowerCase();
if (!forceAdd0x && address.startsWith('0x')) {
address = address.slice(2);
}
const numMissingZeros =
(SUI_ADDRESS_LENGTH - getHexByteLength(address)) * 2;
if (numMissingZeros <= 0) {
return '0x' + address;
}
return '0x' + '0'.repeat(numMissingZeros) + address;
}

function getHexByteLength(value: string): number {
return /^(0x|0X)/.test(value) ? (value.length - 2) / 2 : value.length / 2;
}
1 change: 1 addition & 0 deletions explorer/client/src/pages/object-result/views/PkgView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function PkgView({ data }: { data: DataType }) {
</table>
</Tabs>
<ModulesWrapper
id={data.id}
data={{
title: 'Modules',
content: properties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,10 @@ function TransactionView({ txdata }: { txdata: DataType }) {
styles.txgridcolspan3,
])}
>
<ModulesWrapper data={modules} />
<ModulesWrapper
id={txKindData.objectId?.value}
data={modules}
/>
</section>
)}
</div>
Expand Down

0 comments on commit 340f8a4

Please sign in to comment.