From 2fc164094a157459d7400a5405ea2353e067e5b1 Mon Sep 17 00:00:00 2001 From: Govardhan G D <117805210+chioni16@users.noreply.github.com> Date: Tue, 3 Oct 2023 00:03:32 +0530 Subject: [PATCH] Format source code (#1541) We are importing `forge-fmt` for two reasons: - This ensures that forge-fmt uses the latest solang-parser crate, and we only have one version of solang-parser in solang binary - This makes it possible to publish the solang crate to crates.io, since forge-fmt is not on crates.io and no git depdendencies are allowed - We requested feedback from Foundry on their discord server but received no feedback This feature is enabled with the help of [forge-fmt](https://github.com/foundry-rs/foundry/tree/master/crates/fmt). Cc: @gakonst @DaniPopes Signed-off-by: Govardhan G D --- Cargo.toml | 1 + src/bin/languageserver/mod.rs | 85 +++++++++++++++++++++++-- vscode/package.json | 5 +- vscode/src/test/suite/extension.test.ts | 48 +++++++++++++- vscode/src/testFixture/format.sol | 48 ++++++++++++++ 5 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 vscode/src/testFixture/format.sol diff --git a/Cargo.toml b/Cargo.toml index 5e02b0e39..d337f181d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ contract-build = { version = "3.0.1", optional = true } primitive-types = { version = "0.12", features = ["codec"] } normalize-path = "0.2.1" bitflags = "2.3.3" +forge-fmt = "0.2.0" [dev-dependencies] num-derive = "0.4" diff --git a/src/bin/languageserver/mod.rs b/src/bin/languageserver/mod.rs index d15e70532..05cd32213 100644 --- a/src/bin/languageserver/mod.rs +++ b/src/bin/languageserver/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 +use forge_fmt::{format, parse, FormatterConfig}; use itertools::Itertools; use num_traits::ToPrimitive; use rust_lapper::{Interval, Lapper}; @@ -34,12 +35,12 @@ use tower_lsp::{ DiagnosticRelatedInformation, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse, - Hover, HoverContents, HoverParams, HoverProviderCapability, - ImplementationProviderCapability, InitializeParams, InitializeResult, InitializedParams, - Location, MarkedString, MessageType, OneOf, Position, Range, ReferenceParams, RenameParams, - ServerCapabilities, SignatureHelpOptions, TextDocumentContentChangeEvent, - TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, + DocumentFormattingParams, ExecuteCommandOptions, ExecuteCommandParams, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, + HoverProviderCapability, ImplementationProviderCapability, InitializeParams, + InitializeResult, InitializedParams, Location, MarkedString, MessageType, OneOf, Position, + Range, ReferenceParams, RenameParams, ServerCapabilities, SignatureHelpOptions, + TextDocumentContentChangeEvent, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, TypeDefinitionProviderCapability, Url, WorkspaceEdit, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }, @@ -1862,6 +1863,7 @@ impl LanguageServer for SolangServer { declaration_provider: Some(DeclarationCapability::Simple(true)), references_provider: Some(OneOf::Left(true)), rename_provider: Some(OneOf::Left(true)), + document_formatting_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() }, }) @@ -2311,6 +2313,77 @@ impl LanguageServer for SolangServer { Ok(Some(WorkspaceEdit::new(ws))) } + + /// Called when "Format Document" is called by the user on the client side. + /// + /// Expected to return the formatted version of source code present in the file on which this method was triggered. + /// + /// ### Arguments + /// * `DocumentFormattingParams` + /// * provides the name of the file whose code is to be formatted. + /// * provides options that help configure how the file is formatted. + /// + /// ### Edge cases + /// * Returns `Err` when + /// * an invalid file path is received. + /// * reading the file fails. + /// * parsing the file fails. + /// * formatting the file fails. + async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { + // get parse tree for the input file + let uri = params.text_document.uri; + let source_path = uri.to_file_path().map_err(|_| Error { + code: ErrorCode::InvalidRequest, + message: format!("Received invalid URI: {uri}").into(), + data: None, + })?; + let source = std::fs::read_to_string(source_path).map_err(|err| Error { + code: ErrorCode::InternalError, + message: format!("Failed to read file: {uri}").into(), + data: Some(Value::String(format!("{:?}", err))), + })?; + let source_parsed = parse(&source).map_err(|err| { + let err = err + .into_iter() + .map(|e| Value::String(e.message)) + .collect::>(); + Error { + code: ErrorCode::InternalError, + message: format!("Failed to parse file: {uri}").into(), + data: Some(Value::Array(err)), + } + })?; + + // get the formatted text + let config = FormatterConfig { + line_length: 80, + tab_width: params.options.tab_size as _, + ..Default::default() + }; + let mut source_formatted = String::new(); + format(&mut source_formatted, source_parsed, config).map_err(|err| Error { + code: ErrorCode::InternalError, + message: format!("Failed to format file: {uri}").into(), + data: Some(Value::String(format!("{:?}", err))), + })?; + + // create a `TextEdit` instance that replaces the contents of the file with the formatted text + let text_edit = TextEdit { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: u32::max_value(), + character: u32::max_value(), + }, + }, + new_text: source_formatted, + }; + + Ok(Some(vec![text_edit])) + } } /// Calculate the line and column from the Loc offset received from the parser diff --git a/vscode/package.json b/vscode/package.json index 35bea06d6..fdab5b0fb 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -51,7 +51,10 @@ } }, "capabilities": { - "hoverProvider": "true" + "hoverProvider": "true", + "formatting": { + "dynamicRegistration": true + } }, "languages": [ { diff --git a/vscode/src/test/suite/extension.test.ts b/vscode/src/test/suite/extension.test.ts index 045c32de9..cd8c0068e 100644 --- a/vscode/src/test/suite/extension.test.ts +++ b/vscode/src/test/suite/extension.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { getDocUri, activate } from './helper'; +import { getDocUri, activate, doc } from './helper'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it @@ -110,6 +110,13 @@ suite('Extension Test Suite', function () { test('Testing for Rename', async () => { await testrename(renamedoc1); }); + + // Tests for formatting + this.timeout(20000); + const formatdoc1 = getDocUri('format.sol'); + test('Testing for Formatting', async () => { + await testformat(formatdoc1); + }); }); function toRange(lineno1: number, charno1: number, lineno2: number, charno2: number) { @@ -464,6 +471,45 @@ async function testrename(docUri: vscode.Uri) { assert.strictEqual(loc03.newText, newname0); } +async function testformat(docUri: vscode.Uri) { + await activate(docUri); + + const options = { + tabSize: 4, + insertSpaces: false, + }; + const textedits = (await vscode.commands.executeCommand( + 'vscode.executeFormatDocumentProvider', + docUri, + options, + )) as vscode.TextEdit[]; + // make sure that the input file is not already formatted + assert(textedits.length > 0); + + // undo the changes done during the test + const undochanges = async () => { + for (let i = 0; i < textedits.length; i++) { + await vscode.commands.executeCommand('undo'); + } + }; + + try { + const workedits = new vscode.WorkspaceEdit(); + workedits.set(docUri, textedits); + const done = await vscode.workspace.applyEdit(workedits); + assert(done); + + const actualtext = doc.getText(); + const expectedtext = "contract deck {\n enum suit {\n club,\n diamonds,\n hearts,\n spades\n }\n enum value {\n two,\n three,\n four,\n five,\n six,\n seven,\n eight,\n nine,\n ten,\n jack,\n queen,\n king,\n ace\n }\n\n struct card {\n value v;\n suit s;\n }\n\n function score(card c) public returns (uint32 score) {\n if (c.s == suit.hearts) {\n if (c.v == value.ace) {\n score = 14;\n }\n if (c.v == value.king) {\n score = 13;\n }\n if (c.v == value.queen) {\n score = 12;\n }\n if (c.v == value.jack) {\n score = 11;\n }\n }\n // all others score 0\n }\n}\n"; + assert.strictEqual(actualtext, expectedtext); + } catch (error) { + await undochanges(); + throw error; + } + + await undochanges(); +} + async function testhover(docUri: vscode.Uri) { await activate(docUri); diff --git a/vscode/src/testFixture/format.sol b/vscode/src/testFixture/format.sol new file mode 100644 index 000000000..dd6f4bef2 --- /dev/null +++ b/vscode/src/testFixture/format.sol @@ -0,0 +1,48 @@ +contract deck { + enum + suit { + club, + diamonds, + hearts, + spades + } + enum value { + two, + + + three, + four, + five, + six, + seven, + eight, + nine, + ten,jack, + queen, + king, + ace + } + struct card { + value v; + suit s; + } + + function score + (card c) public returns ( + uint32 score) { + if (c.s == suit.hearts) { + if (c.v == value.ace) { + score = 14; + } + if ( c.v == value.king) { + score = 13; + } + if ( c.v == value.queen) { + score = 12; + } + if ( c.v == value.jack) { + score = 11;} + } + // all others score 0 + } +}