Skip to content

Commit

Permalink
Format source code (#1541)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
chioni16 authored Oct 2, 2023
1 parent 55002c0 commit 2fc1640
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
85 changes: 79 additions & 6 deletions src/bin/languageserver/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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()
},
})
Expand Down Expand Up @@ -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<Option<Vec<TextEdit>>> {
// 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::<Vec<_>>();
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
Expand Down
5 changes: 4 additions & 1 deletion vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@
}
},
"capabilities": {
"hoverProvider": "true"
"hoverProvider": "true",
"formatting": {
"dynamicRegistration": true
}
},
"languages": [
{
Expand Down
48 changes: 47 additions & 1 deletion vscode/src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
48 changes: 48 additions & 0 deletions vscode/src/testFixture/format.sol
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 2fc1640

Please sign in to comment.