diff --git a/.gitignore b/.gitignore index 50d7c7b916..ad98096af7 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,3 @@ testem.log .DS_Store .vscode/settings.json .vscode/launch.json -libs/remix-node/ -libs/remix-niks/ - -apps/remix-react \ No newline at end of file diff --git a/apps/doc-gen/.eslintrc.json b/apps/doc-gen/.eslintrc.json new file mode 100644 index 0000000000..a92d0f887a --- /dev/null +++ b/apps/doc-gen/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "extends": [ + "plugin:@nrwl/nx/react", + "../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/apps/doc-gen/.prettierrc b/apps/doc-gen/.prettierrc new file mode 100644 index 0000000000..6e6d086922 --- /dev/null +++ b/apps/doc-gen/.prettierrc @@ -0,0 +1,13 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "avoid", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "endOfLine": "lf" +} \ No newline at end of file diff --git a/apps/doc-gen/project.json b/apps/doc-gen/project.json new file mode 100644 index 0000000000..51d9d63212 --- /dev/null +++ b/apps/doc-gen/project.json @@ -0,0 +1,59 @@ +{ + "name": "doc-gen", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/doc-gen/src", + "projectType": "application", + "implicitDependencies": [ + ], + "targets": { + "build": { + "executor": "@nrwl/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "development", + "options": { + "compiler": "babel", + "outputPath": "dist/apps/doc-gen", + "index": "apps/doc-gen/src/index.html", + "baseHref": "/", + "main": "apps/doc-gen/src/main.tsx", + "tsConfig": "apps/doc-gen/tsconfig.app.json", + "assets": [ + "apps/doc-gen/src/favicon.ico" + ], + "styles": [], + "scripts": [], + "webpackConfig": "apps/doc-gen/webpack.config.js" + }, + "configurations": { + "development": { + }, + "production": { + "fileReplacements": [ + { + "replace": "apps/doc-gen/src/environments/environment.ts", + "with": "apps/doc-gen/src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "executor": "@nrwl/webpack:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "doc-gen:build", + "hmr": true + }, + "configurations": { + "development": { + "buildTarget": "doc-gen:build:development", + "port": 6003 + }, + "production": { + "buildTarget": "doc-gen:build:production" + } + } + } + }, + "tags": [] +} diff --git a/apps/doc-gen/src/app/App.css b/apps/doc-gen/src/app/App.css new file mode 100644 index 0000000000..609253955e --- /dev/null +++ b/apps/doc-gen/src/app/App.css @@ -0,0 +1,3 @@ +body { + margin: 0; +} \ No newline at end of file diff --git a/apps/doc-gen/src/app/App.tsx b/apps/doc-gen/src/app/App.tsx new file mode 100644 index 0000000000..358d6776b6 --- /dev/null +++ b/apps/doc-gen/src/app/App.tsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react' +import { + CompilationResult, +} from '@remixproject/plugin-api/' + +import './App.css' +import { DocGenClient } from './docgen-client' +import { Build } from './docgen/site' + +export const client = new DocGenClient() + +const App = () => { + const [themeType, setThemeType] = useState('dark'); + const [hasBuild, setHasBuild] = useState(false); + const [fileName, setFileName] = useState(''); + + useEffect(() => { + const watchThemeSwitch = async () => { + client.eventEmitter.on('themeChanged', (theme: string) => { + setThemeType(theme) + }) + client.eventEmitter.on('compilationFinished', (build: Build, fileName: string) => { + setHasBuild(true) + setFileName(fileName) + }) + client.eventEmitter.on('docsGenerated', (docs: string[]) => { + console.log('docsGenerated', docs) + }) + } + watchThemeSwitch() + }, []) + + return ( +
+

Remix Docgen

+ {fileName &&

File: {fileName.split('/')[1].split('.')[0].concat('.sol')}

} + {hasBuild && } +
+ ) +} + +export default App diff --git a/apps/doc-gen/src/app/docgen-client.ts b/apps/doc-gen/src/app/docgen-client.ts new file mode 100644 index 0000000000..76d702c6b4 --- /dev/null +++ b/apps/doc-gen/src/app/docgen-client.ts @@ -0,0 +1,80 @@ +import { PluginClient } from '@remixproject/plugin' +import { CompilationResult, SourceWithTarget } from '@remixproject/plugin-api' +import { createClient } from '@remixproject/plugin-webview' +import EventEmitter from 'events' +import { Config, defaults } from './docgen/config' +import { Build, buildSite } from './docgen/site' +import { loadTemplates } from './docgen/templates' +import { SolcInput, SolcOutput } from 'solidity-ast/solc' +import { render } from './docgen/render' + +export class DocGenClient extends PluginClient { + private currentTheme + public eventEmitter: EventEmitter + private build: Build + public docs: string[] = [] + private fileName: string = '' + + constructor() { + super() + this.eventEmitter = new EventEmitter() + this.methods = ['generateDocs', 'opendDocs'] + createClient(this) + this.onload().then(async () => { + await this.setListeners() + }) + } + + async setListeners() { + this.currentTheme = await this.call('theme', 'currentTheme') + + this.on('theme', 'themeChanged', (theme: any) => { + this.currentTheme = theme + this.eventEmitter.emit('themeChanged', this.currentTheme) + }); + this.eventEmitter.emit('themeChanged', this.currentTheme) + + this.on('solidity', 'compilationFinished', (fileName: string, source: SourceWithTarget, languageVersion: string, data: CompilationResult) => { + const input: SolcInput = { + sources: source.sources + } + const output: SolcOutput = { + sources: data.sources as any + } + this.build = { + input: input, + output: output + } + this.fileName = fileName + this.eventEmitter.emit('compilationFinished', this.build, fileName) + }) + } + + async docgen(builds: Build[], userConfig?: Config): Promise { + const config = { ...defaults, ...userConfig } + const templates = await loadTemplates(config.theme, config.root, config.templates) + const site = buildSite(builds, config, templates.properties ?? {}) + const renderedSite = render(site, templates, config.collapseNewlines) + const docs: string[] = [] + for (const { id, contents } of renderedSite) { + const temp = `${this.fileName.split('/')[1].split('.')[0]}.${id.split('.')[1]}` + const newFileName = `docs/${temp}` + await this.call('fileManager', 'setFile', newFileName , contents) + docs.push(newFileName) + } + this.eventEmitter.emit('docsGenerated', docs) + this.emit('docgen' as any, 'docsGenerated', docs) + this.docs = docs + await this.opendDocs(docs) + } + + async opendDocs(docs: string[]) { + await this.call('manager', 'activatePlugin', 'doc-viewer') + await this.call('tabs' as any, 'focus', 'doc-viewer') + await this.call('doc-viewer' as any, 'viewDocs', docs) + } + + async generateDocs() { + this.docgen([this.build]) + } +} diff --git a/apps/doc-gen/src/app/docgen/common/helpers.ts b/apps/doc-gen/src/app/docgen/common/helpers.ts new file mode 100644 index 0000000000..926e6df9da --- /dev/null +++ b/apps/doc-gen/src/app/docgen/common/helpers.ts @@ -0,0 +1,22 @@ +import { VariableDeclaration } from "solidity-ast"; + +export function trim(text: string) { + if (typeof text === 'string') { + return text.trim(); + } +} + +export function joinLines(text?: string) { + if (typeof text === 'string') { + return text.replace(/\n+/g, ' '); + } +} + +/** + * Format a variable as its type followed by its name, if available. + */ +export function formatVariable(v: VariableDeclaration): string { + return [v.typeName?.typeDescriptions.typeString].concat(v.name || []).join(' '); +} + +export const eq = (a: unknown, b: unknown) => a === b; diff --git a/apps/doc-gen/src/app/docgen/common/properties.ts b/apps/doc-gen/src/app/docgen/common/properties.ts new file mode 100644 index 0000000000..460090a036 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/common/properties.ts @@ -0,0 +1,138 @@ +import { EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, ModifierDefinition, ParameterList, StructDefinition, UserDefinedValueTypeDefinition, VariableDeclaration } from 'solidity-ast'; +import { findAll, isNodeType } from 'solidity-ast/utils'; +import { NatSpec, parseNatspec } from '../utils/natspec'; +import { DocItemContext, DOC_ITEM_CONTEXT } from '../site'; +import { mapValues } from '../utils/map-values'; +import { DocItem, docItemTypes } from '../doc-item'; +import { formatVariable } from './helpers'; +import { PropertyGetter } from '../templates'; +import { itemType } from '../utils/item-type'; + +type TypeDefinition = StructDefinition | EnumDefinition | UserDefinedValueTypeDefinition; + +export function type ({ item }: DocItemContext): string { + return itemType(item); +} + +export function natspec ({ item }: DocItemContext): NatSpec { + return parseNatspec(item); +} + +export function name({ item }: DocItemContext, original?: unknown): string { + if (item.nodeType === 'FunctionDefinition') { + return item.kind === 'function' ? original as string : item.kind; + } else { + return original as string; + } +} + +export function fullName ({ item, contract }: DocItemContext): string { + if (contract) { + return `${contract.name}.${item.name}`; + } else { + return `${item.name}`; + } +} + +export function signature ({ item }: DocItemContext): string | undefined { + switch (item.nodeType) { + case 'ContractDefinition': + return undefined; + + case 'FunctionDefinition': { + const { kind, name } = item; + const params = item.parameters.parameters; + const returns = item.returnParameters.parameters; + const head = (kind === 'function' || kind === 'freeFunction') ? [kind, name].join(' ') : kind; + const res = [ + `${head}(${params.map(formatVariable).join(', ')})`, + item.visibility, + ]; + if (item.stateMutability !== 'nonpayable') { + res.push(item.stateMutability); + } + if (item.virtual) { + res.push('virtual'); + } + if (returns.length > 0) { + res.push(`returns (${returns.map(formatVariable).join(', ')})`); + } + return res.join(' '); + } + + case 'EventDefinition': { + const params = item.parameters.parameters; + return `event ${item.name}(${params.map(formatVariable).join(', ')})`; + } + + case 'ErrorDefinition': { + const params = item.parameters.parameters; + return `error ${item.name}(${params.map(formatVariable).join(', ')})`; + } + + case 'ModifierDefinition': { + const params = item.parameters.parameters; + return `modifier ${item.name}(${params.map(formatVariable).join(', ')})`; + } + + case 'VariableDeclaration': + return formatVariable(item); + } +} + +interface Param extends VariableDeclaration { + type: string; + natspec?: string; +}; + +function getParams (params: ParameterList, natspec: NatSpec['params'] | NatSpec['returns']): Param[] { + return params.parameters.map((p, i) => ({ + ...p, + type: p.typeDescriptions.typeString!, + natspec: natspec?.find((q, j) => q.name === undefined ? i === j : p.name === q.name)?.description, + })); +} + +export function params ({ item }: DocItemContext): Param[] | undefined { + if ('parameters' in item) { + return getParams(item.parameters, natspec(item[DOC_ITEM_CONTEXT]).params); + } +} + +export function returns ({ item }: DocItemContext): Param[] | undefined { + if ('returnParameters' in item) { + return getParams(item.returnParameters, natspec(item[DOC_ITEM_CONTEXT]).returns); + } +} + +export function items ({ item }: DocItemContext): DocItem[] | undefined { + return (item.nodeType === 'ContractDefinition') + ? item.nodes.filter(isNodeType(docItemTypes)).filter(n => !('visibility' in n) || n.visibility !== 'private') + : undefined; +} + +export function functions ({ item }: DocItemContext): FunctionDefinition[] | undefined { + return [...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private'); +} + +export function events ({ item }: DocItemContext): EventDefinition[] | undefined { + return [...findAll('EventDefinition', item)]; +} + +export function modifiers ({ item }: DocItemContext): ModifierDefinition[] | undefined { + return [...findAll('ModifierDefinition', item)]; +} + +export function errors ({ item }: DocItemContext): ErrorDefinition[] | undefined { + return [...findAll('ErrorDefinition', item)]; +} + +export function variables ({ item }: DocItemContext): VariableDeclaration[] | undefined { + return (item.nodeType === 'ContractDefinition') + ? item.nodes.filter(isNodeType('VariableDeclaration')).filter(v => v.stateVariable && v.visibility !== 'private') + : undefined; +} + +export function types ({ item }: DocItemContext): TypeDefinition[] | undefined { + return [...findAll(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition'], item)]; +} diff --git a/apps/doc-gen/src/app/docgen/config.ts b/apps/doc-gen/src/app/docgen/config.ts new file mode 100644 index 0000000000..2a3f9fcc6e --- /dev/null +++ b/apps/doc-gen/src/app/docgen/config.ts @@ -0,0 +1,84 @@ +import type { SourceUnit } from 'solidity-ast'; +import type { DocItem } from './doc-item'; +import type { PageAssigner, PageStructure } from './site'; + +export interface UserConfig { + /** + * The directory where rendered pages will be written. + * Defaults to 'docs'. + */ + outputDir?: string; + + /** + * A directory of custom templates that should take precedence over the + * theme's templates. + */ + templates?: string; + + /** + * The name of the built-in templates that will be used by default. + * Defaults to 'markdown'. + */ + theme?: string; + + /** + * The way documentable items (contracts, functions, custom errors, etc.) + * will be organized in pages. Built in options are: + * - 'single': all items in one page + * - 'items': one page per item + * - 'files': one page per input Solidity file + * More customization is possible by defining a function that returns a page + * path given the AST node for the item and the source unit where it is + * defined. + * Defaults to 'single'. + */ + pages?: 'single' | 'items' | 'files' | PageAssigner; + + /** + * An array of sources subdirectories that should be excluded from + * documentation, relative to the contract sources directory. + */ + exclude?: string[]; + + /** + * Clean up the output by collapsing 3 or more contiguous newlines into only 2. + * Enabled by default. + */ + collapseNewlines?: boolean; + + /** + * The extension for generated pages. + * Defaults to '.md'. + */ + pageExtension?: string; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// Other config parameters that will be provided by the environment (e.g. Hardhat) +// rather than by the user manually, unless using the library directly. +export interface Config extends UserConfig { + /** + * The root directory relative to which 'outputDir', 'sourcesDir', and + * 'templates' are specified. Defaults to the working directory. + */ + root?: string; + + /** + * The Solidity sources directory. + */ + sourcesDir?: string; +} + +export type FullConfig = Required; + +export const defaults: Omit = { + root: process.cwd(), + sourcesDir: 'contracts', + outputDir: 'docs', + pages: 'single', + exclude: [], + theme: 'markdown', + collapseNewlines: true, + pageExtension: '.md', +}; diff --git a/apps/doc-gen/src/app/docgen/doc-item.ts b/apps/doc-gen/src/app/docgen/doc-item.ts new file mode 100644 index 0000000000..eb03223027 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/doc-item.ts @@ -0,0 +1,27 @@ +import { ContractDefinition, ImportDirective, PragmaDirective, SourceUnit, UsingForDirective } from "solidity-ast"; +import { Node, NodeType, NodeTypeMap } from "solidity-ast/node"; +import { AssertEqual } from "./utils/assert-equal-types"; + +export type DocItem = Exclude< + SourceUnit['nodes'][number] | ContractDefinition['nodes'][number], + ImportDirective | PragmaDirective | UsingForDirective +>; + +export const docItemTypes = [ + 'ContractDefinition', + 'EnumDefinition', + 'ErrorDefinition', + 'EventDefinition', + 'FunctionDefinition', + 'ModifierDefinition', + 'StructDefinition', + 'UserDefinedValueTypeDefinition', + 'VariableDeclaration', +] as const; + +// Make sure at compile time that docItemTypes contains exactly the node types of DocItem. +const _: AssertEqual = true; + +export function isDocItem(node: Node): node is DocItem { + return (docItemTypes as readonly string[]).includes(node.nodeType); +} diff --git a/apps/doc-gen/src/app/docgen/render.ts b/apps/doc-gen/src/app/docgen/render.ts new file mode 100644 index 0000000000..5c206f92b6 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/render.ts @@ -0,0 +1,87 @@ +import Handlebars, { RuntimeOptions } from 'handlebars'; +import { Site, Page, DocItemWithContext, DOC_ITEM_CONTEXT } from './site'; +import { Templates } from './templates'; +import { itemType } from './utils/item-type'; +import fs from 'fs'; + +export interface RenderedPage { + id: string; + contents: string; +} + +interface TemplateOptions { + data: { + site: Site; + }; +} + +export function render(site: Site, templates: Templates, collapseNewlines?: boolean): RenderedPage[] { + const renderPage = buildRenderer(templates); + const renderedPages: RenderedPage[] = []; + for (const page of site.pages) { + let contents = renderPage(page, { data: { site } }); + if (collapseNewlines) { + contents = contents.replace(/\n{3,}/g, '\n\n'); + } + renderedPages.push({ + id: page.id, + contents, + }); + } + return renderedPages; +} + +export const itemPartialName = (item: DocItemWithContext) => itemType(item).replace(/ /g, '-').toLowerCase(); + +function itemPartial(item: DocItemWithContext, options?: RuntimeOptions) { + if (!item.__item_context) { + throw new Error(`Partial 'item' used in unsupported context (not a doc item)`); + } + const partial = options?.partials?.[itemPartialName(item)]; + if (!partial) { + throw new Error(`Missing partial '${itemPartialName(item)}'`); + } + return partial(item, options); +} + +function readmeHelper(H: typeof Handlebars, path: string, opts: RuntimeOptions) { + const items: DocItemWithContext[] = opts.data.root.items; + const renderedItems = Object.fromEntries( + items.map(item => [ + item.name, + new H.SafeString( + H.compile('{{>item}}')(item, opts), + ), + ]), + ); + return new H.SafeString( + H.compile(fs.readFileSync(path, 'utf8'))(renderedItems, opts), + ); +} + +function buildRenderer(templates: Templates): (page: Page, options: TemplateOptions) => string { + const pageTemplate = templates.partials?.page; + if (pageTemplate === undefined) { + throw new Error(`Missing 'page' template`); + } + + const H = Handlebars.create(); + + for (const [name, getBody] of Object.entries(templates.partials ?? {})) { + let partial: HandlebarsTemplateDelegate | undefined; + H.registerPartial(name, (...args) => { + partial ??= H.compile(getBody()); + return partial(...args); + }); + } + + H.registerHelper('readme', (path: string, opts: RuntimeOptions) => readmeHelper(H, path, opts)); + + for (const [name, fn] of Object.entries(templates.helpers ?? {})) { + H.registerHelper(name, fn); + } + + H.registerPartial('item', itemPartial); + + return H.compile('{{>page}}'); +} diff --git a/apps/doc-gen/src/app/docgen/site.ts b/apps/doc-gen/src/app/docgen/site.ts new file mode 100644 index 0000000000..53ffd5bc92 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/site.ts @@ -0,0 +1,138 @@ +import path from 'path'; +import { ContractDefinition, SourceUnit } from 'solidity-ast'; +import { SolcOutput, SolcInput } from 'solidity-ast/solc'; +import { astDereferencer, ASTDereferencer, findAll, isNodeType, srcDecoder, SrcDecoder } from 'solidity-ast/utils'; +import { FullConfig } from './config'; +import { DocItem, docItemTypes, isDocItem } from './doc-item'; +import { Properties } from './templates'; +import { clone } from './utils/clone'; +import { isChild } from './utils/is-child'; +import { mapValues } from './utils/map-values'; +import { defineGetterMemoized } from './utils/memoized-getter'; + +export interface Build { + input: SolcInput; + output: SolcOutput; +} + +export interface BuildContext extends Build { + deref: ASTDereferencer; + decodeSrc: SrcDecoder; +} + +export type SiteConfig = Pick; +export type PageStructure = SiteConfig['pages']; +export type PageAssigner = ((item: DocItem, file: SourceUnit, config: SiteConfig) => string | undefined); + +export const pageAssigner: Record = { + single: (_1, _2, { pageExtension: ext }) => 'index' + ext, + items: (item, _, { pageExtension: ext }) => item.name + ext, + files: (_, file, { pageExtension: ext, sourcesDir }) => + path.relative(sourcesDir, file.absolutePath).replace('.sol', ext), +}; + +export interface Site { + items: DocItemWithContext[]; + pages: Page[]; +} + +export interface Page { + id: string; + items: DocItemWithContext[]; +} + +export const DOC_ITEM_CONTEXT = '__item_context' as const; +export type DocItemWithContext = DocItem & { [DOC_ITEM_CONTEXT]: DocItemContext }; + +export interface DocItemContext { + page?: string; + item: DocItemWithContext; + contract?: ContractDefinition; + file: SourceUnit; + build: BuildContext; +} + +export function buildSite (builds: Build[], siteConfig: SiteConfig, properties: Properties = {}): Site { + const assign = typeof siteConfig.pages === 'string' ? pageAssigner[siteConfig.pages] : siteConfig.pages; + + const seen = new Set(); + const items: DocItemWithContext[] = []; + const pages: Record = {}; + + // eslint-disable-next-line prefer-const + for (let { input, output } of builds) { + // Clone because we will mutate in order to add item context. + output = { ...output, sources: clone(output.sources) }; + + const deref = astDereferencer(output); + const decodeSrc = srcDecoder(input, output); + const build = { input, output, deref, decodeSrc }; + + for (const { ast: file } of Object.values(output.sources)) { + const isNewFile = !seen.has(file.absolutePath); + seen.add(file.absolutePath); + + for (const topLevelItem of file.nodes) { + if (!isDocItem(topLevelItem)) continue; + + const page = assignIfIncludedSource(assign, topLevelItem, file, siteConfig); + + const withContext = defineContext(topLevelItem, build, file, page); + defineProperties(withContext, properties); + + if (isNewFile && page !== undefined) { + (pages[page] ??= []).push(withContext); + items.push(withContext); + } + + if (!isNodeType('ContractDefinition', topLevelItem)) { + continue; + } + + for (const item of topLevelItem.nodes) { + if (!isDocItem(item)) continue; + if (isNewFile && page !== undefined) items.push(item as DocItemWithContext); + const contract = topLevelItem.nodeType === 'ContractDefinition' ? topLevelItem : undefined; + const withContext = defineContext(item, build, file, page, contract); + defineProperties(withContext, properties); + } + } + } + } + + return { + items, + pages: Object.entries(pages).map(([id, pageItems]) => ({ id, items: pageItems })), + }; +} + +function defineContext (item: DocItem, build: BuildContext, file: SourceUnit, page?: string, contract?: ContractDefinition): DocItemWithContext { + return Object.assign(item, { + [DOC_ITEM_CONTEXT]: { build, file, contract, page, item: item as DocItemWithContext }, + }); +} + +function defineProperties (item: DocItemWithContext, properties: Properties) { + for (const [prop, fn] of Object.entries(properties)) { + const original: unknown = (item as any)[prop]; + defineGetterMemoized(item as any, prop, () => fn(item.__item_context, original)); + } +} + +function assignIfIncludedSource ( + assign: PageAssigner, + item: DocItem, + file: SourceUnit, + config: SiteConfig, +) { + return isFileIncluded(file.absolutePath, config) + ? assign(item, file, config) + : undefined; +} + +function isFileIncluded (file: string, config: SiteConfig) { + return ( + isChild(file, config.sourcesDir) && + config.exclude.every(e => !isChild(file, path.join(config.sourcesDir, e))) + ); +} diff --git a/apps/doc-gen/src/app/docgen/templates.ts b/apps/doc-gen/src/app/docgen/templates.ts new file mode 100644 index 0000000000..cee8f12c75 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/templates.ts @@ -0,0 +1,90 @@ +import { mapKeys } from './utils/map-keys'; +import { DocItemContext } from './site'; + +import * as defaultProperties from './common/properties'; + +export type PropertyGetter = (ctx: DocItemContext, original?: unknown) => unknown; +export type Properties = Record; + +export interface Templates { + partials?: Record string>; + helpers?: Record string>; + properties?: Record; +} + +/** + * Loads the templates that will be used for rendering a site based on a + * default theme and user templates. + * + * The result contains all partials, helpers, and property getters defined in + * the user templates and the default theme, where the user's take precedence + * if there is a clash. Additionally, all theme partials and helpers are + * included with the theme prefix, e.g. `markdown/contract` will be a partial. + */ +export async function loadTemplates(defaultTheme: string, root: string, userTemplatesPath?: string): Promise { + const themes = await readThemes(); + + // Initialize templates with the default theme. + const templates: Required = { + partials: { ...themes[defaultTheme]?.partials }, + helpers: { ...themes[defaultTheme]?.helpers }, + properties: { ...defaultProperties }, + }; + + + // Add partials and helpers from all themes, prefixed with the theme name. + for (const [themeName, theme] of Object.entries(themes)) { + const addPrefix = (k: string) => `${themeName}/${k}`; + Object.assign(templates.partials, mapKeys(theme.partials, addPrefix)); + Object.assign(templates.helpers, mapKeys(theme.helpers, addPrefix)); + } + + return templates; +} + +/** + * Read templates and helpers from a directory. + */ +export async function readTemplates(): Promise> { + return { + partials: await readPartials(), + helpers: await readHelpers('helpers'), + properties: await readHelpers('properties'), + }; +} + +async function readPartials() { + const partials: NonNullable = {}; + const partialNames = ["common", "contract", "enum", "error", "event", "function", "modifier", "page", "struct", "variable", "user-defined-value-type"] + for (const name of partialNames) { + const p = await import('raw-loader!./themes/markdown/' + name + '.hbs') + partials[name] = () => p.default + } + return partials; +} + +async function readHelpers(name: string) { + let helpersPath; + + const h = await import('./themes/markdown/helpers'); + const helpers: Record any> = {}; + + for (const name in h) { + if (typeof h[name] === 'function') { + helpers[name] = h[name]; + } + } + + return helpers; +} + +/** + * Reads all built-in themes into an object. Partials will always be found in + * src/themes, whereas helpers may instead be found in dist/themes if TypeScript + * can't be imported directly. + */ +async function readThemes(): Promise>> { + const themes: Record> = {} + themes['markdown'] = await readTemplates() + return themes +} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/common.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/common.hbs new file mode 100644 index 0000000000..b2a3b2a5a2 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/common.hbs @@ -0,0 +1,34 @@ +{{h}} {{name}} + +{{#if signature}} +```solidity +{{{signature}}} +``` +{{/if}} + +{{{natspec.notice}}} + +{{#if natspec.dev}} +_{{{natspec.dev}}}_ +{{/if}} + +{{#if natspec.params}} +{{h 2}} Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +{{#each params}} +| {{name}} | {{type}} | {{{joinLines natspec}}} | +{{/each}} +{{/if}} + +{{#if natspec.returns}} +{{h 2}} Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | +{{#each returns}} +| {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} | +{{/each}} +{{/if}} + diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/contract.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/contract.hbs new file mode 100644 index 0000000000..e4ed15831c --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/contract.hbs @@ -0,0 +1,8 @@ +{{>common}} + +{{#each items}} +{{#hsection}} +{{>item}} +{{/hsection}} + +{{/each}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/enum.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/enum.hbs new file mode 100644 index 0000000000..677406db9c --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/enum.hbs @@ -0,0 +1,9 @@ +{{>common}} + +```solidity +enum {{name}} { +{{#each members}} + {{name}}{{#unless @last}},{{/unless}} +{{/each}} +} +``` diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/error.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/error.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/error.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/event.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/event.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/event.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/function.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/function.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/function.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/helpers.ts b/apps/doc-gen/src/app/docgen/themes/markdown/helpers.ts new file mode 100644 index 0000000000..bfd5cfbdd7 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/helpers.ts @@ -0,0 +1,49 @@ +import { HelperOptions, Utils } from 'handlebars'; + +export * from '../../common/helpers'; + +/** + * Returns a Markdown heading marker. An optional number increases the heading level. + * + * Input Output + * {{h}} {{name}} # Name + * {{h 2}} {{name}} ## Name + */ +export function h(opts: HelperOptions): string; +export function h(hsublevel: number, opts: HelperOptions): string; +export function h(hsublevel: number | HelperOptions, opts?: HelperOptions) { + const { hlevel } = getHLevel(hsublevel, opts); + return new Array(hlevel).fill('#').join(''); +}; + +/** + * Delineates a section where headings should be increased by 1 or a custom number. + * + * {{#hsection}} + * {{>partial-with-headings}} + * {{/hsection}} + */ +export function hsection(opts: HelperOptions): string; +export function hsection(hsublevel: number, opts: HelperOptions): string; +export function hsection(this: unknown, hsublevel: number | HelperOptions, opts?: HelperOptions) { + let hlevel; + ({ hlevel, opts } = getHLevel(hsublevel, opts)); + opts.data = Utils.createFrame(opts.data); + opts.data.hlevel = hlevel; + return opts.fn(this as unknown, opts); +} + +/** + * Helper for dealing with the optional hsublevel argument. + */ +function getHLevel(hsublevel: number | HelperOptions, opts?: HelperOptions) { + if (typeof hsublevel === 'number') { + opts = opts!; + hsublevel = Math.max(1, hsublevel); + } else { + opts = hsublevel; + hsublevel = 1; + } + const contextHLevel: number = opts.data?.hlevel ?? 0; + return { opts, hlevel: contextHLevel + hsublevel }; +} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/modifier.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/modifier.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/modifier.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/page.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/page.hbs new file mode 100644 index 0000000000..6597f0b5c7 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/page.hbs @@ -0,0 +1,8 @@ +# Solidity API + +{{#each items}} +{{#hsection}} +{{>item}} +{{/hsection}} + +{{/each}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/struct.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/struct.hbs new file mode 100644 index 0000000000..867069e2bc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/struct.hbs @@ -0,0 +1,9 @@ +{{>common}} + +```solidity +struct {{name}} { +{{#each members}} + {{{typeName.typeDescriptions.typeString}}} {{name}}; +{{/each}} +} +``` diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/user-defined-value-type.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/user-defined-value-type.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/user-defined-value-type.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/themes/markdown/variable.hbs b/apps/doc-gen/src/app/docgen/themes/markdown/variable.hbs new file mode 100644 index 0000000000..5373296cbc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/themes/markdown/variable.hbs @@ -0,0 +1 @@ +{{>common}} diff --git a/apps/doc-gen/src/app/docgen/utils/ItemError.ts b/apps/doc-gen/src/app/docgen/utils/ItemError.ts new file mode 100644 index 0000000000..3791f71443 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/ItemError.ts @@ -0,0 +1,13 @@ +import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site'; + +export class ItemError extends Error { + constructor(msg: string, item: DocItemWithContext) { + const ctx = item[DOC_ITEM_CONTEXT]; + const src = ctx && ctx.build.decodeSrc(item); + if (src) { + super(msg + ` (${src})`); + } else { + super(msg); + } + } +} diff --git a/apps/doc-gen/src/app/docgen/utils/arrays-equal.ts b/apps/doc-gen/src/app/docgen/utils/arrays-equal.ts new file mode 100644 index 0000000000..fe08726e95 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/arrays-equal.ts @@ -0,0 +1,5 @@ +export function arraysEqual(a: T[], b: T[]): boolean; +export function arraysEqual(a: T[], b: T[], mapFn: (x: T) => U): boolean; +export function arraysEqual(a: T[], b: T[], mapFn = (x: T) => x): boolean { + return a.length === b.length && a.every((x, i) => mapFn(x) === mapFn(b[i]!)); +} diff --git a/apps/doc-gen/src/app/docgen/utils/assert-equal-types.ts b/apps/doc-gen/src/app/docgen/utils/assert-equal-types.ts new file mode 100644 index 0000000000..0171d13a36 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/assert-equal-types.ts @@ -0,0 +1 @@ +export type AssertEqual = [T, U] extends [U, T] ? true : never; diff --git a/apps/doc-gen/src/app/docgen/utils/clone.ts b/apps/doc-gen/src/app/docgen/utils/clone.ts new file mode 100644 index 0000000000..f92fa70189 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/clone.ts @@ -0,0 +1,6 @@ +/** + * Deep cloning good enough for simple objects like solc output. Types are not + * sound because the function may lose information: non-enumerable properties, + * symbols, undefined values, prototypes, etc. + */ +export const clone = (obj: T): T => JSON.parse(JSON.stringify(obj)); diff --git a/apps/doc-gen/src/app/docgen/utils/ensure-array.ts b/apps/doc-gen/src/app/docgen/utils/ensure-array.ts new file mode 100644 index 0000000000..cd0f8ea570 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/ensure-array.ts @@ -0,0 +1,12 @@ +// The function below would not be correctly typed if the return type was T[] +// because T may itself be an array type and Array.isArray would not know the +// difference. Adding IfArray makes sure the return type is always correct. +type IfArray = T extends any[] ? T : never; + +export function ensureArray(x: T | T[]): T[] | IfArray { + if (Array.isArray(x)) { + return x; + } else { + return [x]; + } +} diff --git a/apps/doc-gen/src/app/docgen/utils/execall.ts b/apps/doc-gen/src/app/docgen/utils/execall.ts new file mode 100644 index 0000000000..2aca0ad77c --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/execall.ts @@ -0,0 +1,18 @@ +/** + * Iterates over all contiguous matches of the regular expression over the + * text. Stops as soon as the regular expression no longer matches at the + * current position. + */ +export function* execAll(re: RegExp, text: string) { + re = new RegExp(re, re.flags + (re.sticky ? '' : 'y')); + + while (true) { + const match = re.exec(text); + + // We break out of the loop if there is no match or if the empty string is + // matched because no progress will be made and it will loop indefinitely. + if (!match?.[0]) break; + + yield match; + } +} diff --git a/apps/doc-gen/src/app/docgen/utils/is-child.ts b/apps/doc-gen/src/app/docgen/utils/is-child.ts new file mode 100644 index 0000000000..cb07cc7136 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/is-child.ts @@ -0,0 +1,5 @@ +import path from 'path'; + +export function isChild(file: string, parent: string) { + return path.normalize(file + path.sep).startsWith(path.normalize(parent + path.sep)); +} diff --git a/apps/doc-gen/src/app/docgen/utils/item-type.ts b/apps/doc-gen/src/app/docgen/utils/item-type.ts new file mode 100644 index 0000000000..8873ece1fc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/item-type.ts @@ -0,0 +1,7 @@ +import { DocItem } from '../doc-item'; + +export function itemType(item: DocItem): string { + return item.nodeType + .replace(/(Definition|Declaration)$/, '') + .replace(/(\w)([A-Z])/g, '$1 $2'); +} diff --git a/apps/doc-gen/src/app/docgen/utils/map-keys.ts b/apps/doc-gen/src/app/docgen/utils/map-keys.ts new file mode 100644 index 0000000000..7852cd85dc --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/map-keys.ts @@ -0,0 +1,4 @@ +export function mapKeys(obj: Record, fn: (key: string) => string): Record { + return Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v])); +} + diff --git a/apps/doc-gen/src/app/docgen/utils/map-values.ts b/apps/doc-gen/src/app/docgen/utils/map-values.ts new file mode 100644 index 0000000000..c151d7d552 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/map-values.ts @@ -0,0 +1,19 @@ +export function mapValues(obj: Record, fn: (value: T) => U): Record { + const res: Record = {}; + for (const [k, v] of Object.entries(obj)) { + res[k] = fn(v); + } + return res; +} + +export function filterValues(obj: Record, fn: (value: T) => value is U): Record; +export function filterValues(obj: Record, fn: (value: T) => boolean): Record; +export function filterValues(obj: Record, fn: (value: T) => boolean): Record { + const res: Record = {}; + for (const [k, v] of Object.entries(obj)) { + if (fn(v)) { + res[k] = v; + } + } + return res; +} diff --git a/apps/doc-gen/src/app/docgen/utils/memoized-getter.ts b/apps/doc-gen/src/app/docgen/utils/memoized-getter.ts new file mode 100644 index 0000000000..1a2fef4ae5 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/memoized-getter.ts @@ -0,0 +1,23 @@ +export function defineGetterMemoized(obj: O, key: K, getter: () => T) { + let state: 'todo' | 'doing' | 'done' = 'todo'; + let value: T; + + Object.defineProperty(obj, key, { + enumerable: true, + get() { + switch (state) { + case 'done': + return value; + + case 'doing': + throw new Error("Detected recursion"); + + case 'todo': + state = 'doing'; + value = getter(); + state = 'done'; + return value; + } + } + }); +} diff --git a/apps/doc-gen/src/app/docgen/utils/natspec.ts b/apps/doc-gen/src/app/docgen/utils/natspec.ts new file mode 100644 index 0000000000..40f752aaa8 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/natspec.ts @@ -0,0 +1,145 @@ +import { FunctionDefinition } from 'solidity-ast'; +import { findAll } from 'solidity-ast/utils'; +import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site'; +import { arraysEqual } from './arrays-equal'; +import { execAll } from './execall'; +import { itemType } from './item-type'; +import { ItemError } from './ItemError'; +import { readItemDocs } from './read-item-docs'; +import { getContractsInScope } from './scope'; + +export interface NatSpec { + title?: string; + notice?: string; + dev?: string; + params?: { + name: string; + description: string; + }[]; + returns?: { + name?: string; + description: string; + }[]; + custom?: { + [tag: string]: string; + }; +} + +export function parseNatspec(item: DocItemWithContext): NatSpec { + if (!item[DOC_ITEM_CONTEXT]) throw new Error(`Not an item or item is missing context`); + + let res: NatSpec = {}; + + const docSource = readItemDocs(item); + const docString = docSource !== undefined + ? cleanUpDocstringFromSource(docSource) + : 'documentation' in item && item.documentation + ? typeof item.documentation === 'string' + ? item.documentation + : cleanUpDocstringFromSolc(item.documentation.text) + : ''; + + const tagMatches = execAll( + /^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/m, + docString, + ); + + let inheritFrom: FunctionDefinition | undefined; + + for (const [, tag = 'notice', content] of tagMatches) { + if (content === undefined) throw new ItemError('Unexpected error', item); + + if (tag === 'dev' || tag === 'notice') { + res[tag] ??= ''; + res[tag] += content; + } + + if (tag === 'title') { + res.title = content.trim(); + } + + if (tag === 'param') { + const paramMatches = content.match(/(\w+) ([^]*)/); + if (paramMatches) { + const [, name, description] = paramMatches as [string, string, string]; + res.params ??= []; + res.params.push({ name, description: description.trim() }); + } + } + + if (tag === 'return') { + if (!('returnParameters' in item)) { + throw new ItemError(`Item does not contain return parameters`, item); + } + res.returns ??= []; + const i = res.returns.length; + const p = item.returnParameters.parameters[i]; + if (p === undefined) { + throw new ItemError('Got more @return tags than expected', item); + } + if (!p.name) { + res.returns.push({ description: content.trim() }); + } else { + const paramMatches = content.match(/(\w+)( ([^]*))?/); + if (!paramMatches || paramMatches[1] !== p.name) { + throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item); + } + const [, name, description] = paramMatches as [string, string, string?]; + res.returns.push({ name, description: description?.trim() ?? '' }); + } + } + + if (tag?.startsWith('custom:')) { + const key = tag.replace(/^custom:/, ''); + res.custom ??= {}; + res.custom[key] ??= ''; + res.custom[key] += content; + } + + if (tag === 'inheritdoc') { + if (!(item.nodeType === 'FunctionDefinition' || item.nodeType === 'VariableDeclaration')) { + throw new ItemError(`Expected function or variable but saw ${itemType(item)}`, item); + } + const parentContractName = content.trim(); + const parentContract = getContractsInScope(item)[parentContractName]; + if (!parentContract) { + throw new ItemError(`Parent contract '${parentContractName}' not found`, item); + } + inheritFrom = [...findAll('FunctionDefinition', parentContract)].find(f => item.baseFunctions?.includes(f.id)); + } + } + + if (docString.length === 0) { + if ('baseFunctions' in item && item.baseFunctions?.length === 1) { + const baseFn = item[DOC_ITEM_CONTEXT].build.deref('FunctionDefinition', item.baseFunctions[0]!); + const shouldInherit = item.nodeType === 'VariableDeclaration' || arraysEqual(item.parameters.parameters, baseFn.parameters.parameters, p => p.name); + if (shouldInherit) { + inheritFrom = baseFn; + } + } + } + + if (res.dev) res.dev = res.dev.trim(); + if (res.notice) res.notice = res.notice.trim(); + + if (inheritFrom) { + res = { ...parseNatspec(inheritFrom as DocItemWithContext), ...res }; + } + + return res; +} + +// Fix solc buggy parsing of doc comments. +// Reverse engineered from solc behavior. +function cleanUpDocstringFromSolc(text: string) { + return text + .replace(/\n\n?^[ \t]*(?:\*|\/\/\/)/mg, '\n\n') + .replace(/^[ \t]?/mg, ''); +} + +function cleanUpDocstringFromSource(text: string) { + return text + .replace(/^\/\*\*(.*)\*\/$/s, '$1') + .trim() + .replace(/^[ \t]*(\*|\/\/\/)[ \t]?/mg, ''); +} diff --git a/apps/doc-gen/src/app/docgen/utils/read-item-docs.ts b/apps/doc-gen/src/app/docgen/utils/read-item-docs.ts new file mode 100644 index 0000000000..36ee13aa7f --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/read-item-docs.ts @@ -0,0 +1,26 @@ +import { DocItemWithContext, DOC_ITEM_CONTEXT, Build } from '../site'; + +export function readItemDocs(item: DocItemWithContext): string | undefined { + const { build } = item[DOC_ITEM_CONTEXT]; + // Note that Solidity 0.5 has item.documentation: string even though the + // types do not reflect that. This is why we check typeof === object. + if ('documentation' in item && item.documentation && typeof item.documentation === 'object') { + const { source, start, length } = decodeSrc(item.documentation.src, build); + const content = build.input.sources[source]?.content; + if (content !== undefined) { + return Buffer.from(content, 'utf8').slice(start, start + length).toString('utf8'); + } + } +} + +function decodeSrc(src: string, build: Build): { source: string; start: number; length: number } { + const [start, length, sourceId] = src.split(':').map(s => parseInt(s)); + if (start === undefined || length === undefined || sourceId === undefined) { + throw new Error(`Bad source string ${src}`); + } + const source = Object.keys(build.output.sources).find(s => build.output.sources[s]?.id === sourceId); + if (source === undefined) { + throw new Error(`No source with id ${sourceId}`); + } + return { source, start, length }; +} diff --git a/apps/doc-gen/src/app/docgen/utils/scope.ts b/apps/doc-gen/src/app/docgen/utils/scope.ts new file mode 100644 index 0000000000..dabe990137 --- /dev/null +++ b/apps/doc-gen/src/app/docgen/utils/scope.ts @@ -0,0 +1,63 @@ +import { ContractDefinition, SourceUnit } from "solidity-ast"; +import { findAll, isNodeType } from "solidity-ast/utils"; +import { DocItemWithContext } from "../site"; +import { filterValues, mapValues } from './map-values'; +import { mapKeys } from './map-keys'; + +type Definition = SourceUnit['nodes'][number] & { name: string }; +type Scope = { [name in string]: () => { namespace: Scope } | { definition: Definition } }; + +export function getContractsInScope(item: DocItemWithContext) { + const cache = new WeakMap(); + + return filterValues( + flattenScope(run(item.__item_context.file)), + isNodeType('ContractDefinition'), + ); + + function run(file: SourceUnit): Scope { + if (cache.has(file)) { + return cache.get(file)!; + } + + const scope: Scope = {}; + + cache.set(file, scope); + + for (const c of file.nodes) { + if ('name' in c) { + scope[c.name] = () => ({ definition: c }); + } + } + + for (const i of findAll('ImportDirective', file)) { + const importedFile = item.__item_context.build.deref('SourceUnit', i.sourceUnit); + const importedScope = run(importedFile); + if (i.unitAlias) { + scope[i.unitAlias] = () => ({ namespace: importedScope }); + } else if (i.symbolAliases.length === 0) { + Object.assign(scope, importedScope); + } else { + for (const a of i.symbolAliases) { + // Delayed function call supports circular dependencies + scope[a.local ?? a.foreign.name] = importedScope[a.foreign.name] ?? (() => importedScope[a.foreign.name]!()); + } + } + }; + + return scope; + } +} + +function flattenScope(scope: Scope): Record { + return Object.fromEntries( + Object.entries(scope).flatMap(([k, fn]) => { + const v = fn(); + if ('definition' in v) { + return [[k, v.definition] as const]; + } else { + return Object.entries(mapKeys(flattenScope(v.namespace), k2 => k + '.' + k2)); + } + }), + ); +} diff --git a/apps/doc-gen/src/app/hooks/useLocalStorage.tsx b/apps/doc-gen/src/app/hooks/useLocalStorage.tsx new file mode 100644 index 0000000000..d677de7813 --- /dev/null +++ b/apps/doc-gen/src/app/hooks/useLocalStorage.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; + +export function useLocalStorage(key: string, initialValue: any) { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + // If error also return initialValue + console.log(error); + return initialValue; + } + }); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = (value: any) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = + value instanceof Function ? value(storedValue) : value; + // Save state + setStoredValue(valueToStore); + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error); + } + }; + + return [storedValue, setValue]; +} diff --git a/apps/doc-gen/src/app/views/ErrorView.tsx b/apps/doc-gen/src/app/views/ErrorView.tsx new file mode 100644 index 0000000000..f1c4fdaf91 --- /dev/null +++ b/apps/doc-gen/src/app/views/ErrorView.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +export const ErrorView: React.FC = () => { + return ( +
+ Error page +
Sorry, something unexpected happened.
+
+ Please raise an issue:{" "} + + Here + +
+
+ ); +}; diff --git a/apps/doc-gen/src/app/views/index.ts b/apps/doc-gen/src/app/views/index.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/apps/doc-gen/src/app/views/index.ts @@ -0,0 +1 @@ + diff --git a/apps/doc-gen/src/favicon.ico b/apps/doc-gen/src/favicon.ico new file mode 100644 index 0000000000..bcd5dfd67c Binary files /dev/null and b/apps/doc-gen/src/favicon.ico differ diff --git a/apps/doc-gen/src/index.html b/apps/doc-gen/src/index.html new file mode 100644 index 0000000000..6e26b4393e --- /dev/null +++ b/apps/doc-gen/src/index.html @@ -0,0 +1,13 @@ + + + + + Remix Docgen + + + + + +
+ + diff --git a/apps/doc-gen/src/main.tsx b/apps/doc-gen/src/main.tsx new file mode 100644 index 0000000000..a6dc3b3a47 --- /dev/null +++ b/apps/doc-gen/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import App from "./app/App"; +// import { Routes } from "./routes"; + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/apps/doc-gen/src/types.ts b/apps/doc-gen/src/types.ts new file mode 100644 index 0000000000..187fb6e7b3 --- /dev/null +++ b/apps/doc-gen/src/types.ts @@ -0,0 +1,11 @@ +export type Documentation = string + +export interface EthDocumentation { + [contractName: string]: Documentation +} + +export type ContractName = string + +export type FileName = string + +export type PublishedSite = string \ No newline at end of file diff --git a/apps/doc-gen/tsconfig.app.json b/apps/doc-gen/tsconfig.app.json new file mode 100644 index 0000000000..af84f21cfc --- /dev/null +++ b/apps/doc-gen/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/apps/doc-gen/tsconfig.json b/apps/doc-gen/tsconfig.json new file mode 100644 index 0000000000..5aab5e7911 --- /dev/null +++ b/apps/doc-gen/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/apps/doc-gen/webpack.config.js b/apps/doc-gen/webpack.config.js new file mode 100644 index 0000000000..7b3d088f05 --- /dev/null +++ b/apps/doc-gen/webpack.config.js @@ -0,0 +1,82 @@ +const { composePlugins, withNx } = require('@nrwl/webpack') +const { withReact } = require('@nrwl/react') +const webpack = require('webpack') +const TerserPlugin = require("terser-webpack-plugin") +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") + +// Nx plugins for webpack. +module.exports = composePlugins(withNx(), withReact(), (config) => { + // Update the webpack config as needed here. + // e.g. `config.plugins.push(new MyPlugin())` + + // add fallback for node modules + config.resolve.fallback = { + ...config.resolve.fallback, + "crypto": require.resolve("crypto-browserify"), + "stream": require.resolve("stream-browserify"), + "path": require.resolve("path-browserify"), + "http": require.resolve("stream-http"), + "https": require.resolve("https-browserify"), + "constants": require.resolve("constants-browserify"), + "os": false, //require.resolve("os-browserify/browser"), + "timers": false, // require.resolve("timers-browserify"), + "zlib": require.resolve("browserify-zlib"), + "fs": false, + "module": false, + "tls": false, + "net": false, + "readline": false, + "child_process": false, + "buffer": require.resolve("buffer/"), + "vm": require.resolve('vm-browserify'), + } + + // add externals + config.externals = { + ...config.externals, + solc: 'solc', + } + + // add public path + config.output.publicPath = '/' + + // add copy & provide plugin + config.plugins.push( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + url: ['url', 'URL'], + process: 'process/browser', + }), + new webpack.DefinePlugin({ + + }), + ) + + // souce-map loader + config.module.rules.push({ + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" + }) + + config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings + + // set minimizer + config.optimization.minimizer = [ + new TerserPlugin({ + parallel: true, + terserOptions: { + ecma: 2015, + compress: false, + mangle: false, + format: { + comments: false, + }, + }, + extractComments: false, + }), + new CssMinimizerPlugin(), + ]; + + return config; +}) diff --git a/apps/doc-viewer/project.json b/apps/doc-viewer/project.json new file mode 100644 index 0000000000..5ea04be27e --- /dev/null +++ b/apps/doc-viewer/project.json @@ -0,0 +1,59 @@ +{ + "name": "doc-viewer", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/doc-viewer/src", + "projectType": "application", + "implicitDependencies": [ + ], + "targets": { + "build": { + "executor": "@nrwl/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "development", + "options": { + "compiler": "babel", + "outputPath": "dist/apps/doc-viewer", + "index": "apps/doc-viewer/src/index.html", + "baseHref": "/", + "main": "apps/doc-viewer/src/main.tsx", + "tsConfig": "apps/doc-viewer/tsconfig.app.json", + "assets": [ + "apps/doc-viewer/src/favicon.ico" + ], + "styles": [], + "scripts": [], + "webpackConfig": "apps/doc-viewer/webpack.config.js" + }, + "configurations": { + "development": { + }, + "production": { + "fileReplacements": [ + { + "replace": "apps/doc-viewer/src/environments/environment.ts", + "with": "apps/doc-viewer/src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "executor": "@nrwl/webpack:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "doc-viewer:build", + "hmr": true + }, + "configurations": { + "development": { + "buildTarget": "doc-viewer:build:development", + "port": 7003 + }, + "production": { + "buildTarget": "doc-viewer:build:production" + } + } + } + }, + "tags": [] +} diff --git a/apps/doc-viewer/src/app/App.tsx b/apps/doc-viewer/src/app/App.tsx new file mode 100644 index 0000000000..cbb57460d0 --- /dev/null +++ b/apps/doc-viewer/src/app/App.tsx @@ -0,0 +1,23 @@ +import React, { useEffect, useState } from "react" +import { DocViewer } from "./docviewer" +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' + +const client = new DocViewer() + +export default function App() { + const [contents, setContents] = useState('') + useEffect(() => { + client.eventEmitter.on('contentsReady', (fileContents: string) => { + setContents(fileContents) + }) + + }, []) + return ( + <> +
+ +
+ + ) +} \ No newline at end of file diff --git a/apps/doc-viewer/src/app/docviewer.ts b/apps/doc-viewer/src/app/docviewer.ts new file mode 100644 index 0000000000..d6fed6e490 --- /dev/null +++ b/apps/doc-viewer/src/app/docviewer.ts @@ -0,0 +1,22 @@ +import { PluginClient } from '@remixproject/plugin' +import { createClient } from '@remixproject/plugin-webview' +import EventEmitter from 'events' + +export class DocViewer extends PluginClient { + mdFile: string + eventEmitter: EventEmitter + constructor() { + super() + this.eventEmitter = new EventEmitter() + this.methods = ['viewDocs'] + createClient(this) + this.mdFile = '' + this.onload() + } + + async viewDocs(docs: string[]) { + this.mdFile = docs[0] + const contents = await this.call('fileManager', 'readFile', this.mdFile) + this.eventEmitter.emit('contentsReady', contents) + } +} \ No newline at end of file diff --git a/apps/doc-viewer/src/favicon.ico b/apps/doc-viewer/src/favicon.ico new file mode 100644 index 0000000000..bcd5dfd67c Binary files /dev/null and b/apps/doc-viewer/src/favicon.ico differ diff --git a/apps/doc-viewer/src/index.html b/apps/doc-viewer/src/index.html new file mode 100644 index 0000000000..ab3b67eef1 --- /dev/null +++ b/apps/doc-viewer/src/index.html @@ -0,0 +1,13 @@ + + + + + Doc Viewer + + + + + +
+ + diff --git a/apps/doc-viewer/src/main.tsx b/apps/doc-viewer/src/main.tsx new file mode 100644 index 0000000000..7b8dd49ee6 --- /dev/null +++ b/apps/doc-viewer/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './app/App' + +ReactDOM.render( + + + , + document.getElementById("root") +); diff --git a/apps/doc-viewer/tsconfig.app.json b/apps/doc-viewer/tsconfig.app.json new file mode 100644 index 0000000000..af84f21cfc --- /dev/null +++ b/apps/doc-viewer/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/apps/doc-viewer/tsconfig.json b/apps/doc-viewer/tsconfig.json new file mode 100644 index 0000000000..5aab5e7911 --- /dev/null +++ b/apps/doc-viewer/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/apps/doc-viewer/webpack.config.js b/apps/doc-viewer/webpack.config.js new file mode 100644 index 0000000000..7b3d088f05 --- /dev/null +++ b/apps/doc-viewer/webpack.config.js @@ -0,0 +1,82 @@ +const { composePlugins, withNx } = require('@nrwl/webpack') +const { withReact } = require('@nrwl/react') +const webpack = require('webpack') +const TerserPlugin = require("terser-webpack-plugin") +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") + +// Nx plugins for webpack. +module.exports = composePlugins(withNx(), withReact(), (config) => { + // Update the webpack config as needed here. + // e.g. `config.plugins.push(new MyPlugin())` + + // add fallback for node modules + config.resolve.fallback = { + ...config.resolve.fallback, + "crypto": require.resolve("crypto-browserify"), + "stream": require.resolve("stream-browserify"), + "path": require.resolve("path-browserify"), + "http": require.resolve("stream-http"), + "https": require.resolve("https-browserify"), + "constants": require.resolve("constants-browserify"), + "os": false, //require.resolve("os-browserify/browser"), + "timers": false, // require.resolve("timers-browserify"), + "zlib": require.resolve("browserify-zlib"), + "fs": false, + "module": false, + "tls": false, + "net": false, + "readline": false, + "child_process": false, + "buffer": require.resolve("buffer/"), + "vm": require.resolve('vm-browserify'), + } + + // add externals + config.externals = { + ...config.externals, + solc: 'solc', + } + + // add public path + config.output.publicPath = '/' + + // add copy & provide plugin + config.plugins.push( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + url: ['url', 'URL'], + process: 'process/browser', + }), + new webpack.DefinePlugin({ + + }), + ) + + // souce-map loader + config.module.rules.push({ + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" + }) + + config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings + + // set minimizer + config.optimization.minimizer = [ + new TerserPlugin({ + parallel: true, + terserOptions: { + ecma: 2015, + compress: false, + mangle: false, + format: { + comments: false, + }, + }, + extractComments: false, + }), + new CssMinimizerPlugin(), + ]; + + return config; +}) diff --git a/apps/etherscan/src/app/components/HeaderWithSettings.tsx b/apps/etherscan/src/app/components/HeaderWithSettings.tsx index 14e048d6cd..fb6fb0dab7 100644 --- a/apps/etherscan/src/app/components/HeaderWithSettings.tsx +++ b/apps/etherscan/src/app/components/HeaderWithSettings.tsx @@ -148,9 +148,7 @@ export const HeaderWithSettings: React.FC = ({
{title}
- -
diff --git a/apps/etherscan/src/app/views/VerifyView.tsx b/apps/etherscan/src/app/views/VerifyView.tsx index f13fea1929..cc71a46418 100644 --- a/apps/etherscan/src/app/views/VerifyView.tsx +++ b/apps/etherscan/src/app/views/VerifyView.tsx @@ -23,8 +23,6 @@ interface FormValues { contractAddress: string } - - export const VerifyView: React.FC = ({ apiKey, client, diff --git a/apps/remix-ide-e2e/src/commands/addFile.ts b/apps/remix-ide-e2e/src/commands/addFile.ts index 5463538fbb..08e9ff6d5b 100644 --- a/apps/remix-ide-e2e/src/commands/addFile.ts +++ b/apps/remix-ide-e2e/src/commands/addFile.ts @@ -54,9 +54,9 @@ function addFile(browser: NightwatchBrowser, name: string, content: NightwatchCo suppressNotFoundErrors: true, timeout: 60000 }) - .waitForElementPresent({ - selector: `//*[@data-id="tab-active" and contains(@title, "${name}")]`, - locateStrategy: 'xpath', + .waitForElementVisible({ + selector: `//*[@data-id='tab-active' and contains(@data-path, "${name}")]`, + locateStrategy: 'xpath' }) .setEditorValue(content.content) .getEditorValue((result) => { diff --git a/apps/remix-ide-e2e/src/helpers/init.ts b/apps/remix-ide-e2e/src/helpers/init.ts index 405aa3337e..572fa7a113 100644 --- a/apps/remix-ide-e2e/src/helpers/init.ts +++ b/apps/remix-ide-e2e/src/helpers/init.ts @@ -7,7 +7,7 @@ type LoadPlugin = { url: string } -export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, loadPlugin?: LoadPlugin): void { +export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, loadPlugin?: LoadPlugin, hideToolTips: boolean = true): void { browser .url(url || 'http://127.0.0.1:8080') //.switchBrowserTab(0) @@ -27,6 +27,30 @@ export default function (browser: NightwatchBrowser, callback: VoidFunction, url }) .verifyLoad() .perform(() => { + if (hideToolTips) { + browser.execute(function () { // hide tooltips + function addStyle(styleString) { + const style = document.createElement('style'); + style.textContent = styleString; + document.head.append(style); + } + + addStyle(` + .bs-popover-right { + display:none !important; + } + .bs-popover-top { + display:none !important; + } + .bs-popover-left { + display:none !important; + } + .bs-popover-bottom { + display:none !important; + } + `); + }) + } if (preloadPlugins) { initModules(browser, () => { browser diff --git a/apps/remix-ide-e2e/src/tests/ballot_0_4_14.test.ts b/apps/remix-ide-e2e/src/tests/ballot_0_4_14.test.ts index 9cfcf0ad65..80b0678c1f 100644 --- a/apps/remix-ide-e2e/src/tests/ballot_0_4_14.test.ts +++ b/apps/remix-ide-e2e/src/tests/ballot_0_4_14.test.ts @@ -11,7 +11,7 @@ const sources = [ module.exports = { '@disabled': true, before: function (browser: NightwatchBrowser, done: VoidFunction) { - init(browser, done, null, false) + init(browser, done) }, '@sources': function () { return sources @@ -25,9 +25,6 @@ module.exports = { .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('solidity') .setSolidityCompilerVersion('soljson-v0.4.14+commit.c2215d46.js') - .waitForElementVisible('[for="autoCompile"]') - .click('[for="autoCompile"]') - .verify.elementPresent('[data-id="compilerContainerAutoCompile"]:checked') }, 'Compile Ballot with compiler version 0.4.14': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts b/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts index ae1f51af60..fb03f0ce35 100644 --- a/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts +++ b/apps/remix-ide-e2e/src/tests/defaultLayout.test.ts @@ -66,7 +66,7 @@ module.exports = { .waitForElementVisible('div[data-id="filePanelFileExplorerTree"]') .click('[data-id="treeViewLitreeViewItemcontracts"]') .openFile('contracts/3_Ballot.sol') - .assert.containsText('div[title="default_workspace/contracts/3_Ballot.sol"]', '3_Ballot.sol') + .assert.containsText('div[data-path="default_workspace/contracts/3_Ballot.sol"]', '3_Ballot.sol') .end() } } diff --git a/apps/remix-ide-e2e/src/tests/file_decorator.test.ts b/apps/remix-ide-e2e/src/tests/file_decorator.test.ts index 3483853cad..e9bd9b529e 100644 --- a/apps/remix-ide-e2e/src/tests/file_decorator.test.ts +++ b/apps/remix-ide-e2e/src/tests/file_decorator.test.ts @@ -29,8 +29,8 @@ module.exports = { .waitForElementContainsText('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') .waitForElementContainsText('//*[@class="mainview"]//*[@data-id="file-decoration-custom-contracts/3_Ballot.sol"]', 'customtext') .moveToElement('//*[@id="fileExplorerView"]//*[@data-id="file-decoration-error-contracts/2_Owner.sol"]', 0, 0) - .waitForElementVisible('//*[@id="error-tooltip-contracts/2_Owner.sol"]') - .waitForElementContainsText('//*[@id="error-tooltip-contracts/2_Owner.sol"]', 'error on owner') + //.waitForElementVisible('//*[@id="error-tooltip-contracts/2_Owner.sol"]') + //.waitForElementContainsText('//*[@id="error-tooltip-contracts/2_Owner.sol"]', 'error on owner') }, 'clear ballot decorator': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/gist.test.ts b/apps/remix-ide-e2e/src/tests/gist.test.ts index f0a17f03ff..83d122c63e 100644 --- a/apps/remix-ide-e2e/src/tests/gist.test.ts +++ b/apps/remix-ide-e2e/src/tests/gist.test.ts @@ -73,7 +73,7 @@ module.exports = { browser.clickLaunchIcon('home') .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('filePanel') - .click('div[title="home"]') + .click('div[data-id="verticalIconsHomeIcon"]') .waitForElementVisible('button[data-id="landingPageImportFromGistButton"]') .pause(1000) .scrollAndClick('button[data-id="landingPageImportFromGistButton"]') @@ -143,8 +143,8 @@ module.exports = { .modalFooterOKClick('gisthandler') .pause(10000) .openFile(`gist-${testData.validGistId}/README.txt`) - .waitForElementVisible(`div[title='default_workspace/gist-${testData.validGistId}/README.txt']`) - .assert.containsText(`div[title='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt') + .waitForElementVisible(`div[data-path='default_workspace/gist-${testData.validGistId}/README.txt']`) + .assert.containsText(`div[data-path='default_workspace/gist-${testData.validGistId}/README.txt'] > span`, 'README.txt') .end() } } diff --git a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts index cf163dbb94..5799955a1e 100644 --- a/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts +++ b/apps/remix-ide-e2e/src/tests/importFromGithub.test.ts @@ -18,7 +18,7 @@ module.exports = { browser.clickLaunchIcon('home') .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('filePanel') - .click('div[title="home"]') + .click('div[data-id="verticalIconsHomeIcon"]') .waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]') .pause(1000) .click('button[data-id="landingPageImportFromGitHubButton"]') @@ -45,7 +45,7 @@ module.exports = { browser .waitForElementVisible('*[data-id="remixIdeIconPanel"]', 10000) .clickLaunchIcon('filePanel') - .click('div[title="home"]') + .click('div[data-id="verticalIconsHomeIcon"]') .waitForElementVisible('button[data-id="landingPageImportFromGitHubButton"]').pause(1000) .click('button[data-id="landingPageImportFromGitHubButton"]') .waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]') @@ -57,14 +57,17 @@ module.exports = { .waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]') .click('[data-id="homeTab-modal-footer-ok-react"]') .openFile('github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol') - .waitForElementVisible("div[title='default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol'") + .waitForElementVisible({ + selector: `//*[@data-id='tab-active' and @data-path="default_workspace/github/OpenZeppelin/openzeppelin-solidity/contracts/access/Roles.sol"]`, + locateStrategy: 'xpath' + }) .getEditorValue((content) => { browser.assert.ok(content.indexOf('library Roles {') !== -1, 'content does contain "library Roles {"') }) }, 'Import JSON From GitHub For Valid URL #group2': function (browser: NightwatchBrowser) { browser - .click('div[title="home"]') + .click('div[data-id="verticalIconsHomeIcon"]') .click('button[data-id="landingPageImportFromGitHubButton"]') .waitForElementVisible('input[data-id="homeTabModalDialogCustomPromptText"]').pause(1000) .execute(() => { @@ -75,7 +78,7 @@ module.exports = { .waitForElementVisible('*[data-id="homeTab-modal-footer-ok-react"]') .click('[data-id="homeTab-modal-footer-ok-react"]') .openFile('github/ethereum/remix-project/package.json') - .waitForElementVisible("div[title='default_workspace/github/ethereum/remix-project/package.json'") + .waitForElementVisible("div[data-path='default_workspace/github/ethereum/remix-project/package.json'") .getEditorValue((content) => { browser.assert.ok(content.indexOf('"name": "remix-project",') !== -1, 'content does contain "name": "remix-project"') }) diff --git a/apps/remix-ide-e2e/src/tests/recorder.test.ts b/apps/remix-ide-e2e/src/tests/recorder.test.ts index e8dd925de2..17363edbfc 100644 --- a/apps/remix-ide-e2e/src/tests/recorder.test.ts +++ b/apps/remix-ide-e2e/src/tests/recorder.test.ts @@ -95,12 +95,10 @@ module.exports = { .clickInstance(1) .pause(1000) .clickFunction('set2 - transact (not payable)', { types: 'uint256 _po', values: '10' }) - .testFunction('last', - { - status: 'true Transaction mined and execution succeed', - 'decoded input': { 'uint256 _po': '10' } - }) - + .testFunction('last', { + status: 'true Transaction mined and execution succeed', + 'decoded input': { 'uint256 _po': '10' } + }) }, 'Run with live "mode" #group2': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/search.test.ts b/apps/remix-ide-e2e/src/tests/search.test.ts index c91531d587..905aa839f6 100644 --- a/apps/remix-ide-e2e/src/tests/search.test.ts +++ b/apps/remix-ide-e2e/src/tests/search.test.ts @@ -150,14 +150,14 @@ module.exports = { browser.assert.ok(content.includes("123test' contract"), 'should replace text ok') }) .waitForElementVisible('*[data-id="undo-replace-README.txt"]') - .click('div[title="default_workspace/contracts/1_Storage.sol"]').pause(2000) + .click('div[data-path="default_workspace/contracts/1_Storage.sol"]').pause(2000) .waitForElementVisible('*[data-id="undo-replace-contracts/1_Storage.sol"]') .click('*[data-id="undo-replace-contracts/1_Storage.sol"]').pause(2000) .getEditorValue((content) => { browser.assert.ok(content.includes('contract Storage'), 'should undo text ok') browser.assert.ok(content.includes('title Storage'), 'should undo text ok') }) - .click('div[title="default_workspace/README.txt"]').pause(2000) + .click('div[data-path="default_workspace/README.txt"]').pause(2000) .waitForElementVisible('*[data-id="undo-replace-README.txt"]') .click('*[data-id="undo-replace-README.txt"]').pause(2000) .getEditorValue((content) => { diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 4dc1d89b6b..17e340cd2d 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -48,7 +48,7 @@ module.exports = { .waitForElementPresent('*[data-id="testTabGenerateTestFile"]') .click('*[data-id="testTabGenerateTestFile"]') .clickLaunchIcon('filePanel') - .waitForElementPresent('*[title="default_workspace/tests/simple_storage_test.sol"]') + .waitForElementPresent('*[data-path="default_workspace/tests/simple_storage_test.sol"]') .removeFile('tests/simple_storage_test.sol', 'default_workspace') }, @@ -106,7 +106,7 @@ module.exports = { 'Should fail on compilation, open file on error click, not disappear error #group2': function (browser: NightwatchBrowser) { browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .addFile('tests/compilationError_test.sol', sources[0]['compilationError_test.sol']) - .click('div[title="default_workspace/tests/compilationError_test.sol"] span[class="close-tabs"]') + .click('div[data-path="default_workspace/tests/compilationError_test.sol"] span[class="close-tabs"]') .clickLaunchIcon('solidityUnitTesting') .pause(2000) .click('*[data-id="testTabCheckAllTests"]') diff --git a/apps/remix-ide-e2e/src/tests/terminal.test.ts b/apps/remix-ide-e2e/src/tests/terminal.test.ts index 5e9b58d499..0e64b513da 100644 --- a/apps/remix-ide-e2e/src/tests/terminal.test.ts +++ b/apps/remix-ide-e2e/src/tests/terminal.test.ts @@ -317,6 +317,33 @@ module.exports = { .executeScriptInTerminal(`web3.eth.getCode('0x75F509A4eDA030470272DfBAf99A47D587E76709')`) // sepolia contract .waitForElementContainsText('*[data-id="terminalJournal"]', byteCodeInSepolia, 120000) }, + + 'Should run free function which logs in the terminal #group10': function (browser: NightwatchBrowser) { + const script = `import "hardhat/console.sol"; + + function runSomething () view { + console.log("test running free function"); + } + ` + browser + .addFile('test.sol', { content: script }) + .scrollToLine(3) + const path = "//*[@class='view-line' and contains(.,'runSomething') and contains(.,'view')]//span//span[contains(.,'(')]" + const pathRunFunction = `//li//*[@aria-label='Run the free function "runSomething" in the Remix VM']` + browser.waitForElementVisible('#editorView') + .useXpath() + .click(path) + .pause(3000) // the parser need to parse the code + .perform(function () { + const actions = this.actions({ async: true }); + return actions + .keyDown(this.Keys.SHIFT) + .keyDown(this.Keys.ALT) + .sendKeys('r') + }) + .useCss() + .waitForElementContainsText('*[data-id="terminalJournal"]', 'test running free function', 120000) + } } diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 8155a709ab..5c8abaf428 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -16,6 +16,7 @@ import { PermissionHandlerPlugin } from './app/plugins/permission-handler-plugin import { AstWalker } from '@remix-project/remix-astwalker' import { LinkLibraries, DeployLibraries, OpenZeppelinProxy } from '@remix-project/core-plugin' import { CodeParser } from './app/plugins/parser/code-parser' +import { SolidityScript } from './app/plugins/solidity-script' import { WalkthroughService } from './walkthroughService' @@ -246,7 +247,7 @@ class AppComponent { ) const codeParser = new CodeParser(new AstWalker()) - + const solidityScript = new SolidityScript() this.notification = new NotificationPlugin() @@ -298,7 +299,8 @@ class AppComponent { this.walkthroughService, search, solidityumlgen, - contractFlattener + contractFlattener, + solidityScript ]) // LAYOUT & SYSTEM VIEWS @@ -414,6 +416,7 @@ class AppComponent { await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormatter', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) + await this.appManager.activatePlugin(['solidity-script']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/panels/tab-proxy.js b/apps/remix-ide/src/app/panels/tab-proxy.js index d9b62256b4..984efe7534 100644 --- a/apps/remix-ide/src/app/panels/tab-proxy.js +++ b/apps/remix-ide/src/app/panels/tab-proxy.js @@ -146,7 +146,7 @@ export class TabProxy extends Plugin { } }) - this.on('manager', 'pluginActivated', ({ name, location, displayName, icon }) => { + this.on('manager', 'pluginActivated', ({ name, location, displayName, icon, description }) => { if (location === 'mainPanel') { this.addTab( name, @@ -160,7 +160,8 @@ export class TabProxy extends Plugin { this.emit('closeApp', name) this.call('manager', 'deactivatePlugin', name) }, - icon + icon, + description ) this.switchTab(name) } @@ -223,7 +224,7 @@ export class TabProxy extends Plugin { this.removeTab(oldName) } - addTab (name, title, switchTo, close, icon) { + addTab (name, title, switchTo, close, icon, description = '') { if (this._handlers[name]) return this.renderComponent() var slash = name.split('/') @@ -252,6 +253,7 @@ export class TabProxy extends Plugin { const index = this.loadedTabs.findIndex(({ title }) => title === formatPath.join('/')) if (index > -1) { const duplicateTabName = this.loadedTabs[index].name + const duplicateTabTooltip = this.loadedTabs[index].description const duplicateTabPath = duplicateTabName.split('/') const duplicateTabFormatPath = [...duplicateTabPath].reverse() const duplicateTabTitle = duplicateTabFormatPath.slice(0, titleLength).reverse().join('/') @@ -260,7 +262,7 @@ export class TabProxy extends Plugin { name: duplicateTabName, title: duplicateTabTitle, icon, - tooltip: duplicateTabName, + tooltip: duplicateTabTooltip || duplicateTabTitle, iconClass: getPathIcon(duplicateTabName) } } @@ -274,7 +276,7 @@ export class TabProxy extends Plugin { name, title, icon, - tooltip: name, + tooltip: description || title, iconClass: getPathIcon(name) }) } diff --git a/apps/remix-ide/src/app/plugins/solidity-script.tsx b/apps/remix-ide/src/app/plugins/solidity-script.tsx new file mode 100644 index 0000000000..ff8877efe0 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/solidity-script.tsx @@ -0,0 +1,102 @@ +import React from 'react' // eslint-disable-line +import { format } from 'util' +import { Plugin } from '@remixproject/engine' +import { compile } from '@remix-project/remix-solidity' +import { TransactionConfig } from 'web3-core' +const _paq = window._paq = window._paq || [] //eslint-disable-line + +const profile = { + name: 'solidity-script', + displayName: 'solidity-script', + description: 'solidity-script', + methods: ['execute'] +} + +export class SolidityScript extends Plugin { + constructor () { + super(profile) + } + + async execute (path: string, functionName: string = 'run') { + _paq.push(['trackEvent', 'SolidityScript', 'execute', 'script']) + this.call('terminal', 'log', `running free function ${functionName} from ${path}...`) + let content = await this.call('fileManager', 'readFile', path) + const params = await this.call('solidity', 'getCompilerParameters') + + + content = ` + import "${path}"; + + contract SolidityScript { + constructor () {} + + function remixRun () public { + ${functionName}(); + } + }` + const targets = { 'script.sol': { content } } + + // compile + const compilation = await compile(targets, params, async (url, cb) => { + await this.call('contentImport', 'resolveAndSave', url).then((result) => cb(null, result)).catch((error) => cb(error.message)) + }) + + if (compilation.data.error) { + this.call('terminal', 'log', compilation.data.error.formattedMessage) + } + if (compilation.data.errors && compilation.data.errors.length > 0) { + compilation.data.errors.map((error) => { + this.call('terminal', 'log', error.formattedMessage) + }) + } + + // get the contract + const contract = compilation.getContract('SolidityScript') + if (!contract) { + console.log('compilation failed') + return + } + const bytecode = '0x' + contract.object.evm.bytecode.object + const web3 = await this.call('blockchain', 'web3VM') + const accounts = await this.call('blockchain', 'getAccounts') + if (!accounts || accounts.length === 0) { + throw new Error('no account available') + } + + // deploy the contract + let tx: TransactionConfig = { + from: accounts[0], + data: bytecode + } + const receipt = await web3.eth.sendTransaction(tx) + tx = { + from: accounts[0], + to: receipt.contractAddress, + data: '0x69d4394b' // function remixRun() public + } + const receiptCall = await web3.eth.sendTransaction(tx) + + const hhlogs = await web3.eth.getHHLogsForTx(receiptCall.transactionHash) + + if (hhlogs && hhlogs.length) { + const finalLogs =
console.log:
+ { + hhlogs.map((log) => { + let formattedLog + // Hardhat implements the same formatting options that can be found in Node.js' console.log, + // which in turn uses util.format: https://nodejs.org/dist/latest-v12.x/docs/api/util.html#util_util_format_format_args + // For example: console.log("Name: %s, Age: %d", remix, 6) will log 'Name: remix, Age: 6' + // We check first arg to determine if 'util.format' is needed + if (typeof log[0] === 'string' && (log[0].includes('%s') || log[0].includes('%d'))) { + formattedLog = format(log[0], ...log.slice(1)) + } else { + formattedLog = log.join(' ') + } + return
{formattedLog}
+ })} +
+ _paq.push(['trackEvent', 'udapp', 'hardhat', 'console.log']) + this.call('terminal', 'logHtml', finalLogs) + } + } +} diff --git a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx index 8c9341f6d5..80a767f083 100644 --- a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx +++ b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx @@ -18,7 +18,7 @@ const _paq = window._paq = window._paq || [] const profile = { name: 'solidityumlgen', displayName: 'Solidity UML Generator', - description: 'Generate UML diagram in svg format from last compiled contract', + description: 'Generates UML diagram in svg format from last compiled contract', location: 'mainPanel', methods: ['showUmlDiagram', 'generateUml', 'generateCustomAction'], events: [], diff --git a/apps/remix-ide/src/app/providers/injected-provider.tsx b/apps/remix-ide/src/app/providers/injected-provider.tsx index 4de903c0c8..9a1f915128 100644 --- a/apps/remix-ide/src/app/providers/injected-provider.tsx +++ b/apps/remix-ide/src/app/providers/injected-provider.tsx @@ -99,7 +99,7 @@ export abstract class InjectedProvider extends Plugin implements IProvider { resolve({ jsonrpc: '2.0', error: 'no return data provided', id: data.id }) } } catch (error) { - resolve({ jsonrpc: '2.0', error: error.message, id: data.id }) + resolve({ jsonrpc: '2.0', error: error.data && error.data.message ? error.data.message : error.message, id: data.id }) } } } diff --git a/apps/remix-ide/src/app/tabs/compile-tab.js b/apps/remix-ide/src/app/tabs/compile-tab.js index a43e71ca4b..d7fdc499e9 100644 --- a/apps/remix-ide/src/app/tabs/compile-tab.js +++ b/apps/remix-ide/src/app/tabs/compile-tab.js @@ -21,7 +21,7 @@ const profile = { documentation: 'https://remix-ide.readthedocs.io/en/latest/compile.html', version: packageJson.version, maintainedBy: 'Remix', - methods: ['getCompilationResult', 'compile', 'compileWithParameters', 'setCompilerConfig', 'compileFile', 'getCompilerState'] + methods: ['getCompilationResult', 'compile', 'compileWithParameters', 'setCompilerConfig', 'compileFile', 'getCompilerState', 'getCompilerParameters', 'getCompiler'] } // EditorApi: @@ -133,8 +133,13 @@ class CompileTab extends CompilerApiMixin(ViewPlugin) { // implements ICompilerA } } + getCompiler () { + return this.compileTabLogic.compiler + } + getCompilerParameters () { const params = this.queryParams.get() + params.evmVersion = params.evmVersion === 'null' || params.evmVersion === 'undefined' ? null : params.evmVersion params.optimize = (params.optimize === 'false' || params.optimize === null || params.optimize === undefined) ? false : params.optimize params.optimize = params.optimize === 'true' ? true : params.optimize return params diff --git a/apps/remix-ide/src/app/tabs/locales/en/udapp.json b/apps/remix-ide/src/app/tabs/locales/en/udapp.json index c2a864e32b..0c4634c529 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/udapp.json +++ b/apps/remix-ide/src/app/tabs/locales/en/udapp.json @@ -5,8 +5,8 @@ "udapp.value": "Value", "udapp.contract": "Contract", "udapp.compiledBy": "Compiled by {compilerName}", - "udapp.infoSyncCompiledContractTooltip": "Click here to import contracts compiled from an external framework.{br}This action is enabled when Remix is connected to an external{br} framework (hardhat, truffle, foundry) through remixd.", - "udapp.remixIpfsUdappTooltip": "Publishing the source code and metadata to IPFS facilitates{br} source code verification using Sourcify and will greatly foster{br} contract adoption (auditing, debugging, calling it, etc...)", + "udapp.infoSyncCompiledContractTooltip": "Click here to import contracts compiled from an external framework.This action is enabled when Remix is connected to an external framework (hardhat, truffle, foundry) through remixd.", + "udapp.remixIpfsUdappTooltip": "Publishing the source code and metadata to IPFS facilitates source code verification using Sourcify and will greatly foster contract adoption (auditing, debugging, calling it, etc...)", "udapp.signAMessage": "Sign a message", "udapp.enterAMessageToSign": "Enter a message to sign", "udapp.hash": "hash", @@ -19,15 +19,15 @@ "udapp.or": "or", "udapp.atAddress": "At Address", "udapp.atAddressOptionsTitle1": "address of contract", - "udapp.atAddressOptionsTitle2": "Interact with the deployed contract - requires the .abi file or {br} compiled .sol file to be selected in the editor {br}(with the same compiler configuration)", + "udapp.atAddressOptionsTitle2": "Interact with the deployed contract - requires the .abi file or compiled .sol file to be selected in the editor (with the same compiler configuration)", "udapp.atAddressOptionsTitle3": "Compile a *.sol file or select a *.abi file.", - "udapp.atAddressOptionsTitle4": "To interact with a deployed contract, either{br} enter its address and compile its source *.sol file {br}(with the same compiler settings) or select its .abi file in the editor. ", + "udapp.atAddressOptionsTitle4": "To interact with a deployed contract, either enter its address and compile its source *.sol file (with the same compiler settings) or select its .abi file in the editor. ", "udapp.contractOptionsTitle1": "Please compile *.sol file to deploy or access a contract", "udapp.contractOptionsTitle2": "Select a compiled contract to deploy or to use with At Address.", "udapp.contractOptionsTitle3": "Select and compile *.sol file to deploy or access a contract.", - "udapp.contractOptionsTitle4": "When there is a compiled .sol file, choose the {br} contract to deploy or to use with AtAddress.'", - "udapp.checkSumWarning": "It seems you are not using a checksumed address.{br}A checksummed address is an address that contains uppercase letters, as specified in {a}.{br}Checksummed addresses are meant to help prevent users from sending transactions to the wrong address.", - "udapp.isOverSizePrompt": "Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails. {br}More info: {a}", + "udapp.contractOptionsTitle4": "When there is a compiled .sol file, choose the contract to deploy or to use with AtAddress.'", + "udapp.checkSumWarning": "It seems you are not using a checksumed address.A checksummed address is an address that contains uppercase letters, as specified in {a}.Checksummed addresses are meant to help prevent users from sending transactions to the wrong address.", + "udapp.isOverSizePrompt": "Contract creation initialization returns data with length of more than 24576 bytes. The deployment will likely fails. More info: {a}", "udapp.thisContractMayBeAbstract": "This contract may be abstract, it may not implement an abstract parent's methods completely or it may not invoke an inherited contract's constructor correctly.", "udapp.noCompiledContracts": "No compiled contracts", "udapp.addressOfContract": "Address of contract", @@ -40,8 +40,8 @@ "udapp.transactionSaveTooltip1": "No transactions to save", "udapp.transactionSaveTooltip2": "Save {count} transaction as scenario file", "udapp.transactionSaveTooltip3": "Save {count} transactions as scenario file", - "udapp.infoRecorderTooltip": "Save transactions (deployed contracts and function executions) {br}and replay them in another environment e.g Transactions created {br}in Remix VM can be replayed in the Injected Provider.", - "udapp.livemodeRecorderTooltip": "If contracts are updated after recording transactions,{br} checking this box will run recorded transactions {br}with the latest copy of the compiled contracts", + "udapp.infoRecorderTooltip": "Save transactions (deployed contracts and function executions) and replay them in another environment e.g Transactions created in Remix VM can be replayed in the Injected Provider.", + "udapp.livemodeRecorderTooltip": "If contracts are updated after recording transactions, checking this box will run recorded transactions with the latest copy of the compiled contracts", "udapp.livemodeRecorderLabel": "Run transactions using the latest compilation result", "udapp.runRecorderTooltip": "Run transaction(s) from the current scenario file", "udapp.save": "Save", diff --git a/apps/remix-ide/src/app/ui/landing-page/landing-page.js b/apps/remix-ide/src/app/ui/landing-page/landing-page.js index 81444c5c1e..b07e2c9432 100644 --- a/apps/remix-ide/src/app/ui/landing-page/landing-page.js +++ b/apps/remix-ide/src/app/ui/landing-page/landing-page.js @@ -9,7 +9,7 @@ const profile = { displayName: 'Home', methods: [], events: [], - description: 'Remix home tab ', + description: 'Remix Home', icon: 'assets/img/home.webp', location: 'mainPanel', version: packageJson.version diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css index 17bdde289a..9542aa513a 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-cerulean.min.css @@ -5250,7 +5250,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after { bottom:1px; border-width:.5rem .5rem 0; - border-top-color:#fff + border-top-color: var(--secondary) } .bs-popover-auto[x-placement^=right],.bs-popover-right { margin-left:.5rem @@ -5269,7 +5269,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after { left:1px; border-width:.5rem .5rem .5rem 0; - border-right-color:#fff + border-right-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom],.bs-popover-bottom { margin-top:.5rem @@ -5285,7 +5285,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after { top:1px; border-width:0 .5rem .5rem .5rem; - border-bottom-color:#fff + border-bottom-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before { position:absolute; @@ -5314,7 +5314,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after { right:1px; border-width:.5rem 0 .5rem .5rem; - border-left-color:#fff + border-left-color: var(--secondary) } .popover-header { padding:.5rem .75rem; diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css index 918bbb71b7..5e813c6d42 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-cyborg.min.css @@ -5252,7 +5252,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after { bottom:1px; border-width:.5rem .5rem 0; - border-top-color:#282828 + border-top-color: var(--secondary); } .bs-popover-auto[x-placement^=right],.bs-popover-right { margin-left:.5rem @@ -5271,7 +5271,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after { left:1px; border-width:.5rem .5rem .5rem 0; - border-right-color:#282828 + border-right-color: var(--secondary); } .bs-popover-auto[x-placement^=bottom],.bs-popover-bottom { margin-top:.5rem @@ -5287,7 +5287,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after { top:1px; border-width:0 .5rem .5rem .5rem; - border-bottom-color:#282828 + border-bottom-color: var(--secondary); } .bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before { position:absolute; @@ -5316,7 +5316,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after { right:1px; border-width:.5rem 0 .5rem .5rem; - border-left-color:#282828 + border-left-color: var(--secondary); } .popover-header { padding:.5rem .75rem; diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css index 5940867b25..648375ddc8 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-flatly.min.css @@ -4253,7 +4253,7 @@ a.close.disabled { bottom:0; border-width:.5rem .5rem 0; border-top-color:rgba(0,0,0,.25) } .bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after { - bottom:1px; border-width:.5rem .5rem 0; border-top-color:#fff + bottom:1px; border-width:.5rem .5rem 0; border-top-color: var(--secondary) } .bs-popover-auto[x-placement^=right],.bs-popover-right { margin-left:.5rem @@ -4265,7 +4265,7 @@ a.close.disabled { left:0; border-width:.5rem .5rem .5rem 0; border-right-color:rgba(0,0,0,.25) } .bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after { - left:1px; border-width:.5rem .5rem .5rem 0; border-right-color:#fff + left:1px; border-width:.5rem .5rem .5rem 0; border-right-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom],.bs-popover-bottom { margin-top:.5rem @@ -4277,7 +4277,7 @@ a.close.disabled { top:0; border-width:0 .5rem .5rem .5rem; border-bottom-color:rgba(0,0,0,.25) } .bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after { - top:1px; border-width:0 .5rem .5rem .5rem; border-bottom-color:#fff + top:1px; border-width:0 .5rem .5rem .5rem; border-bottom-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before { position:absolute; top:0; left:50%; display:block; width:1rem; margin-left:-.5rem; content:""; border-bottom:1px solid #f7f7f7 @@ -4292,7 +4292,7 @@ a.close.disabled { right:0; border-width:.5rem 0 .5rem .5rem; border-left-color:rgba(0,0,0,.25) } .bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after { - right:1px; border-width:.5rem 0 .5rem .5rem; border-left-color:#fff + right:1px; border-width:.5rem 0 .5rem .5rem; border-left-color: var(--secondary) } .popover-header { padding:.5rem .75rem; margin-bottom:0; font-size:.9375rem; background-color:#f7f7f7; border-bottom:1px solid #ebebeb; border-top-left-radius:calc(.3rem - 1px); border-top-right-radius:calc(.3rem - 1px) diff --git a/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css b/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css index b8e90b0107..4c792a1245 100644 --- a/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css +++ b/apps/remix-ide/src/assets/css/themes/bootstrap-spacelab.min.css @@ -5253,7 +5253,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after { bottom:1px; border-width:.5rem .5rem 0; - border-top-color:#fff + border-top-color: var(--secondary) } .bs-popover-auto[x-placement^=right],.bs-popover-right { margin-left:.5rem @@ -5272,7 +5272,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after { left:1px; border-width:.5rem .5rem .5rem 0; - border-right-color:#fff + border-right-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom],.bs-popover-bottom { margin-top:.5rem @@ -5288,7 +5288,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after { top:1px; border-width:0 .5rem .5rem .5rem; - border-bottom-color:#fff + border-bottom-color: var(--secondary) } .bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before { position:absolute; @@ -5317,7 +5317,7 @@ a.close.disabled { .bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after { right:1px; border-width:.5rem 0 .5rem .5rem; - border-left-color:#fff + border-left-color: var(--secondary) } .popover-header { padding:.5rem .75rem; @@ -5486,7 +5486,6 @@ a.close.disabled { .carousel-indicators li { transition:none } - } .carousel-indicators .active { opacity:1 diff --git a/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css b/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css index 36381f4163..61dbb692ce 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css +++ b/apps/remix-ide/src/assets/css/themes/remix-black_undtds.css @@ -5310,7 +5310,7 @@ a.close.disabled { .bs-popover-top > .arrow::after { bottom: 1px; border-width: 5px 5px 0; - border-top-color: #000; + border-top-color: var(--secondary); } .bs-popover-auto[x-placement^="right"], .bs-popover-right { @@ -5333,7 +5333,7 @@ a.close.disabled { .bs-popover-right > .arrow::after { left: 1px; border-width: 5px 5px 5px 0; - border-right-color: #000; + border-right-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"], .bs-popover-bottom { @@ -5353,7 +5353,7 @@ a.close.disabled { .bs-popover-bottom > .arrow::after { top: 1px; border-width: 0 5px 5px 5px; - border-bottom-color: #000; + border-bottom-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"] .popover-header::before, .bs-popover-bottom .popover-header::before { @@ -5387,7 +5387,7 @@ a.close.disabled { .bs-popover-left > .arrow::after { right: 1px; border-width: 5px 0 5px 5px; - border-left-color: #000; + border-left-color: var(--secondary); } .popover-header { padding: 8px 14px; diff --git a/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css b/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css index 14764f2994..fd25e0642d 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css +++ b/apps/remix-ide/src/assets/css/themes/remix-candy_ikhg4m.css @@ -5773,7 +5773,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="top"] > .arrow::after { bottom: 1px; border-width: 0.5rem 0.5rem 0; - border-top-color: #fff; + border-top-color: var(--secondary); } .bs-popover-right, @@ -5797,7 +5797,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="right"] > .arrow::after { left: 1px; border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: #fff; + border-right-color: var(--secondary); } .bs-popover-bottom, @@ -5818,7 +5818,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="bottom"] > .arrow::after { top: 1px; border-width: 0 0.5rem 0.5rem 0.5rem; - border-bottom-color: #fff; + border-bottom-color: var(--secondary); } .bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { @@ -5853,7 +5853,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="left"] > .arrow::after { right: 1px; border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: #fff; + border-left-color: var(--secondary); } .popover-header { diff --git a/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css b/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css index a38163a92c..c53a7e7ac3 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css +++ b/apps/remix-ide/src/assets/css/themes/remix-dark_tvx1s2.css @@ -5312,7 +5312,7 @@ a.close.disabled { .bs-popover-top > .arrow::after { bottom: 1px; border-width: 5px 5px 0; - border-top-color: #000; + border-top-color: var(--secondary); } .bs-popover-auto[x-placement^="right"], .bs-popover-right { @@ -5335,7 +5335,7 @@ a.close.disabled { .bs-popover-right > .arrow::after { left: 1px; border-width: 5px 5px 5px 0; - border-right-color: #000; + border-right-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"], .bs-popover-bottom { @@ -5355,7 +5355,7 @@ a.close.disabled { .bs-popover-bottom > .arrow::after { top: 1px; border-width: 0 5px 5px 5px; - border-bottom-color: #000; + border-bottom-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"] .popover-header::before, .bs-popover-bottom .popover-header::before { @@ -5389,7 +5389,7 @@ a.close.disabled { .bs-popover-left > .arrow::after { right: 1px; border-width: 5px 0 5px 5px; - border-left-color: #000; + border-left-color: var(--secondary); } .popover-header { padding: 8px 14px; diff --git a/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css b/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css index 3db8d66dec..58f3aa64fc 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css +++ b/apps/remix-ide/src/assets/css/themes/remix-hacker_owl.css @@ -5323,7 +5323,7 @@ a.close.disabled { .bs-popover-top > .arrow::after { bottom: 1px; border-width: 5px 5px 0; - border-top-color: #000; + border-top-color: var(--secondary); } .bs-popover-auto[x-placement^="right"], .bs-popover-right { @@ -5346,7 +5346,7 @@ a.close.disabled { .bs-popover-right > .arrow::after { left: 1px; border-width: 5px 5px 5px 0; - border-right-color: #000; + border-right-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"], .bs-popover-bottom { @@ -5366,7 +5366,7 @@ a.close.disabled { .bs-popover-bottom > .arrow::after { top: 1px; border-width: 0 5px 5px 5px; - border-bottom-color: #000; + border-bottom-color: var(--secondary); } .bs-popover-auto[x-placement^="bottom"] .popover-header::before, .bs-popover-bottom .popover-header::before { @@ -5400,7 +5400,7 @@ a.close.disabled { .bs-popover-left > .arrow::after { right: 1px; border-width: 5px 0 5px 5px; - border-left-color: #000; + border-left-color: var(--secondary); } .popover-header { padding: 8px 14px; diff --git a/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css b/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css index 66b438c4d6..800b7dc5a8 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css +++ b/apps/remix-ide/src/assets/css/themes/remix-light_powaqg.css @@ -5769,7 +5769,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="top"] > .arrow::after { bottom: 1px; border-width: 0.5rem 0.5rem 0; - border-top-color: #fff; + border-top-color: var(--secondary); } .bs-popover-right, @@ -5793,7 +5793,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="right"] > .arrow::after { left: 1px; border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: #fff; + border-right-color: var(--secondary); } .bs-popover-bottom, @@ -5814,7 +5814,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="bottom"] > .arrow::after { top: 1px; border-width: 0 0.5rem 0.5rem 0.5rem; - border-bottom-color: #fff; + border-bottom-color: var(--secondary); } .bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { @@ -5849,7 +5849,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="left"] > .arrow::after { right: 1px; border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: #fff; + border-left-color: var(--secondary); } .popover-header { diff --git a/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css b/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css index c2501b8496..de6be478db 100644 --- a/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css +++ b/apps/remix-ide/src/assets/css/themes/remix-midcentury_hrzph3.css @@ -5775,7 +5775,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="top"] > .arrow::after { bottom: 1px; border-width: 0.5rem 0.5rem 0; - border-top-color: #eeede9; + border-top-color: var(--secondary); } .bs-popover-right, @@ -5799,7 +5799,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="right"] > .arrow::after { left: 1px; border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: #eeede9; + border-right-color: var(--secondary); } .bs-popover-bottom, @@ -5820,7 +5820,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="bottom"] > .arrow::after { top: 1px; border-width: 0 0.5rem 0.5rem 0.5rem; - border-bottom-color: #eeede9; + border-bottom-color: var(--secondary); } .bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before { @@ -5855,7 +5855,7 @@ a.close.disabled { .bs-popover-auto[x-placement^="left"] > .arrow::after { right: 1px; border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: #eeede9; + border-left-color: var(--secondary); } .popover-header { @@ -6141,7 +6141,7 @@ button.bg-primary:focus { } .bg-secondary { - background-color: #EDC2A1 !important; + background-color: #f1f1f100 !important; } a.bg-secondary:hover, diff --git a/apps/remix-ide/src/blockchain/blockchain.js b/apps/remix-ide/src/blockchain/blockchain.js index 49367096ab..d3ee2cd90b 100644 --- a/apps/remix-ide/src/blockchain/blockchain.js +++ b/apps/remix-ide/src/blockchain/blockchain.js @@ -23,7 +23,7 @@ const profile = { name: 'blockchain', displayName: 'Blockchain', description: 'Blockchain - Logic', - methods: ['getCode', 'getTransactionReceipt', 'addProvider', 'removeProvider', 'getCurrentFork', 'web3VM', 'getProvider'], + methods: ['getCode', 'getTransactionReceipt', 'addProvider', 'removeProvider', 'getCurrentFork', 'getAccounts', 'web3VM', 'getProvider'], version: packageJson.version } diff --git a/apps/remix-ide/src/index.html b/apps/remix-ide/src/index.html index a7a99db6c2..ee0f9f81a8 100644 --- a/apps/remix-ide/src/index.html +++ b/apps/remix-ide/src/index.html @@ -1,6 +1,6 @@ - + diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 2a4d7f4671..59a13e819b 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -11,7 +11,7 @@ const requiredModules = [ // services + layout views + system views 'filePanel', 'terminal', 'settings', 'pluginManager', 'tabs', 'udapp', 'dGitProvider', 'solidity', 'solidity-logic', 'gistHandler', 'layout', 'notification', 'permissionhandler', 'walkthrough', 'storage', 'restorebackupzip', 'link-libraries', 'deploy-libraries', 'openzeppelin-proxy', 'hardhat-provider', 'ganache-provider', 'foundry-provider', 'basic-http-provider', 'injected', 'injected-trustwallet', 'injected-optimism-provider', 'injected-arbitrum-one-provider', 'vm-custom-fork', 'vm-goerli-fork', 'vm-mainnet-fork', 'vm-sepolia-fork', 'vm-merge', 'vm-london', 'vm-berlin', - 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener'] + 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormatter', 'solidityumlgen', 'contractflattener', 'doc-gen', 'doc-viewer', 'solidity-script'] // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) const dependentModules = ['foundry', 'hardhat', 'truffle', 'slither'] diff --git a/libs/remix-solidity/src/compiler/compiler-helpers.ts b/libs/remix-solidity/src/compiler/compiler-helpers.ts index b7f5b7a0d4..3097eeab28 100644 --- a/libs/remix-solidity/src/compiler/compiler-helpers.ts +++ b/libs/remix-solidity/src/compiler/compiler-helpers.ts @@ -3,21 +3,17 @@ import { canUseWorker, urlFromVersion } from './compiler-utils' import { CompilerAbstract } from './compiler-abstract' import { Compiler } from './compiler' -export const compile = async (compilationTargets, settings, contentResolverCallback) => { - const res = await (() => { - return new Promise((resolve, reject) => { - console.log('compilationTargets', compilationTargets) - const compiler = new Compiler(contentResolverCallback) - compiler.set('evmVersion', settings.evmVersion) - compiler.set('optimize', settings.optimize) - compiler.set('language', settings.language) - compiler.set('runs', settings.runs) - compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version)) - compiler.event.register('compilationFinished', (success, compilationData, source, input, version) => { - resolve(new CompilerAbstract(settings.version, compilationData, source, input)) - }) - compiler.event.register('compilerLoaded', _ => compiler.compile(compilationTargets, '')) +export const compile = (compilationTargets, settings, contentResolverCallback): Promise => { + return new Promise((resolve, reject) => { + const compiler = new Compiler(contentResolverCallback) + compiler.set('evmVersion', settings.evmVersion) + compiler.set('optimize', settings.optimize) + compiler.set('language', settings.language) + compiler.set('runs', settings.runs) + compiler.loadVersion(canUseWorker(settings.version), urlFromVersion(settings.version)) + compiler.event.register('compilationFinished', (success, compilationData, source, input, version) => { + resolve(new CompilerAbstract(settings.version, compilationData, source, input)) }) - })() - return res + compiler.event.register('compilerLoaded', _ => compiler.compile(compilationTargets, '')) + }) } diff --git a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx index d928c5f963..6c8be869d4 100644 --- a/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/remix-app.tsx @@ -9,6 +9,7 @@ import AppDialogs from './components/modals/dialogs' import DialogViewPlugin from './components/modals/dialogViewPlugin' import { AppContext } from './context/context' import { IntlProvider } from 'react-intl' +import { CustomTooltip } from '@remix-ui/helper'; interface IRemixAppUi { app: any @@ -87,8 +88,15 @@ const RemixApp = (props: IRemixAppUi) => {
{props.app.menuicons.render()}
{props.app.sidePanel.render()}
-
+
+ +
+
{props.app.hiddenPanel.render()}
diff --git a/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css index d177ce8bcf..70686d632f 100644 --- a/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css +++ b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css @@ -34,7 +34,6 @@ pre { width : 320px; transition : width 0.25s; } - .highlightcode { position : absolute; z-index : 20; @@ -54,19 +53,24 @@ pre { height : 200px; } .centered svg path { - fill: var(--secondary); + fill : var(--secondary); } .centered svg polygon { - fill : var(--secondary); + fill : var(--secondary); } .onboarding { - color : var(--text-info); - background-color : var(--info); + color : var(--text-info); + background-color : var(--info); } .matomoBtn { width : 100px; } - .splash { - text-align: center; + text-align : center; +} +.remix-ui-tabs_end { + height : 2.15rem; + width : 4rem; + right : -10px; + filter : opacity(0.5); } \ No newline at end of file diff --git a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx index a2fd923a72..c6497de3b1 100644 --- a/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx +++ b/libs/remix-ui/checkbox/src/lib/remix-ui-checkbox.tsx @@ -38,11 +38,11 @@ export const RemixUiCheckbox = ({ }: RemixUiCheckboxProps) => { const childJSXWithTooltip = ( - +
-
- ) +
+ ) const childJSX = (
{
- } - placement="top-start" - > - {customJSX} - + } + placement="top-start" + > + {customJSX} +
{ state.isLocalNodeUsed &&
- { - setState(prevState => { - return { ...prevState, opt: { ...prevState.opt, debugWithLocalNode: checked } } - }) - }} type="checkbox" title="Force the debugger to use the current local node" /> + + { + setState(prevState => { + return { ...prevState, opt: { ...prevState.opt, debugWithLocalNode: checked } } + }) + }} + type="checkbox" + /> +
} diff --git a/libs/remix-ui/editor/src/lib/helpers/retrieveNodesAtPosition.ts b/libs/remix-ui/editor/src/lib/helpers/retrieveNodesAtPosition.ts new file mode 100644 index 0000000000..3e21a6804f --- /dev/null +++ b/libs/remix-ui/editor/src/lib/helpers/retrieveNodesAtPosition.ts @@ -0,0 +1,15 @@ +import { EditorAPIType, PluginType } from "../remix-ui-editor" + +export const retrieveNodesAtPosition = async (editorAPI: EditorAPIType, plugin: PluginType) => { + const cursorPosition = editorAPI.getCursorPosition() + let nodesAtPosition = await plugin.call('codeParser', 'nodesAtPosition', cursorPosition) + // if no nodes exits at position, try to get the block of which the position is in + const block = await plugin.call('codeParser', 'getANTLRBlockAtPosition', cursorPosition, null) + + if (!nodesAtPosition.length) { + if (block) { + nodesAtPosition = await plugin.call('codeParser', 'nodesAtPosition', block.start) + } + } + return { nodesAtPosition, block } +} \ No newline at end of file diff --git a/libs/remix-ui/editor/src/lib/providers/completionProvider.ts b/libs/remix-ui/editor/src/lib/providers/completionProvider.ts index 5e28da8d44..305095cc76 100644 --- a/libs/remix-ui/editor/src/lib/providers/completionProvider.ts +++ b/libs/remix-ui/editor/src/lib/providers/completionProvider.ts @@ -3,6 +3,7 @@ import { isArray } from "lodash" import { EditorUIProps } from "../remix-ui-editor" import { GeCompletionUnits, GetCompletionKeywords, getCompletionSnippets, GetCompletionTypes, getContextualAutoCompleteBTypeName, getContextualAutoCompleteByGlobalVariable, GetGlobalFunctions, GetGlobalVariable, GetImports } from "./completion/completionGlobals" import { monacoTypes } from '@remix-ui/editor'; +import { retrieveNodesAtPosition } from "../helpers/retrieveNodesAtPosition"; export class RemixCompletionProvider implements monacoTypes.languages.CompletionItemProvider { props: EditorUIProps @@ -251,17 +252,8 @@ export class RemixCompletionProvider implements monacoTypes.languages.Completion private getContractCompletions = async () => { let nodes: any[] = [] - const cursorPosition = this.props.editorAPI.getCursorPosition() - let nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', cursorPosition) - // if no nodes exits at position, try to get the block of which the position is in - const block = await this.props.plugin.call('codeParser', 'getANTLRBlockAtPosition', cursorPosition, null) + const { nodesAtPosition, block } = await retrieveNodesAtPosition(this.props.editorAPI, this.props.plugin) const fileNodes = await this.props.plugin.call('codeParser', 'getCurrentFileNodes') - - if (!nodesAtPosition.length) { - if (block) { - nodesAtPosition = await this.props.plugin.call('codeParser', 'nodesAtPosition', block.start) - } - } // find the contract and get the nodes of the contract and the base contracts and imports if (isArray(nodesAtPosition) && nodesAtPosition.length) { let contractNode: any = {} diff --git a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx index 0097e05f66..eabeb3557d 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line - +import { isArray } from "lodash" import Editor, { loader, Monaco } from '@monaco-editor/react' import { AlertModal } from '@remix-ui/app' import { reducerActions, reducerListener, initialState } from './actions/editor' @@ -8,15 +8,14 @@ import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo' import { zokratesTokensProvider, zokratesLanguageConfig } from './syntaxes/zokrates' import { moveTokenProvider, moveLanguageConfig } from './syntaxes/move' import { monacoTypes } from '@remix-ui/editor'; - -import './remix-ui-editor.css' import { loadTypes } from './web-types' - +import { retrieveNodesAtPosition } from './helpers/retrieveNodesAtPosition' import { RemixHoverProvider } from './providers/hoverProvider' import { RemixReferenceProvider } from './providers/referenceProvider' import { RemixCompletionProvider } from './providers/completionProvider' import { RemixHighLightProvider } from './providers/highlightProvider' import { RemixDefinitionProvider } from './providers/definitionProvider' +import './remix-ui-editor.css' enum MarkerSeverity { @@ -93,6 +92,24 @@ export type DecorationsReturn = { registeredDecorations?: Array } +export type PluginType = { + on: (plugin: string, event: string, listener: any) => void + call: (plugin: string, method: string, arg1?: any, arg2?: any, arg3?: any, arg4?: any) => any +} + +export type EditorAPIType = { + findMatches: (uri: string, value: string) => any + getFontSize: () => number, + getValue: (uri: string) => string + getCursorPosition: (offset?: boolean) => number | monacoTypes.IPosition + getHoverPosition: (position: monacoTypes.IPosition) => number + addDecoration: (marker: sourceMarker, filePath: string, typeOfDecoration: string) => DecorationsReturn + clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn + keepDecorationsFor: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn + addErrorMarker: (errors: errorMarker[], from: string) => void + clearErrorMarkers: (sources: string[] | {[fileName: string]: any}, from: string) => void +} + /* eslint-disable-next-line */ export interface EditorUIProps { contextualListener: any @@ -105,22 +122,8 @@ export interface EditorUIProps { onDidChangeContent: (file: string) => void onEditorMounted: () => void } - plugin: { - on: (plugin: string, event: string, listener: any) => void - call: (plugin: string, method: string, arg1?: any, arg2?: any, arg3?: any, arg4?: any) => any - } - editorAPI: { - findMatches: (uri: string, value: string) => any - getFontSize: () => number, - getValue: (uri: string) => string - getCursorPosition: (offset?: boolean) => number | monacoTypes.IPosition - getHoverPosition: (position: monacoTypes.IPosition) => number - addDecoration: (marker: sourceMarker, filePath: string, typeOfDecoration: string) => DecorationsReturn - clearDecorationsByPlugin: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn - keepDecorationsFor: (filePath: string, plugin: string, typeOfDecoration: string, registeredDecorations: any, currentDecorations: any) => DecorationsReturn - addErrorMarker: (errors: errorMarker[], from: string) => void - clearErrorMarkers: (sources: string[] | {[fileName: string]: any}, from: string) => void - } + plugin: PluginType + editorAPI: EditorAPIType } export const EditorUI = (props: EditorUIProps) => { const [, setCurrentBreakpoints] = useState({}) @@ -630,9 +633,67 @@ export const EditorUI = (props: EditorUIProps) => { await props.plugin.call('codeFormatter', 'format', file) }, } + + const freeFunctionCondition = editor.createContextKey('freeFunctionCondition', false); + let freeFunctionAction + const executeFreeFunctionAction = { + id: "executeFreeFunction", + label: "Run a free function in the Remix VM", + contextMenuOrder: 0, // choose the order + contextMenuGroupId: "execute", // create a new grouping + precondition: 'freeFunctionCondition', + keybindings: [ + // eslint-disable-next-line no-bitwise + monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR, + ], + run: async () => { + const { nodesAtPosition } = await retrieveNodesAtPosition(props.editorAPI, props.plugin) + // find the contract and get the nodes of the contract and the base contracts and imports + if (nodesAtPosition && isArray(nodesAtPosition) && nodesAtPosition.length) { + const freeFunctionNode = nodesAtPosition.find((node) => node.kind === 'freeFunction') + if (freeFunctionNode) { + const file = await props.plugin.call('fileManager', 'getCurrentFile') + props.plugin.call('solidity-script', 'execute', file, freeFunctionNode.name) + } else { + props.plugin.call('notification', 'toast', 'This can only execute free function') + } + } else { + props.plugin.call('notification', 'toast', 'Please go to Remix settings and activate the code editor features or wait that the current editor context is loaded.') + } + }, + } editor.addAction(formatAction) editor.addAction(zoomOutAction) editor.addAction(zoominAction) + editor.addAction(executeFreeFunctionAction) + + // we have to add the command because the menu action isn't always available (see onContextMenuHandlerForFreeFunction) + editor.addCommand(monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyR, () => executeFreeFunctionAction.run()) + + const contextmenu = editor.getContribution('editor.contrib.contextmenu') + const orgContextMenuMethod = contextmenu._onContextMenu; + const onContextMenuHandlerForFreeFunction = async () => { + const file = await props.plugin.call('fileManager', 'getCurrentFile') + if (!file.endsWith('.sol')) { + freeFunctionCondition.set(false) + return + } + const { nodesAtPosition } = await retrieveNodesAtPosition(props.editorAPI, props.plugin) + const freeFunctionNode = nodesAtPosition.find((node) => node.kind === 'freeFunction') + if (freeFunctionAction) freeFunctionAction.dispose() + if (freeFunctionNode) { + executeFreeFunctionAction.label = `Run the free function "${freeFunctionNode.name}" in the Remix VM` + freeFunctionAction = editor.addAction(executeFreeFunctionAction) + } + freeFunctionCondition.set(!!freeFunctionNode) + } + contextmenu._onContextMenu = (...args) => { + if (args[0]) args[0].event?.preventDefault() + onContextMenuHandlerForFreeFunction() + .then(() => orgContextMenuMethod.apply(contextmenu, args)) + .catch(() => orgContextMenuMethod.apply(contextmenu, args)) + } + const editorService = editor._codeEditorService; const openEditorBase = editorService.openCodeEditor.bind(editorService); editorService.openCodeEditor = async (input , source) => { diff --git a/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx b/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx index f05cea9f65..20a648fc68 100644 --- a/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx +++ b/libs/remix-ui/file-decorators/src/lib/components/file-decoration-icon.tsx @@ -8,44 +8,40 @@ import FileDecorationTooltip from './filedecorationicons/file-decoration-tooltip import FileDecorationWarningIcon from './filedecorationicons/file-decoration-warning-icon' export type fileDecorationProps = { - file: FileType, - fileDecorations: fileDecoration[] + file: FileType, + fileDecorations: fileDecoration[] } export const FileDecorationIcons = (props: fileDecorationProps) => { - const [states, setStates] = useState([]) - useEffect(() => { - //console.log(props.file) - //console.log(props.fileState) - setStates(props.fileDecorations.filter((fileDecoration) => fileDecoration.path === props.file.path || `${fileDecoration.workspace.name}/${fileDecoration.path}` === props.file.path)) - }, [props.fileDecorations]) + const [states, setStates] = useState([]) + useEffect(() => { + setStates(props.fileDecorations.filter((fileDecoration) => fileDecoration.path === props.file.path || `${fileDecoration.workspace.name}/${fileDecoration.path}` === props.file.path)) + }, [props.fileDecorations]) + const getTags = function () { + if (states && states.length) { + const elements: JSX.Element[] = [] - const getTags = function () { - if (states && states.length) { - const elements: JSX.Element[] = [] - - for (const [index, state] of states.entries()) { - switch (state.fileStateType) { - case fileDecorationType.Error: - elements.push(}/>) - break - case fileDecorationType.Warning: - elements.push(}/>) - break - case fileDecorationType.Custom: - elements.push(}/>) - break - } - } - - return elements + for (const [index, state] of states.entries()) { + switch (state.fileStateType) { + case fileDecorationType.Error: + elements.push(}/>) + break + case fileDecorationType.Warning: + elements.push(}/>) + break + case fileDecorationType.Custom: + elements.push(}/>) + break } + } + return elements } + } - return <> - {getTags()} - + return <> + {getTags()} + } export default FileDecorationIcons \ No newline at end of file diff --git a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx index 1d87846192..d841e85021 100644 --- a/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx +++ b/libs/remix-ui/file-decorators/src/lib/components/filedecorationicons/file-decoration-tooltip.tsx @@ -1,33 +1,35 @@ import React from "react"; -import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { OverlayTrigger, Popover } from "react-bootstrap"; import { fileDecoration } from "../../types"; const FileDecorationTooltip = (props: { - fileDecoration: fileDecoration, - icon: JSX.Element - index: number + fileDecoration: fileDecoration, + icon: JSX.Element + index: number }, ) => { - const getComments = function (fileDecoration: fileDecoration) { - if (fileDecoration.comment) { - const comments = Array.isArray(fileDecoration.comment) ? fileDecoration.comment : [fileDecoration.comment] - return comments.map((comment, index) => { - return
{comment}

- }) - } + const getComments = function (fileDecoration: fileDecoration) { + if (fileDecoration.comment) { + const comments = Array.isArray(fileDecoration.comment) ? fileDecoration.comment : [fileDecoration.comment] + return comments.map((comment, index) => { + return
{comment}
+ }) } + } - return - <>{getComments(props.fileDecoration)} - - } - >
{props.icon}
- + return + +
{getComments(props.fileDecoration)}
+
+ + } + > +
{props.icon}
+
} - export default FileDecorationTooltip; \ No newline at end of file diff --git a/libs/remix-ui/helper/src/lib/components/custom-tooltip.tsx b/libs/remix-ui/helper/src/lib/components/custom-tooltip.tsx index 6c55ed06c8..560d06ea78 100644 --- a/libs/remix-ui/helper/src/lib/components/custom-tooltip.tsx +++ b/libs/remix-ui/helper/src/lib/components/custom-tooltip.tsx @@ -1,19 +1,27 @@ import React from 'react'; import { Fragment } from 'react'; -import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; import { CustomTooltipType } from '../../types/customtooltip' +export function CustomTooltip ({ children, placement, tooltipId, tooltipClasses, tooltipText, tooltipTextClasses, delay }: CustomTooltipType) { -export function CustomTooltip({ children, placement, tooltipId, tooltipClasses, tooltipText, tooltipTextClasses, delay }: CustomTooltipType) { + if (typeof tooltipText !== 'string') { + const newTooltipText = React.cloneElement(tooltipText, { + className: " bg-secondary text-wrap p-1 px-2 " + }) + tooltipText = newTooltipText + } return ( - {typeof tooltipText === 'string' ? ({tooltipText}) : (tooltipText)} - + + + {typeof tooltipText === 'string' ? ({tooltipText}) : (tooltipText)} + + } delay={delay} > diff --git a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx index 760e5aa6b0..d73d1c979a 100644 --- a/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx +++ b/libs/remix-ui/home-tab/src/lib/components/homeTabTitle.tsx @@ -6,7 +6,7 @@ import { CustomTooltip } from '@remix-ui/helper' const _paq = window._paq = window._paq || [] // eslint-disable-line -function HomeTabTitle() { +function HomeTabTitle () { useEffect(() => { document.addEventListener("keyup", (e) => handleSearchKeyDown(e)) return () => { @@ -184,4 +184,4 @@ function HomeTabTitle() { ) } -export default HomeTabTitle +export default HomeTabTitle \ No newline at end of file diff --git a/libs/remix-ui/home-tab/src/lib/components/pluginButton.tsx b/libs/remix-ui/home-tab/src/lib/components/pluginButton.tsx index 1b6bb08092..426c84bf2f 100644 --- a/libs/remix-ui/home-tab/src/lib/components/pluginButton.tsx +++ b/libs/remix-ui/home-tab/src/lib/components/pluginButton.tsx @@ -32,7 +32,7 @@ function PluginButton ({ imgPath, envID, envText, callback, l2, description, rem { remixMaintained && diff --git a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx index 66330349b0..bb02249b82 100644 --- a/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx +++ b/libs/remix-ui/modal-dialog/src/lib/remix-ui-modal-dialog.tsx @@ -87,7 +87,7 @@ export const ModalDialog = (props: ModalDialogProps) => { {!props.showCancelIcon && handleHide()}> - + }
diff --git a/libs/remix-ui/panel/src/lib/main/main-panel.css b/libs/remix-ui/panel/src/lib/main/main-panel.css index d569338fab..28a24ba01d 100644 --- a/libs/remix-ui/panel/src/lib/main/main-panel.css +++ b/libs/remix-ui/panel/src/lib/main/main-panel.css @@ -3,6 +3,6 @@ flex-direction : column; height : 100%; width : 100%; - position: relative; + position : relative; } diff --git a/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx b/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx index 229ca41581..066d664f27 100644 --- a/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx +++ b/libs/remix-ui/panel/src/lib/plugins/panel-header.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react' // eslint-disable-lin import { FormattedMessage } from 'react-intl' import { PluginRecord } from '../types' import './panel.css' -import { OverlayTrigger, Tooltip } from 'react-bootstrap' import { CustomTooltip } from '@remix-ui/helper' export interface RemixPanelProps { @@ -39,16 +38,14 @@ const RemixUIPanelHeader = (props: RemixPanelProps) => {
{plugin?.profile?.maintainedBy?.toLowerCase() === "remix" && ( - - {"Maintained by Remix"} - - } - > + - + )}
@@ -63,7 +60,6 @@ const RemixUIPanelHeader = (props: RemixPanelProps) => {
-
{plugin?.profile?.author && @@ -76,7 +72,14 @@ const RemixUIPanelHeader = (props: RemixPanelProps) => { {plugin?.profile?.documentation && - + + + } {plugin?.profile?.description && diff --git a/libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx b/libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx index 47e35a55db..91484ff727 100644 --- a/libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx +++ b/libs/remix-ui/plugin-manager/src/lib/components/ActivePluginCard.tsx @@ -1,6 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-use-before-define import React from 'react' import '../remix-ui-plugin-manager.css' +import { CustomTooltip } from '@remix-ui/helper' interface PluginCardProps { profile: any buttonText: string @@ -14,35 +15,72 @@ function ActivePluginCard ({ }: PluginCardProps) { return (
-
+
{ profile.displayName || profile.name } { profile?.maintainedBy?.toLowerCase() == "remix" && - + + + } { profile.documentation && - -