diff --git a/Cargo.lock b/Cargo.lock index 872f69c01..2faabd045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,6 +1172,7 @@ dependencies = [ "futures", "gen-lsp-types", "gleam-core", + "gleam-format", "gleam-language-server", "hexpm", "hostname", @@ -1263,6 +1264,19 @@ dependencies = [ "xxhash-rust", ] +[[package]] +name = "gleam-format" +version = "1.17.0" +dependencies = [ + "camino", + "ecow", + "gleam-core", + "itertools", + "pretty-arena", + "pretty_assertions", + "vec1", +] + [[package]] name = "gleam-language-server" version = "1.17.0" @@ -1272,6 +1286,7 @@ dependencies = [ "ecow", "gen-lsp-types", "gleam-core", + "gleam-format", "hexpm", "im", "insta", diff --git a/Cargo.toml b/Cargo.toml index 4cd7fe072..43de0ee8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "test-project-compiler", "hexpm", "pretty-arena", + "format", ] # common dependencies diff --git a/compiler-cli/Cargo.toml b/compiler-cli/Cargo.toml index 297c15c8a..2e0f854f0 100644 --- a/compiler-cli/Cargo.toml +++ b/compiler-cli/Cargo.toml @@ -11,6 +11,8 @@ license = "Apache-2.0" [dependencies] # The pure compiler gleam-core = { path = "../compiler-core" } +# The gleam formatter +gleam-format = { path = "../format" } # The language server gleam-language-server = { path = "../language-server" } # OS SIGINT and SIGTERM signal handling diff --git a/compiler-cli/src/format.rs b/compiler-cli/src/format.rs index 3d7543148..34729d438 100644 --- a/compiler-cli/src/format.rs +++ b/compiler-cli/src/format.rs @@ -21,7 +21,7 @@ pub fn run(stdin: bool, check: bool, files: Vec) -> Result<()> { fn process_stdin(check: bool) -> Result<()> { let src = read_stdin()?.into(); let mut out = String::new(); - gleam_core::format::pretty(&mut out, &src, Utf8Path::new(""))?; + gleam_format::pretty(&mut out, &src, Utf8Path::new(""))?; if !check { print!("{out}"); @@ -96,7 +96,7 @@ pub fn unformatted_files(files: Vec) -> Result> { fn format_file(problem_files: &mut Vec, path: Utf8PathBuf) -> Result<()> { let src = crate::fs::read(&path)?.into(); let mut output = String::new(); - gleam_core::format::pretty(&mut output, &src, &path)?; + gleam_format::pretty(&mut output, &src, &path)?; if src != output { problem_files.push(Unformatted { diff --git a/compiler-core/src/fix.rs b/compiler-core/src/fix.rs deleted file mode 100644 index d33069e05..000000000 --- a/compiler-core/src/fix.rs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023 The Gleam contributors - -use crate::{ - Error, Result, - format::{Formatter, Intermediate}, - io::Utf8Writer, - warning::WarningEmitter, -}; -use camino::Utf8Path; -use ecow::EcoString; -use pretty_arena::DocumentArena; - -pub fn parse_fix_and_format(src: &EcoString, path: &Utf8Path) -> Result { - // Parse - let parsed = crate::parse::parse_module(path.to_owned(), src, &WarningEmitter::null()) - .map_err(|error| Error::Parse { - path: path.to_path_buf(), - src: src.clone(), - error: Box::new(error), - })?; - let intermediate = Intermediate::from_extra(&parsed.extra, src); - let module = parsed.module; - - // Fix - // let module = some_fixer_module::Fixer::fix(module); - - // Format - let mut buffer = String::new(); - let arena = DocumentArena::new(); - Formatter::with_comments(&intermediate) - .module(&arena, &module) - .pretty_print(80, &mut buffer) - .map_err(|error| buffer.convert_err(error))?; - - Ok(buffer) -} diff --git a/compiler-core/src/lib.rs b/compiler-core/src/lib.rs index ed1bd4357..7c56ee476 100644 --- a/compiler-core/src/lib.rs +++ b/compiler-core/src/lib.rs @@ -74,8 +74,6 @@ pub mod docs; pub mod encryption; pub mod erlang; pub mod error; -pub mod fix; -pub mod format; pub mod hex; pub mod io; pub mod javascript; diff --git a/format/Cargo.toml b/format/Cargo.toml new file mode 100644 index 000000000..30b84f88a --- /dev/null +++ b/format/Cargo.toml @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2020 The Gleam contributors + +[package] +name = "gleam-format" +version = "1.17.0" +authors = ["Louis Pilfold "] +edition = "2024" +license = "Apache-2.0" + +[dependencies] +gleam-core = { path = "../compiler-core" } +pretty-arena = { path = "../pretty-arena" } +itertools.workspace = true +vec1.workspace = true +camino.workspace = true +ecow.workspace = true +pretty_assertions.workspace = true diff --git a/compiler-core/src/format.rs b/format/src/lib.rs similarity index 99% rename from compiler-core/src/format.rs rename to format/src/lib.rs index 4f6468d69..2d12be474 100644 --- a/compiler-core/src/format.rs +++ b/format/src/lib.rs @@ -4,7 +4,9 @@ #[cfg(test)] mod tests; -use crate::{ +use camino::Utf8Path; +use ecow::{EcoString, eco_format}; +use gleam_core::{ Error, Result, ast::{ CustomType, Import, ModuleConstant, TypeAlias, TypeAstConstructor, TypeAstFn, TypeAstHole, @@ -13,21 +15,18 @@ use crate::{ build::Target, io::Utf8Writer, parse::extra::{Comment, ModuleExtra}, + type_::Deprecation, warning::WarningEmitter, }; -use ecow::{EcoString, eco_format}; use itertools::Itertools; use pretty_arena::*; use std::cmp::Ordering; use vec1::Vec1; -use crate::type_::Deprecation; -use camino::Utf8Path; - const INDENT: isize = 2; pub fn pretty(writer: &mut impl Utf8Writer, src: &EcoString, path: &Utf8Path) -> Result<()> { - let parsed = crate::parse::parse_module(path.to_owned(), src, &WarningEmitter::null()) + let parsed = gleam_core::parse::parse_module(path.to_owned(), src, &WarningEmitter::null()) .map_err(|error| Error::Parse { path: path.to_path_buf(), src: src.clone(), @@ -406,6 +405,20 @@ impl<'a, 'doc> Formatter<'a> { arena.join(imports, LINE_DOCUMENT) } + fn unqualified_import( + &self, + arena: &'doc DocumentArena<'a, 'doc>, + unqualified_import: &'a UnqualifiedImport, + ) -> Document<'a, 'doc> { + unqualified_import.name.as_ref().to_doc(arena).append( + arena, + match &unqualified_import.as_name { + None => EMPTY_DOCUMENT, + Some(s) => " as ".to_doc(arena).append(arena, s.as_str()), + }, + ) + } + fn definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, @@ -431,11 +444,17 @@ impl<'a, 'doc> Formatter<'a> { let unqualified_types = unqualified_types .iter() .sorted_by(|a, b| a.name.cmp(&b.name)) - .map(|type_| docvec![arena, TYPE_SPACE_DOCUMENT, type_]); + .map(|import_| { + docvec![ + arena, + TYPE_SPACE_DOCUMENT, + self.unqualified_import(arena, import_) + ] + }); let unqualified_values = unqualified_values .iter() .sorted_by(|a, b| a.name.cmp(&b.name)) - .map(|value| value.to_doc(arena)); + .map(|import_| self.unqualified_import(arena, import_)); let unqualified = arena.join( unqualified_types.chain(unqualified_values), FLEX_COMMA_DOCUMENT, @@ -966,6 +985,20 @@ impl<'a, 'doc> Formatter<'a> { ) } + fn argument_names( + &self, + arena: &'doc DocumentArena<'a, 'doc>, + argument_names: &'a ArgNames, + ) -> Document<'a, 'doc> { + match argument_names { + ArgNames::Named { name, .. } | ArgNames::Discard { name, .. } => name.to_doc(arena), + ArgNames::LabelledDiscard { label, name, .. } + | ArgNames::NamedLabelled { label, name, .. } => { + docvec![arena, label, " ", name] + } + } + } + fn fn_arg( &mut self, arena: &'doc DocumentArena<'a, 'doc>, @@ -973,10 +1006,9 @@ impl<'a, 'doc> Formatter<'a> { ) -> Document<'a, 'doc> { let comments = self.pop_comments(argument.location.start); let doc = match &argument.annotation { - None => argument.names.to_doc(arena), - Some(type_) => argument - .names - .to_doc(arena) + None => self.argument_names(arena, &argument.names), + Some(type_) => self + .argument_names(arena, &argument.names) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.type_ast(arena, type_)), } @@ -3858,30 +3890,6 @@ fn init_and_last(vec: &[T]) -> Option<(&[T], &T)> { } } -impl<'a, 'doc> Documentable<'a, 'doc> for &'a ArgNames { - fn to_doc(self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { - match self { - ArgNames::Named { name, .. } | ArgNames::Discard { name, .. } => name.to_doc(arena), - ArgNames::LabelledDiscard { label, name, .. } - | ArgNames::NamedLabelled { label, name, .. } => { - docvec![arena, label, " ", name] - } - } - } -} - -impl<'a, 'doc> Documentable<'a, 'doc> for &'a UnqualifiedImport { - fn to_doc(self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { - self.name.as_str().to_doc(arena).append( - arena, - match &self.as_name { - None => EMPTY_DOCUMENT, - Some(s) => " as ".to_doc(arena).append(arena, s.as_str()), - }, - ) - } -} - fn binop<'a, 'doc>(binop: BinOp) -> Document<'a, 'doc> { match binop { BinOp::And => AND_DOCUMENT, diff --git a/compiler-core/src/format/tests.rs b/format/src/tests.rs similarity index 99% rename from compiler-core/src/format/tests.rs rename to format/src/tests.rs index ba4d79263..214dce0da 100644 --- a/compiler-core/src/format/tests.rs +++ b/format/src/tests.rs @@ -28,8 +28,7 @@ mod use_; macro_rules! assert_format { ($src:expr $(,)?) => { let mut writer = String::new(); - $crate::format::pretty(&mut writer, &$src.into(), camino::Utf8Path::new("")) - .unwrap(); + $crate::pretty(&mut writer, &$src.into(), camino::Utf8Path::new("")).unwrap(); assert_eq!($src, writer); }; } @@ -38,8 +37,7 @@ macro_rules! assert_format { macro_rules! assert_format_rewrite { ($src:expr, $expected:expr $(,)?) => { let mut writer = String::new(); - $crate::format::pretty(&mut writer, &$src.into(), camino::Utf8Path::new("")) - .unwrap(); + $crate::pretty(&mut writer, &$src.into(), camino::Utf8Path::new("")).unwrap(); assert_eq!(writer, $expected); }; } diff --git a/compiler-core/src/format/tests/asignments.rs b/format/src/tests/asignments.rs similarity index 100% rename from compiler-core/src/format/tests/asignments.rs rename to format/src/tests/asignments.rs diff --git a/compiler-core/src/format/tests/binary_operators.rs b/format/src/tests/binary_operators.rs similarity index 100% rename from compiler-core/src/format/tests/binary_operators.rs rename to format/src/tests/binary_operators.rs diff --git a/compiler-core/src/format/tests/bit_array.rs b/format/src/tests/bit_array.rs similarity index 100% rename from compiler-core/src/format/tests/bit_array.rs rename to format/src/tests/bit_array.rs diff --git a/compiler-core/src/format/tests/blocks.rs b/format/src/tests/blocks.rs similarity index 100% rename from compiler-core/src/format/tests/blocks.rs rename to format/src/tests/blocks.rs diff --git a/compiler-core/src/format/tests/cases.rs b/format/src/tests/cases.rs similarity index 100% rename from compiler-core/src/format/tests/cases.rs rename to format/src/tests/cases.rs diff --git a/compiler-core/src/format/tests/conditional_compilation.rs b/format/src/tests/conditional_compilation.rs similarity index 100% rename from compiler-core/src/format/tests/conditional_compilation.rs rename to format/src/tests/conditional_compilation.rs diff --git a/compiler-core/src/format/tests/constant.rs b/format/src/tests/constant.rs similarity index 100% rename from compiler-core/src/format/tests/constant.rs rename to format/src/tests/constant.rs diff --git a/compiler-core/src/format/tests/custom_type.rs b/format/src/tests/custom_type.rs similarity index 100% rename from compiler-core/src/format/tests/custom_type.rs rename to format/src/tests/custom_type.rs diff --git a/compiler-core/src/format/tests/echo.rs b/format/src/tests/echo.rs similarity index 100% rename from compiler-core/src/format/tests/echo.rs rename to format/src/tests/echo.rs diff --git a/compiler-core/src/format/tests/external_fn.rs b/format/src/tests/external_fn.rs similarity index 100% rename from compiler-core/src/format/tests/external_fn.rs rename to format/src/tests/external_fn.rs diff --git a/compiler-core/src/format/tests/external_types.rs b/format/src/tests/external_types.rs similarity index 100% rename from compiler-core/src/format/tests/external_types.rs rename to format/src/tests/external_types.rs diff --git a/compiler-core/src/format/tests/function.rs b/format/src/tests/function.rs similarity index 100% rename from compiler-core/src/format/tests/function.rs rename to format/src/tests/function.rs diff --git a/compiler-core/src/format/tests/guards.rs b/format/src/tests/guards.rs similarity index 100% rename from compiler-core/src/format/tests/guards.rs rename to format/src/tests/guards.rs diff --git a/compiler-core/src/format/tests/imports.rs b/format/src/tests/imports.rs similarity index 100% rename from compiler-core/src/format/tests/imports.rs rename to format/src/tests/imports.rs diff --git a/compiler-core/src/format/tests/lists.rs b/format/src/tests/lists.rs similarity index 100% rename from compiler-core/src/format/tests/lists.rs rename to format/src/tests/lists.rs diff --git a/compiler-core/src/format/tests/pipeline.rs b/format/src/tests/pipeline.rs similarity index 100% rename from compiler-core/src/format/tests/pipeline.rs rename to format/src/tests/pipeline.rs diff --git a/compiler-core/src/format/tests/record_update.rs b/format/src/tests/record_update.rs similarity index 100% rename from compiler-core/src/format/tests/record_update.rs rename to format/src/tests/record_update.rs diff --git a/compiler-core/src/format/tests/tuple.rs b/format/src/tests/tuple.rs similarity index 100% rename from compiler-core/src/format/tests/tuple.rs rename to format/src/tests/tuple.rs diff --git a/compiler-core/src/format/tests/use_.rs b/format/src/tests/use_.rs similarity index 100% rename from compiler-core/src/format/tests/use_.rs rename to format/src/tests/use_.rs diff --git a/language-server/Cargo.toml b/language-server/Cargo.toml index 932a7a4a7..b4693f069 100644 --- a/language-server/Cargo.toml +++ b/language-server/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" [dependencies] gleam-core = { path = "../compiler-core" } +gleam-format = { path = "../format" } camino.workspace = true debug-ignore.workspace = true diff --git a/language-server/src/server.rs b/language-server/src/server.rs index 411058c25..a44ad61bf 100644 --- a/language-server/src/server.rs +++ b/language-server/src/server.rs @@ -338,7 +338,7 @@ where Err(error) => return self.path_error_response(path, error), }; - if let Err(error) = gleam_core::format::pretty(&mut new_text, &src, &path) { + if let Err(error) = gleam_format::pretty(&mut new_text, &src, &path) { return self.path_error_response(path, error); }