From 975a959845eb9ba1c7b1c2d0f80734c6a152997f Mon Sep 17 00:00:00 2001 From: filip mertens Date: Wed, 14 Sep 2022 16:20:08 +0200 Subject: [PATCH] add code format --- apps/remix-ide/src/app.js | 21 +- apps/remix-ide/src/app/plugins/code-format.ts | 87 ++++++++ .../src/app/plugins/code-format/parser.ts | 197 ++++++++++++++++++ apps/remix-ide/src/remixAppManager.js | 2 +- .../editor/src/lib/remix-ui-editor.tsx | 14 ++ package.json | 2 + yarn.lock | 50 ++++- 7 files changed, 362 insertions(+), 11 deletions(-) create mode 100644 apps/remix-ide/src/app/plugins/code-format.ts create mode 100644 apps/remix-ide/src/app/plugins/code-format/parser.ts diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index b001164e9c..6848c65c6a 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -33,6 +33,7 @@ import { ExternalHttpProvider } from './app/tabs/external-http-provider' import { Injected0ptimismProvider } from './app/tabs/injected-optimism-provider' import { InjectedArbitrumOneProvider } from './app/tabs/injected-arbitrum-one-provider' import { FileDecorator } from './app/plugins/file-decorator' +import { CodeFormat } from './app/plugins/code-format' const isElectron = require('is-electron') @@ -63,7 +64,7 @@ const Terminal = require('./app/panels/terminal') const { TabProxy } = require('./app/panels/tab-proxy.js') class AppComponent { - constructor () { + constructor() { this.appManager = new RemixAppManager({}) this.queryParams = new QueryParams() this._components = {} @@ -100,7 +101,7 @@ class AppComponent { }) } - async run () { + async run() { // APP_MANAGER const appManager = this.appManager const pluginLoader = this.appManager.pluginLoader @@ -161,6 +162,9 @@ class AppComponent { // ------- FILE DECORATOR PLUGIN ------------------ const fileDecorator = new FileDecorator() + // ------- CODE FORMAT PLUGIN ------------------ + const codeFormat = new CodeFormat() + //----- search const search = new SearchPlugin() @@ -213,7 +217,7 @@ class AppComponent { } } ) - + const codeParser = new CodeParser(new AstWalker()) @@ -221,7 +225,7 @@ class AppComponent { const configPlugin = new ConfigPlugin() this.layout = new Layout() - + const permissionHandler = new PermissionHandlerPlugin() this.engine.register([ @@ -241,6 +245,7 @@ class AppComponent { offsetToLineColumnConverter, codeParser, fileDecorator, + codeFormat, terminal, web3Provider, compileAndRun, @@ -350,10 +355,10 @@ class AppComponent { } } - async activate () { + async activate() { const queryParams = new QueryParams() const params = queryParams.get() - + try { this.engine.register(await this.appManager.registeredPlugins()) } catch (e) { @@ -367,9 +372,9 @@ class AppComponent { await this.appManager.activatePlugin(['sidePanel']) // activating host plugin separately await this.appManager.activatePlugin(['home']) await this.appManager.activatePlugin(['settings', 'config']) - await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) + await this.appManager.activatePlugin(['hiddenPanel', 'pluginManager', 'codeParser', 'codeFormat', 'fileDecorator', 'terminal', 'blockchain', 'fetchAndCompile', 'contentImport', 'gistHandler']) await this.appManager.activatePlugin(['settings']) - await this.appManager.activatePlugin(['walkthrough','storage', 'search','compileAndRun', 'recorder']) + await this.appManager.activatePlugin(['walkthrough', 'storage', 'search', 'compileAndRun', 'recorder']) this.appManager.on( 'filePanel', diff --git a/apps/remix-ide/src/app/plugins/code-format.ts b/apps/remix-ide/src/app/plugins/code-format.ts new file mode 100644 index 0000000000..7e701310f4 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/code-format.ts @@ -0,0 +1,87 @@ +'use strict' +import { Plugin } from '@remixproject/engine' +import prettier from 'prettier/standalone' +import { Options } from 'prettier'; +import * as sol from 'prettier-plugin-solidity' +import { parse } from './code-format/parser' +import * as ts from 'prettier/parser-typescript' +import * as babel from 'prettier/parser-babel' +import * as espree from 'prettier/parser-espree' +import path from 'path' + +const profile = { + name: 'codeFormat', + desciption: 'prettier plugin for Remix', + methods: ['format'], + events: [''], + version: '0.0.1' +} + +export class CodeFormat extends Plugin { + + constructor() { + super(profile) + // need to change the parser in the npm package because it conflicts with the parser already in the app + const loc = { + locEnd: (node) => getRange(1, node), + locStart: (node) => getRange(0, node) + } + const parser = { astFormat: 'solidity-ast', parse, ...loc }; + sol.parsers = { + 'solidity-parse': parser + } + } + + async format() { + const file = await this.call('fileManager', 'getCurrentFile') + const content = await this.call('fileManager', 'readFile', file) + let parserName = '' + let options: Options = { + } + switch (path.extname(file)) { + case '.sol': + parserName = 'solidity-parse' + break + case '.ts': + parserName = 'typescript' + options = { + ...options, + trailingComma: 'all', + semi: false, + singleQuote: true, + quoteProps: 'as-needed', + bracketSpacing: true, + arrowParens: 'always', + } + break + case '.js': + parserName = "espree" + options = { + ...options, + semi: false, + singleQuote: true, + } + break + case '.json': + parserName = 'json' + break + } + const result = prettier.format(content, { + plugins: [sol, ts, babel, espree], + parser: parserName, + ...options + }) + await this.call('fileManager', 'writeFile', file, result) + } + +} + +function getRange(index, node) { + if (node.range) { + return node.range[index]; + } + if (node.expression && node.expression.range) { + return node.expression.range[index]; + } + return null; +} diff --git a/apps/remix-ide/src/app/plugins/code-format/parser.ts b/apps/remix-ide/src/app/plugins/code-format/parser.ts new file mode 100644 index 0000000000..15ef38f871 --- /dev/null +++ b/apps/remix-ide/src/app/plugins/code-format/parser.ts @@ -0,0 +1,197 @@ +// https://prettier.io/docs/en/plugins.html#parsers +import extractComments from 'solidity-comments-extractor'; +// use the parser already included in the app +const parser = (window as any).SolidityParser +import semver from 'semver'; + +const tryHug = (node, operators) => { + if (node.type === 'BinaryOperation' && operators.includes(node.operator)) + return { + type: 'TupleExpression', + components: [node], + isArray: false + }; + return node; +}; + +export function parse(text, _parsers, options) { + const compiler = semver.coerce(options.compiler); + const parsed = parser.parse(text, { loc: true, range: true }); + parsed.comments = extractComments(text) + + parser.visit(parsed, { + PragmaDirective(ctx) { + // if the pragma is not for solidity we leave. + if (ctx.name !== 'solidity') return; + // if the compiler option has not been provided we leave. + if (!compiler) return; + // we make a check against each pragma directive in the document. + if (!semver.satisfies(compiler, ctx.value)) { + // @TODO: investigate the best way to warn that would apply to + // different editors. + // eslint-disable-next-line no-console + console.warn( + `[prettier-solidity] The compiler option is set to '${options.compiler}', which does not satisfy 'pragma solidity ${ctx.value}'.` + ); + } + }, + ModifierDefinition(ctx) { + if (!ctx.parameters) { + ctx.parameters = []; + } + }, + FunctionDefinition(ctx) { + if (!ctx.isConstructor) { + ctx.modifiers.forEach((modifier) => { + if (modifier.arguments && modifier.arguments.length === 0) { + // eslint-disable-next-line no-param-reassign + modifier.arguments = null; + } + }); + } + }, + ForStatement(ctx) { + if (ctx.initExpression) { + ctx.initExpression.omitSemicolon = true; + } + ctx.loopExpression.omitSemicolon = true; + }, + HexLiteral(ctx) { + ctx.value = options.singleQuote + ? `hex'${ctx.value.slice(4, -1)}'` + : `hex"${ctx.value.slice(4, -1)}"`; + }, + ElementaryTypeName(ctx) { + // if the compiler is below 0.8.0 we will recognize the type 'byte' as an + // alias of 'bytes1'. Otherwise we will ignore this and enforce always + // 'bytes1'. + const pre080 = compiler && semver.satisfies(compiler, '<0.8.0'); + if (!pre080 && ctx.name === 'byte') ctx.name = 'bytes1'; + + if (options.explicitTypes === 'always') { + if (ctx.name === 'uint') ctx.name = 'uint256'; + if (ctx.name === 'int') ctx.name = 'int256'; + if (pre080 && ctx.name === 'byte') ctx.name = 'bytes1'; + } else if (options.explicitTypes === 'never') { + if (ctx.name === 'uint256') ctx.name = 'uint'; + if (ctx.name === 'int256') ctx.name = 'int'; + if (pre080 && ctx.name === 'bytes1') ctx.name = 'byte'; + } + }, + BinaryOperation(ctx) { + switch (ctx.operator) { + case '+': + case '-': + ctx.left = tryHug(ctx.left, ['%']); + ctx.right = tryHug(ctx.right, ['%']); + break; + case '*': + ctx.left = tryHug(ctx.left, ['/', '%']); + break; + case '/': + ctx.left = tryHug(ctx.left, ['*', '%']); + break; + case '%': + ctx.left = tryHug(ctx.left, ['*', '/', '%']); + break; + case '**': + // If the compiler has not been given as an option using we leave a**b**c. + if (!compiler) break; + + if (semver.satisfies(compiler, '<0.8.0')) { + // If the compiler is less than 0.8.0 then a**b**c is formatted as + // (a**b)**c. + ctx.left = tryHug(ctx.left, ['**']); + break; + } + if ( + ctx.left.type === 'BinaryOperation' && + ctx.left.operator === '**' + ) { + // the parser still organizes the a**b**c as (a**b)**c so we need + // to restructure it. + ctx.right = { + type: 'TupleExpression', + components: [ + { + type: 'BinaryOperation', + operator: '**', + left: ctx.left.right, + right: ctx.right + } + ], + isArray: false + }; + ctx.left = ctx.left.left; + } + break; + case '<<': + case '>>': + ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']); + ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**']); + break; + case '&': + ctx.left = tryHug(ctx.left, ['+', '-', '*', '/', '**', '<<', '>>']); + ctx.right = tryHug(ctx.right, ['+', '-', '*', '/', '**', '<<', '>>']); + break; + case '|': + ctx.left = tryHug(ctx.left, [ + '+', + '-', + '*', + '/', + '**', + '<<', + '>>', + '&', + '^' + ]); + ctx.right = tryHug(ctx.right, [ + '+', + '-', + '*', + '/', + '**', + '<<', + '>>', + '&', + '^' + ]); + break; + case '^': + ctx.left = tryHug(ctx.left, [ + '+', + '-', + '*', + '/', + '**', + '<<', + '>>', + '&' + ]); + ctx.right = tryHug(ctx.right, [ + '+', + '-', + '*', + '/', + '**', + '<<', + '>>', + '&' + ]); + break; + case '||': + ctx.left = tryHug(ctx.left, ['&&']); + ctx.right = tryHug(ctx.right, ['&&']); + break; + case '&&': + default: + break; + } + } + }); + + return parsed; +} + + diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 0f97707ce6..3fc4ffc9bb 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-optimism-provider', 'injected-arbitrum-one-provider', - 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser'] + 'compileAndRun', 'search', 'recorder', 'fileDecorator', 'codeParser', 'codeFormat'] // dependentModules shouldn't be manually activated (e.g hardhat is activated by remixd) const dependentModules = ['hardhat', 'truffle', 'slither'] 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 38f9a5f7ac..ab075895b0 100644 --- a/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx +++ b/libs/remix-ui/editor/src/lib/remix-ui-editor.tsx @@ -568,6 +568,20 @@ export const EditorUI = (props: EditorUIProps) => { ], run: () => { editor.updateOptions({ fontSize: editor.getOption(43).fontSize - 1 }) }, } + const formatAction = { + id: "autoFormat", + label: "Format Code", + contextMenuOrder: 0, // choose the order + contextMenuGroupId: "formatting", // create a new grouping + keybindings: [ + // eslint-disable-next-line no-bitwise + monacoRef.current.KeyMod.Shift | monacoRef.current.KeyMod.Alt | monacoRef.current.KeyCode.KeyF, + ], + run: async () => { + await props.plugin.call('codeFormat', 'format') + }, + } + editor.addAction(formatAction) editor.addAction(zoomOutAction) editor.addAction(zoominAction) const editorService = editor._codeEditorService; diff --git a/package.json b/package.json index 8399c040bc..f02f9a5b52 100644 --- a/package.json +++ b/package.json @@ -197,6 +197,8 @@ "merge": "^2.1.1", "monaco-editor": "^0.30.1", "npm-install-version": "^6.0.2", + "prettier": "^2.7.1", + "prettier-plugin-solidity": "^1.0.0-beta.24", "raw-loader": "^4.0.2", "react": "^17.0.2", "react-beautiful-dnd": "^13.1.0", diff --git a/yarn.lock b/yarn.lock index 3809c46cb6..28e737473a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4103,6 +4103,13 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@solidity-parser/parser@^0.14.3": + version "0.14.3" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.14.3.tgz#0d627427b35a40d8521aaa933cc3df7d07bfa36f" + integrity sha512-29g2SZ29HtsqA58pLCtopI1P/cPy5/UAzlcAXO6T/CNJimG6yA8kx4NaseMyJULiC+TEs02Y9/yeHzClqoA0hw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@svgr/babel-plugin-add-jsx-attribute@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906" @@ -5339,6 +5346,11 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha512-6QWEyvMgIXX0eO972y7YPBLSBsq7UWKFAoNNTLGaOJ9bstcEL9sCbcjf96dVfNDdUsRoGOK82vWFJlKApXds7g== +antlr4ts@^0.5.0-alpha.4: + version "0.5.0-alpha.4" + resolved "https://registry.yarnpkg.com/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz#71702865a87478ed0b40c0709f422cf14d51652a" + integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -9740,6 +9752,11 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emoji-regex@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66" + integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -9989,7 +10006,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@4.0.0: +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== @@ -19118,6 +19135,23 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +prettier-plugin-solidity@^1.0.0-beta.24: + version "1.0.0-beta.24" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-beta.24.tgz#67573ca87098c14f7ccff3639ddd8a4cab2a87eb" + integrity sha512-6JlV5BBTWzmDSq4kZ9PTXc3eLOX7DF5HpbqmmaF+kloyUwOZbJ12hIYsUaZh2fVgZdV2t0vWcvY6qhILhlzgqg== + dependencies: + "@solidity-parser/parser" "^0.14.3" + emoji-regex "^10.1.0" + escape-string-regexp "^4.0.0" + semver "^7.3.7" + solidity-comments-extractor "^0.0.7" + string-width "^4.2.3" + +prettier@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -20951,6 +20985,13 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -21370,6 +21411,11 @@ solc@0.7.4: semver "^5.5.0" tmp "0.0.33" +solidity-comments-extractor@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz#99d8f1361438f84019795d928b931f4e5c39ca19" + integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -21798,7 +21844,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==