forked from FuelLabs/sway
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added initial version of the formatter (FuelLabs#74)
* remove typescript ignores * included formatting * initial formatting logic * format * handle multiline strings * update comments * format * fixed comments bug * refactor with helpers * fixed typo in comment * refactor to separate methods * add newline * format * switch to using content directly in order to aviod sync problems
- Loading branch information
1 parent
f253593
commit 8008f1b
Showing
9 changed files
with
555 additions
and
11 deletions.
There are no files selected for viewing
268 changes: 268 additions & 0 deletions
268
sway-server/src/capabilities/formatting/code_builder.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
use std::{ | ||
iter::{Enumerate, Peekable}, | ||
str::Chars, | ||
}; | ||
|
||
use lspower::lsp::{Position, Range, TextEdit}; | ||
|
||
use super::parse_helpers::{clean_all_incoming_whitespace, is_comment}; | ||
use super::{ | ||
code_line::CodeLine, | ||
parse_helpers::{ | ||
handle_ampersand_case, handle_assignment_case, handle_colon_case, handle_dash_case, | ||
handle_pipe_case, handle_string_case, handle_whitespace_case, | ||
}, | ||
}; | ||
|
||
#[derive(Debug)] | ||
pub struct CodeBuilder { | ||
tab_size: u32, | ||
indent_level: u32, | ||
edits: Vec<CodeLine>, | ||
} | ||
|
||
impl CodeBuilder { | ||
pub fn new(tab_size: u32) -> Self { | ||
Self { | ||
tab_size, | ||
indent_level: 0, | ||
edits: vec![], | ||
} | ||
} | ||
|
||
pub fn to_text_edit(&mut self, text_lines_count: usize) -> Vec<TextEdit> { | ||
let line_end = std::cmp::max(self.edits.len(), text_lines_count) as u32; | ||
|
||
// add new line at the end if needed | ||
if let Some(code_line) = self.edits.last() { | ||
if !code_line.is_empty() { | ||
self.edits.push(CodeLine::empty_line()) | ||
} | ||
} | ||
|
||
let main_edit = TextEdit { | ||
range: Range::new(Position::new(0, 0), Position::new(line_end as u32, 0)), | ||
new_text: self | ||
.edits | ||
.iter() | ||
.map(|code_line| code_line.text.clone()) | ||
.collect::<Vec<String>>() | ||
.join("\n"), | ||
}; | ||
|
||
vec![main_edit] | ||
} | ||
|
||
/// formats line of code and adds it to Vec<CodeLine> | ||
pub fn format_and_add(&mut self, line: &str) { | ||
let mut code_line = self.get_unfinished_code_line_or_new(); | ||
|
||
let line = if !code_line.is_string { | ||
line.trim() | ||
} else { | ||
line | ||
}; | ||
|
||
// handle comment | ||
if is_comment(line) { | ||
code_line.push_str(line); | ||
return self.complete_and_add_line(code_line); | ||
} | ||
|
||
// handle multiline string | ||
if code_line.is_string { | ||
code_line.push_char('\n'); | ||
} | ||
|
||
let mut iter = line.chars().enumerate().peekable(); | ||
|
||
loop { | ||
if let Some((_, current_char)) = iter.next() { | ||
if code_line.is_string { | ||
handle_string_case(&mut code_line, current_char); | ||
} else { | ||
match current_char { | ||
' ' => handle_whitespace_case(&mut code_line, &mut iter), | ||
'=' => handle_assignment_case(&mut code_line, &mut iter), | ||
':' => handle_colon_case(&mut code_line, &mut iter), | ||
'-' => handle_dash_case(&mut code_line, &mut iter), | ||
'|' => handle_pipe_case(&mut code_line, &mut iter), | ||
'&' => handle_ampersand_case(&mut code_line, &mut iter), | ||
|
||
',' => code_line.push_str(", "), | ||
'+' => code_line.append_with_whitespace("+ "), | ||
'*' => code_line.append_with_whitespace("* "), | ||
'/' => code_line.append_with_whitespace("- "), | ||
'%' => code_line.append_with_whitespace("% "), | ||
'^' => code_line.append_with_whitespace("^ "), | ||
'!' => code_line.append_with_whitespace("!"), | ||
|
||
// handle beginning of the string | ||
'"' => { | ||
if !code_line.is_string { | ||
code_line.push_char(current_char); | ||
code_line.become_string(); | ||
} | ||
} | ||
|
||
// handle line breakers ';', '{' AND '}' | ||
';' => return self.handle_semicolon_case(line, code_line, iter), | ||
|
||
'{' => { | ||
code_line.append_with_whitespace("{"); | ||
self.complete_and_add_line(code_line); | ||
self.indent(); | ||
|
||
// if there is more - move to new line! | ||
return self.move_rest_to_new_line(line, iter); | ||
} | ||
|
||
'}' => return self.handle_close_brace(line, code_line, iter), | ||
|
||
// add the rest | ||
_ => code_line.push_char(current_char), | ||
} | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
|
||
self.add_line(code_line); | ||
} | ||
|
||
/// if previous line is not completed get it, otherwise start a new one | ||
fn get_unfinished_code_line_or_new(&mut self) -> CodeLine { | ||
match self.edits.last() { | ||
Some(code_line) => { | ||
if code_line.is_completed { | ||
CodeLine::default() | ||
} else { | ||
self.edits.pop().unwrap() | ||
} | ||
} | ||
None => CodeLine::default(), | ||
} | ||
} | ||
|
||
fn handle_semicolon_case( | ||
&mut self, | ||
line: &str, | ||
code_line: CodeLine, | ||
iter: Peekable<Enumerate<Chars>>, | ||
) { | ||
let mut code_line = code_line; | ||
code_line.push_char(';'); | ||
|
||
if code_line.text == ";" { | ||
if let Some(previous_code_line) = self.edits.last() { | ||
// case when '}' was separated from ';' by one or more new lines | ||
if previous_code_line.is_completed { | ||
// remove empty line first | ||
if !(previous_code_line.text.chars().last().unwrap_or(' ') == '}') { | ||
self.edits.pop(); | ||
} | ||
|
||
let mut updated_code_line = self.edits.pop().unwrap(); | ||
updated_code_line.push_char(';'); | ||
self.complete_and_add_line(updated_code_line); | ||
} | ||
} | ||
} else { | ||
self.complete_and_add_line(code_line); | ||
} | ||
|
||
self.move_rest_to_new_line(line, iter); | ||
} | ||
|
||
fn handle_close_brace( | ||
&mut self, | ||
line: &str, | ||
code_line: CodeLine, | ||
iter: Peekable<Enumerate<Chars>>, | ||
) { | ||
let mut iter = iter; | ||
|
||
// if there was something prior to '}', add as separate line | ||
if !code_line.is_empty() { | ||
self.complete_and_add_line(code_line); | ||
} | ||
|
||
self.outdent(); | ||
clean_all_incoming_whitespace(&mut iter); | ||
|
||
match iter.peek() { | ||
// check is there a ';' and add it after '}' | ||
Some((_, ';')) => { | ||
self.complete_and_add_line(CodeLine::new("};".into())); | ||
iter.next(); | ||
self.move_rest_to_new_line(line, iter); | ||
} | ||
// if there is more - move to new line! | ||
Some(_) => { | ||
self.move_rest_to_new_line(line, iter); | ||
} | ||
None => { | ||
self.complete_and_add_line(CodeLine::new("}".into())); | ||
} | ||
} | ||
} | ||
|
||
fn move_rest_to_new_line(&mut self, line: &str, iter: Peekable<Enumerate<Chars>>) { | ||
let mut iter = iter; | ||
|
||
if iter.peek().is_some() { | ||
let (next_index, _) = iter.peek().unwrap(); | ||
|
||
let next_line = &line[*next_index..]; | ||
self.format_and_add(next_line); | ||
} | ||
} | ||
|
||
fn complete_and_add_line(&mut self, code_line: CodeLine) { | ||
let mut code_line = code_line; | ||
code_line.complete(); | ||
self.add_line(code_line); | ||
} | ||
|
||
fn add_line(&mut self, code_line: CodeLine) { | ||
let mut code_line = code_line; | ||
|
||
if code_line.is_empty() { | ||
// don't add more than one new empty line! | ||
if self | ||
.edits | ||
.last() | ||
.unwrap_or(&CodeLine::empty_line()) | ||
.is_empty() | ||
{ | ||
return; | ||
} else { | ||
// push empty line | ||
self.edits.push(CodeLine::empty_line()); | ||
} | ||
} else { | ||
if code_line.was_previously_stored { | ||
self.edits.push(code_line); | ||
} else { | ||
code_line.update_for_storage(self.get_indentation()); | ||
self.edits.push(code_line); | ||
} | ||
} | ||
} | ||
|
||
fn indent(&mut self) { | ||
self.indent_level += 1; | ||
} | ||
|
||
fn outdent(&mut self) { | ||
if self.indent_level > 0 { | ||
self.indent_level -= 1; | ||
} | ||
} | ||
|
||
fn get_indentation(&self) -> String { | ||
let times = (self.tab_size * self.indent_level) as usize; | ||
" ".repeat(times) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
#[derive(Debug)] | ||
pub struct CodeLine { | ||
pub text: String, | ||
pub is_string: bool, | ||
pub is_completed: bool, | ||
pub was_previously_stored: bool, | ||
} | ||
|
||
impl CodeLine { | ||
pub fn new(text: String) -> Self { | ||
Self { | ||
text, | ||
is_string: false, | ||
is_completed: false, | ||
was_previously_stored: false, | ||
} | ||
} | ||
|
||
pub fn default() -> Self { | ||
Self { | ||
text: "".into(), | ||
is_string: false, | ||
is_completed: false, | ||
was_previously_stored: false, | ||
} | ||
} | ||
|
||
pub fn empty_line() -> Self { | ||
Self { | ||
text: "".into(), | ||
is_string: false, | ||
is_completed: true, | ||
was_previously_stored: false, | ||
} | ||
} | ||
|
||
pub fn push_str(&mut self, line: &str) { | ||
self.text.push_str(line); | ||
} | ||
|
||
pub fn push_char(&mut self, c: char) { | ||
self.text.push(c); | ||
} | ||
|
||
pub fn complete(&mut self) { | ||
self.is_completed = true; | ||
} | ||
|
||
pub fn become_string(&mut self) { | ||
self.is_string = true; | ||
} | ||
|
||
pub fn end_string(&mut self) { | ||
self.is_string = false; | ||
} | ||
|
||
pub fn update_for_storage(&mut self, indentation: String) { | ||
self.was_previously_stored = true; | ||
self.text = format!("{}{}", indentation, self.text); | ||
} | ||
|
||
pub fn append_with_whitespace(&mut self, value: &str) { | ||
let last = self.text.chars().last(); | ||
let is_previous_whitespace = if last.is_none() { | ||
true | ||
} else { | ||
last.unwrap() == ' ' | ||
}; | ||
|
||
if !is_previous_whitespace { | ||
self.push_char(' '); | ||
} | ||
|
||
self.push_str(value); | ||
} | ||
|
||
pub fn is_empty(&self) -> bool { | ||
self.text.is_empty() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
use std::sync::Arc; | ||
|
||
use crate::core::session::Session; | ||
use core_lang::parse; | ||
use lspower::lsp::{DocumentFormattingParams, FormattingOptions, TextDocumentIdentifier, TextEdit}; | ||
|
||
use super::code_builder::CodeBuilder; | ||
|
||
pub fn format_document( | ||
session: Arc<Session>, | ||
params: DocumentFormattingParams, | ||
) -> Option<Vec<TextEdit>> { | ||
let options: FormattingOptions = params.options; | ||
let text_document: TextDocumentIdentifier = params.text_document; | ||
let url = text_document.uri; | ||
|
||
session.format_text(&url, options) | ||
} | ||
|
||
pub fn get_format_text_edits(text: &str, options: FormattingOptions) -> Option<Vec<TextEdit>> { | ||
// we only format if code is correct | ||
match parse(text) { | ||
core_lang::CompileResult::Ok { | ||
value: _, | ||
warnings: _, | ||
errors: _, | ||
} => Some(build_edits(text, options)), | ||
_ => None, | ||
} | ||
} | ||
|
||
fn build_edits(text: &str, options: FormattingOptions) -> Vec<TextEdit> { | ||
let mut code_builder = CodeBuilder::new(options.tab_size); | ||
let lines: Vec<&str> = text.split("\n").collect(); | ||
let length_of_lines = lines.len(); | ||
|
||
// todo: handle lengthy lines of code | ||
for line in lines { | ||
code_builder.format_and_add(line); | ||
} | ||
|
||
code_builder.to_text_edit(length_of_lines) | ||
} |
Oops, something went wrong.