Merge branch 'main' into main

This commit is contained in:
Anton-4 2024-05-10 14:29:09 +02:00 committed by GitHub
commit b5a9dc3ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 789 additions and 220 deletions

View File

@ -11,8 +11,6 @@ jobs:
name: test zig, rust, wasm...
runs-on: [self-hosted, i7-6700K]
timeout-minutes: 90
env:
RUSTC_WRAPPER: /home/big-ci-user/.cargo/bin/sccache
steps:
- uses: actions/checkout@v4
@ -39,7 +37,7 @@ jobs:
- name: regular rust tests
# see #5904 for skipped test
run: cargo test --locked --release -- --skip cli_run::expects_dev_and_test && sccache --show-stats
run: cargo test --locked --release -- --skip cli_run::expects_dev_and_test
- name: tests examples in docs
run: cargo test --doc --release
@ -48,19 +46,19 @@ jobs:
run: cd examples/platform-switching/rust-platform && LD_LIBRARY_PATH=. cargo test --release --locked
- name: test the dev backend # these tests require an explicit feature flag
run: cargo test --locked --release --package test_gen --no-default-features --features gen-dev && sccache --show-stats
run: cargo test --locked --release --package test_gen --no-default-features --features gen-dev
- name: test gen-wasm single threaded # gen-wasm has some multithreading problems to do with the wasmer runtime
run: cargo test --locked --release --package test_gen --no-default-features --features gen-wasm -- --test-threads=1 && sccache --show-stats
run: cargo test --locked --release --package test_gen --no-default-features --features gen-wasm -- --test-threads=1
- name: roc test all builtins
run: ./ci/roc_test_builtins.sh
- name: wasm repl test
run: crates/repl_test/test_wasm.sh && sccache --show-stats
run: crates/repl_test/test_wasm.sh
- name: test building wasm repl
run: ./ci/www-repl.sh && sccache --show-stats
run: ./ci/www-repl.sh
#TODO i386 (32-bit linux) cli tests
#TODO verify-no-git-changes

View File

@ -969,6 +969,21 @@ mod cli_run {
TestCliCommands::Run,
)
}
#[test]
#[serial(cli_platform)]
#[cfg_attr(windows, ignore)]
fn ingested_file_bytes_no_ann() {
test_roc_app(
"examples/cli",
"ingested-file-bytes-no-ann.roc",
&[],
&[],
&[],
"162088\n",
UseValgrind::No,
TestCliCommands::Run,
)
}
#[test]
#[serial(zig_platform_parser_package_basic_cli_url)]

View File

@ -167,7 +167,7 @@ enum PendingValueDef<'a> {
/// Ingested file
IngestedFile(
Loc<Pattern>,
Loc<ast::TypeAnnotation<'a>>,
Option<Loc<ast::TypeAnnotation<'a>>>,
Loc<ast::StrLiteral<'a>>,
),
}
@ -2336,7 +2336,7 @@ fn canonicalize_pending_value_def<'a>(
None,
)
}
IngestedFile(loc_pattern, loc_ann, path_literal) => {
IngestedFile(loc_pattern, opt_loc_ann, path_literal) => {
let relative_path =
if let ast::StrLiteral::PlainLine(ingested_path) = path_literal.value {
ingested_path
@ -2373,23 +2373,29 @@ fn canonicalize_pending_value_def<'a>(
let loc_expr = Loc::at(path_literal.region, expr);
let can_ann = canonicalize_annotation(
env,
scope,
&loc_ann.value,
loc_ann.region,
var_store,
pending_abilities_in_scope,
AnnotationFor::Value,
);
let opt_loc_can_ann = if let Some(loc_ann) = opt_loc_ann {
let can_ann = canonicalize_annotation(
env,
scope,
&loc_ann.value,
loc_ann.region,
var_store,
pending_abilities_in_scope,
AnnotationFor::Value,
);
output.references.union_mut(&can_ann.references);
output.references.union_mut(&can_ann.references);
Some(Loc::at(loc_ann.region, can_ann))
} else {
None
};
let def = single_can_def(
loc_pattern,
loc_expr,
var_store.fresh(),
Some(Loc::at(loc_ann.region, can_ann)),
opt_loc_can_ann,
SendMap::default(),
);
@ -3108,9 +3114,9 @@ fn to_pending_value_def<'a>(
})
}
IngestedFileImport(ingested_file) => {
let typed_ident = ingested_file.name.item.extract_spaces().item;
let loc_name = ingested_file.name.item;
let symbol = match scope.introduce(typed_ident.ident.value.into(), typed_ident.ident.region) {
let symbol = match scope.introduce(loc_name.value.into(), loc_name.region) {
Ok(symbol ) => symbol,
Err((original, shadow, _)) => {
env.problem(Problem::Shadowing {
@ -3123,9 +3129,9 @@ fn to_pending_value_def<'a>(
}
};
let loc_pattern = Loc::at(typed_ident.ident.region, Pattern::Identifier(symbol));
let loc_pattern = Loc::at(loc_name.region, Pattern::Identifier(symbol));
PendingValue::Def(PendingValueDef::IngestedFile(loc_pattern, typed_ident.ann, ingested_file.path))
PendingValue::Def(PendingValueDef::IngestedFile(loc_pattern, ingested_file.annotation.map(|ann| ann.annotation), ingested_file.path))
}
Stmt(_) => internal_error!("a Stmt was not desugared correctly, should have been converted to a Body(...) in desguar"),
}

View File

@ -6,8 +6,8 @@ use crate::spaces::{fmt_default_newline, fmt_default_spaces, fmt_spaces, INDENT}
use crate::Buf;
use roc_parse::ast::{
AbilityMember, Defs, Expr, ExtractSpaces, ImportAlias, ImportAsKeyword, ImportExposingKeyword,
ImportedModuleName, IngestedFileImport, ModuleImport, Pattern, Spaces, StrLiteral,
TypeAnnotation, TypeDef, TypeHeader, ValueDef,
ImportedModuleName, IngestedFileAnnotation, IngestedFileImport, ModuleImport, Pattern, Spaces,
StrLiteral, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
};
use roc_parse::header::Keyword;
use roc_region::all::Loc;
@ -257,8 +257,9 @@ impl<'a> Formattable for IngestedFileImport<'a> {
before_path,
path: _,
name,
annotation,
} = self;
!before_path.is_empty() || name.is_multiline()
!before_path.is_empty() || name.keyword.is_multiline() || annotation.is_multiline()
}
fn format_with_options(
@ -272,6 +273,7 @@ impl<'a> Formattable for IngestedFileImport<'a> {
before_path,
path,
name,
annotation,
} = self;
buf.indent(indent);
@ -281,7 +283,11 @@ impl<'a> Formattable for IngestedFileImport<'a> {
fmt_default_spaces(buf, before_path, indent);
fmt_str_literal(buf, path.value, indent);
name.format(buf, indent);
name.keyword.format(buf, indent);
buf.push_str(name.item.value);
annotation.format(buf, indent);
}
}
@ -361,6 +367,34 @@ impl Formattable for ImportExposingKeyword {
}
}
impl<'a> Formattable for IngestedFileAnnotation<'a> {
fn is_multiline(&self) -> bool {
let Self {
before_colon,
annotation,
} = self;
!before_colon.is_empty() || annotation.is_multiline()
}
fn format_with_options(
&self,
buf: &mut Buf,
_parens: Parens,
_newlines: Newlines,
indent: u16,
) {
let Self {
before_colon,
annotation,
} = self;
fmt_default_spaces(buf, before_colon, indent);
buf.push_str(":");
buf.spaces(1);
annotation.format(buf, indent);
}
}
impl<'a> Formattable for ValueDef<'a> {
fn is_multiline(&self) -> bool {
use roc_parse::ast::ValueDef::*;

View File

@ -5,9 +5,10 @@ use roc_parse::{
ast::{
AbilityImpls, AbilityMember, AssignedField, Collection, CommentOrNewline, Defs, Expr,
Header, Implements, ImplementsAbilities, ImplementsAbility, ImplementsClause, ImportAlias,
ImportAsKeyword, ImportExposingKeyword, ImportedModuleName, IngestedFileImport, Module,
ModuleImport, Pattern, PatternAs, RecordBuilderField, Spaced, Spaces, StrLiteral,
StrSegment, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef, WhenBranch,
ImportAsKeyword, ImportExposingKeyword, ImportedModuleName, IngestedFileAnnotation,
IngestedFileImport, Module, ModuleImport, Pattern, PatternAs, RecordBuilderField, Spaced,
Spaces, StrLiteral, StrSegment, Tag, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
WhenBranch,
},
header::{
AppHeader, ExposedName, HostedHeader, ImportsEntry, KeywordItem, ModuleHeader, ModuleName,
@ -600,6 +601,7 @@ impl<'a> RemoveSpaces<'a> for IngestedFileImport<'a> {
before_path: &[],
path: self.path.remove_spaces(arena),
name: self.name.remove_spaces(arena),
annotation: self.annotation.remove_spaces(arena),
}
}
}
@ -631,6 +633,15 @@ impl<'a> RemoveSpaces<'a> for ImportExposingKeyword {
}
}
impl<'a> RemoveSpaces<'a> for IngestedFileAnnotation<'a> {
fn remove_spaces(&self, arena: &'a Bump) -> Self {
IngestedFileAnnotation {
before_colon: &[],
annotation: self.annotation.remove_spaces(arena),
}
}
}
impl<'a> RemoveSpaces<'a> for Implements<'a> {
fn remove_spaces(&self, _arena: &'a Bump) -> Self {
Implements::Implements

View File

@ -4914,6 +4914,176 @@ mod test_reporting {
"
);
test_report!(
unfinished_import,
indoc!(
r"
import [
"
),
@r###"
UNFINISHED IMPORT in tmp/unfinished_import/Test.roc
I was partway through parsing an `import`, but I got stuck here:
4 import [
^
I was expecting to see a module name, like:
import BigNum
Or a package module name, like:
import pf.Stdout
Or a file path to ingest, like:
import "users.json" as users : Str
"###
);
test_report!(
unfinished_import_as_or_exposing,
indoc!(
r"
import svg.Path a
"
),
@r###"
UNFINISHED IMPORT in tmp/unfinished_import_as_or_exposing/Test.roc
I was partway through parsing an `import`, but I got stuck here:
4 import svg.Path a
^
I was expecting to see the `as` keyword, like:
import svg.Path as SvgPath
Or the `exposing` keyword, like:
import svg.Path exposing [arc, rx]
"###
);
test_report!(
unfinished_import_alias,
indoc!(
r"
import svg.Path as
"
),
@r###"
UNFINISHED IMPORT in tmp/unfinished_import_alias/Test.roc
I was partway through parsing an `import`, but I got stuck here:
4 import svg.Path as
^
I just saw the `as` keyword, so I was expecting to see an alias next.
"###
);
test_report!(
lowercase_import_alias,
indoc!(
r"
import svg.Path as path
"
),
@r###"
LOWERCASE ALIAS in tmp/lowercase_import_alias/Test.roc
This import is using a lowercase alias:
4 import svg.Path as path
^^^^
Module names and aliases must start with an uppercase letter.
"###
);
test_report!(
unfinished_import_exposing,
indoc!(
r"
import svg.Path exposing
"
),
@r###"
UNFINISHED IMPORT in tmp/unfinished_import_exposing/Test.roc
I was partway through parsing an `import`, but I got stuck here:
4 import svg.Path exposing
^
I just saw the `exposing` keyword, so I was expecting to see `[` next.
"###);
test_report!(
unfinished_import_exposing_name,
indoc!(
r"
import svg.Path exposing [3
"
),
@r###"
WEIRD EXPOSING in tmp/unfinished_import_exposing_name/Test.roc
I'm partway through parsing an exposing list, but I got stuck here:
4 import svg.Path exposing [3
^
I was expecting a type, value, or function name next, like:
import Svg exposing [Path, arc, rx]
"###);
test_report!(
unfinished_ingested_file_name,
indoc!(
r#"
import "example.json" as
"#
),
@r###"
UNFINISHED IMPORT in tmp/unfinished_ingested_file_name/Test.roc
I was partway through parsing an `import`, but I got stuck here:
4 import "example.json" as
^
I was expecting to see a name next, like:
import "users.json" as users : Str
"###
);
test_report!(
ingested_file_import_ann_syntax_err,
indoc!(
r#"
import "example.json" as example : List U8, U32
"#
),
@r###"
UNFINISHED TYPE in tmp/ingested_file_import_ann_syntax_err/Test.roc
I am partway through parsing a type, but I got stuck here:
4 import "example.json" as example : List U8, U32
^
Note: I may be confused by indentation
"###
);
// TODO could do better by pointing out we're parsing a function type
test_report!(
dict_type_formatting,

View File

@ -1,5 +1,6 @@
use std::fmt::Debug;
use crate::expr::merge_spaces;
use crate::header::{
self, AppHeader, HostedHeader, ModuleHeader, ModuleName, PackageHeader, PlatformHeader,
};
@ -162,6 +163,8 @@ impl<'a> Module<'a> {
Self::header_import_to_value_def(None, name, exposed, import.region)
}
header::ImportsEntry::IngestedFile(path, typed_ident) => {
let typed_ident = typed_ident.extract_spaces();
ValueDef::IngestedFileImport(IngestedFileImport {
before_path: &[],
path: Loc {
@ -174,11 +177,16 @@ impl<'a> Module<'a> {
item: ImportAsKeyword,
after: &[],
},
item: Loc {
value: typed_ident,
region: import.region,
},
item: typed_ident.item.ident,
},
annotation: Some(IngestedFileAnnotation {
before_colon: merge_spaces(
arena,
typed_ident.before,
typed_ident.item.spaces_before_colon,
),
annotation: typed_ident.item.ann,
}),
})
}
};
@ -1052,7 +1060,20 @@ pub struct ModuleImport<'a> {
pub struct IngestedFileImport<'a> {
pub before_path: &'a [CommentOrNewline<'a>],
pub path: Loc<StrLiteral<'a>>,
pub name: header::KeywordItem<'a, ImportAsKeyword, Loc<Spaced<'a, header::TypedIdent<'a>>>>,
pub name: header::KeywordItem<'a, ImportAsKeyword, Loc<&'a str>>,
pub annotation: Option<IngestedFileAnnotation<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IngestedFileAnnotation<'a> {
pub before_colon: &'a [CommentOrNewline<'a>],
pub annotation: Loc<TypeAnnotation<'a>>,
}
impl<'a> Malformed for IngestedFileAnnotation<'a> {
fn is_malformed(&self) -> bool {
self.annotation.value.is_malformed()
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
@ -2621,7 +2642,8 @@ impl<'a> Malformed for ValueDef<'a> {
before_path: _,
path,
name: _,
}) => path.is_malformed(),
annotation,
}) => path.is_malformed() || annotation.is_malformed(),
ValueDef::Stmt(loc_expr) => loc_expr.is_malformed(),
}
}

View File

@ -1,16 +1,15 @@
use crate::ast::{
is_expr_suffixed, AssignedField, Collection, CommentOrNewline, Defs, Expr, ExtractSpaces,
Implements, ImplementsAbilities, ImportAlias, ImportAsKeyword, ImportExposingKeyword,
ImportedModuleName, IngestedFileImport, ModuleImport, Pattern, RecordBuilderField, Spaceable,
Spaced, Spaces, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
ImportedModuleName, IngestedFileAnnotation, IngestedFileImport, ModuleImport, Pattern,
RecordBuilderField, Spaceable, Spaced, Spaces, TypeAnnotation, TypeDef, TypeHeader, ValueDef,
};
use crate::blankspace::{
space0_after_e, space0_around_e_no_after_indent_check, space0_around_ee, space0_before_e,
space0_before_optional_after, space0_e, spaces, spaces_around, spaces_before,
};
use crate::ident::{
integer_ident, lowercase_ident, parse_ident, unqualified_ident, uppercase_ident, Accessor,
Ident, Suffix,
integer_ident, lowercase_ident, parse_ident, unqualified_ident, Accessor, Ident, Suffix,
};
use crate::module::module_name_help;
use crate::parser::{
@ -72,20 +71,6 @@ pub struct ExprParseOptions {
///
/// > Just foo if foo == 2 -> ...
pub check_for_arrow: bool,
/// Check for a suffixed expression, if we find one then
/// subsequent parsing for this expression should have an increased
/// indent, this is so we can distinguish between the end of the
/// statement and the next expression.
pub suffixed_found: bool,
}
impl ExprParseOptions {
pub fn set_suffixed_found(&self) -> Self {
let mut new = *self;
new.suffixed_found = true;
new
}
}
pub fn expr_help<'a>() -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
@ -368,10 +353,10 @@ fn expr_operator_chain<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a
let initial_state = state.clone();
let end = state.pos();
let new_options = if is_expr_suffixed(&expr.value) {
options.set_suffixed_found()
let new_min_indent = if is_expr_suffixed(&expr.value) {
min_indent + 1
} else {
options
min_indent
};
match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
@ -386,8 +371,8 @@ fn expr_operator_chain<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a
};
match parse_expr_end(
min_indent,
new_options,
new_min_indent,
options,
expr_state,
arena,
state,
@ -656,8 +641,9 @@ pub fn parse_single_def<'a>(
min_indent,
) {
Err((NoProgress, _)) => {
match loc!(import()).parse(arena, state.clone(), min_indent) {
Err((_, _)) => {
let pos_before_import = state.pos();
match import().parse(arena, state.clone(), min_indent) {
Err((NoProgress, _)) => {
match parse_expect.parse(arena, state.clone(), min_indent) {
Err((_, _)) => {
// a hacky way to get expression-based error messages. TODO fix this
@ -684,12 +670,16 @@ pub fn parse_single_def<'a>(
),
}
}
Ok((_, loc_import, state)) => Ok((
Err((MadeProgress, err)) => {
Err((MadeProgress, EExpr::Import(err, pos_before_import)))
}
Ok((_, (loc_import, spaces_after), state)) => Ok((
MadeProgress,
Some(SingleDef {
type_or_value: Either::Second(loc_import.value),
region: loc_import.region,
spaces_before: spaces_before_current,
spaces_after,
}),
state,
)),
@ -753,6 +743,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::First(type_def),
region: def_region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -813,6 +804,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::First(type_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -836,6 +828,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::First(type_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -849,6 +842,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -887,6 +881,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::First(type_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -911,6 +906,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::First(type_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -924,6 +920,7 @@ pub fn parse_single_def<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
));
@ -957,10 +954,23 @@ pub fn parse_single_def<'a>(
}
}
fn import<'a>() -> impl Parser<'a, ValueDef<'a>, EImport<'a>> {
skip_first!(
parser::keyword(keyword::IMPORT, EImport::Import),
increment_min_indent(one_of!(import_body(), import_ingested_file_body()))
fn import<'a>() -> impl Parser<'a, (Loc<ValueDef<'a>>, &'a [CommentOrNewline<'a>]), EImport<'a>> {
then(
and!(
loc!(skip_first!(
parser::keyword(keyword::IMPORT, EImport::Import),
increment_min_indent(one_of!(import_body(), import_ingested_file_body()))
)),
space0_e(EImport::EndNewline)
),
|_arena, state, progress, (import, spaces_after)| {
if !spaces_after.is_empty() || state.has_reached_end() {
Ok((progress, (import, spaces_after), state))
} else {
// We require EOF, comment, or newline after import
Err((progress, EImport::EndNewline(state.pos())))
}
},
)
}
@ -969,8 +979,8 @@ fn import_body<'a>() -> impl Parser<'a, ValueDef<'a>, EImport<'a>> {
record!(ModuleImport {
before_name: space0_e(EImport::IndentStart),
name: loc!(imported_module_name()),
alias: optional(backtrackable(import_as())),
exposed: optional(backtrackable(import_exposing()))
alias: optional(import_as()),
exposed: optional(import_exposing())
}),
ValueDef::ModuleImport
)
@ -997,10 +1007,20 @@ fn import_as<'a>(
EImport::IndentAs,
EImport::IndentAlias
),
item: loc!(map!(
specialize_err(|_, pos| EImport::Alias(pos), uppercase_ident()),
ImportAlias::new
))
item: then(
specialize_err(|_, pos| EImport::Alias(pos), loc!(unqualified_ident())),
|_arena, state, _progress, loc_ident| {
match loc_ident.value.chars().next() {
Some(first) if first.is_uppercase() => Ok((
MadeProgress,
loc_ident.map(|ident| ImportAlias::new(ident)),
state,
)),
Some(_) => Err((MadeProgress, EImport::LowercaseAlias(loc_ident.region))),
None => Err((MadeProgress, EImport::Alias(state.pos()))),
}
}
)
})
}
@ -1050,17 +1070,15 @@ fn import_ingested_file_body<'a>() -> impl Parser<'a, ValueDef<'a>, EImport<'a>>
string_literal::parse_str_literal()
)),
name: import_ingested_file_as(),
annotation: optional(import_ingested_file_annotation())
}),
ValueDef::IngestedFileImport
)
}
#[inline(always)]
fn import_ingested_file_as<'a>() -> impl Parser<
'a,
header::KeywordItem<'a, ImportAsKeyword, Loc<Spaced<'a, header::TypedIdent<'a>>>>,
EImport<'a>,
> {
fn import_ingested_file_as<'a>(
) -> impl Parser<'a, header::KeywordItem<'a, ImportAsKeyword, Loc<&'a str>>, EImport<'a>> {
record!(header::KeywordItem {
keyword: module::spaces_around_keyword(
ImportAsKeyword,
@ -1068,7 +1086,22 @@ fn import_ingested_file_as<'a>() -> impl Parser<
EImport::IndentAs,
EImport::IndentIngestedName
),
item: specialize_err(EImport::IngestedName, loc!(module::typed_ident()))
item: specialize_err(
|(), pos| EImport::IngestedName(pos),
loc!(lowercase_ident())
)
})
}
#[inline(always)]
fn import_ingested_file_annotation<'a>() -> impl Parser<'a, IngestedFileAnnotation<'a>, EImport<'a>>
{
record!(IngestedFileAnnotation {
before_colon: skip_second!(
backtrackable(space0_e(EImport::IndentColon)),
byte(b':', EImport::Colon)
),
annotation: specialize_err(EImport::Annotation, type_annotation::located(false))
})
}
@ -1120,6 +1153,7 @@ pub fn parse_single_def_assignment<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state_after_rest_of_def,
));
@ -1141,6 +1175,7 @@ pub fn parse_single_def_assignment<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state_after_first_expression,
));
@ -1156,6 +1191,7 @@ pub fn parse_single_def_assignment<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state_after_first_expression,
))
@ -1203,6 +1239,7 @@ fn parse_statement_inside_def<'a>(
type_or_value: Either::Second(value_def),
region,
spaces_before: spaces_before_current,
spaces_after: &[],
}),
state,
))
@ -1286,10 +1323,16 @@ fn parse_defs_end<'a>(
Ok((_, Some(single_def), next_state)) => {
let region = single_def.region;
let spaces_before_current = single_def.spaces_before;
let spaces_after_current = single_def.spaces_after;
match single_def.type_or_value {
Either::First(type_def) => {
defs.push_type_def(type_def, region, spaces_before_current, &[]);
defs.push_type_def(
type_def,
region,
spaces_before_current,
spaces_after_current,
);
}
Either::Second(value_def) => {
// If we got a ValueDef::Body, check if a type annotation preceded it.
@ -1351,7 +1394,12 @@ fn parse_defs_end<'a>(
if !joined {
// the previous and current def can't be joined up
defs.push_value_def(value_def, region, spaces_before_current, &[]);
defs.push_value_def(
value_def,
region,
spaces_before_current,
spaces_after_current,
);
}
}
}
@ -1373,6 +1421,7 @@ pub struct SingleDef<'a> {
pub type_or_value: Either<TypeDef<'a>, ValueDef<'a>>,
pub region: Region,
pub spaces_before: &'a [CommentOrNewline<'a>],
pub spaces_after: &'a [CommentOrNewline<'a>],
}
fn parse_defs_expr<'a>(
@ -1993,15 +2042,15 @@ fn parse_expr_operator<'a>(
expr_state.end = new_end;
expr_state.spaces_after = spaces;
let new_options = if is_expr_suffixed(&new_expr.value) {
options.set_suffixed_found()
let new_min_indent = if is_expr_suffixed(&new_expr.value) {
min_indent + 1
} else {
options
min_indent
};
match parse_expr_end(
min_indent,
new_options,
new_min_indent,
options,
expr_state,
arena,
state,
@ -2056,18 +2105,12 @@ fn parse_expr_end<'a>(
state: State<'a>,
initial_state: State<'a>,
) -> ParseResult<'a, Expr<'a>, EExpr<'a>> {
let inner_min_indent = if options.suffixed_found {
min_indent + 1
} else {
min_indent
};
let parser = skip_first!(
crate::blankspace::check_indent(EExpr::IndentEnd),
loc_term_or_underscore(options)
);
match parser.parse(arena, state.clone(), inner_min_indent) {
match parser.parse(arena, state.clone(), min_indent) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((
_,
@ -2125,10 +2168,10 @@ fn parse_expr_end<'a>(
Ok((_, mut arg, state)) => {
let new_end = state.pos();
let new_options = if is_expr_suffixed(&arg.value) {
options.set_suffixed_found()
let min_indent = if is_expr_suffixed(&arg.value) {
min_indent + 1
} else {
options
min_indent
};
// now that we have `function arg1 ... <spaces> argn`, attach the spaces to the `argn`
@ -2155,14 +2198,7 @@ fn parse_expr_end<'a>(
expr_state.end = new_end;
expr_state.spaces_after = new_spaces;
parse_expr_end(
min_indent,
new_options,
expr_state,
arena,
state,
initial_state,
)
parse_expr_end(min_indent, options, expr_state, arena, state, initial_state)
}
}
}
@ -2280,7 +2316,6 @@ pub fn loc_expr<'a>(accept_multi_backpassing: bool) -> impl Parser<'a, Loc<Expr<
expr_start(ExprParseOptions {
accept_multi_backpassing,
check_for_arrow: true,
suffixed_found: false,
})
}
@ -2479,7 +2514,6 @@ pub fn parse_top_level_defs<'a>(
let options = ExprParseOptions {
accept_multi_backpassing: true,
check_for_arrow: true,
suffixed_found: false,
};
let existing_len = output.tags.len();
@ -2860,11 +2894,11 @@ fn dbg_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpect<
fn import_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpr<'a>> {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| {
let (_, import_def, state) =
loc!(specialize_err(EExpr::Import, import())).parse(arena, state, min_indent)?;
let (_, (import_def, spaces_after), state) =
specialize_err(EExpr::Import, import()).parse(arena, state, min_indent)?;
let mut defs = Defs::default();
defs.push_value_def(import_def.value, import_def.region, &[], &[]);
defs.push_value_def(import_def.value, import_def.region, &[], spaces_after);
parse_defs_expr(options, min_indent, defs, arena, state)
}

View File

@ -534,6 +534,7 @@ pub enum EImport<'a> {
As(Position),
IndentAlias(Position),
Alias(Position),
LowercaseAlias(Region),
IndentExposing(Position),
Exposing(Position),
ExposingListStart(Position),
@ -542,8 +543,13 @@ pub enum EImport<'a> {
IndentIngestedPath(Position),
IngestedPath(Position),
IndentIngestedName(Position),
IngestedName(ETypedIdent<'a>, Position),
IngestedName(Position),
IndentColon(Position),
Colon(Position),
IndentAnnotation(Position),
Annotation(EType<'a>, Position),
Space(BadInputError, Position),
EndNewline(Position),
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -1 +1 @@
NotEndOfFile(@12)
Expr(Import(LowercaseAlias(@15-19), @0), @0)

View File

@ -13,14 +13,14 @@ Defs {
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 1, length = 0),
Slice(start = 2, length = 0),
Slice(start = 3, length = 0),
],
space_after: [
Slice(start = 0, length = 1),
Slice(start = 1, length = 1),
Slice(start = 2, length = 1),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 1, length = 0),
Slice(start = 2, length = 0),
Slice(start = 3, length = 0),
],
spaces: [

View File

@ -9,10 +9,10 @@ Defs {
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 0),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 0),
],
spaces: [

View File

@ -31,6 +31,20 @@ Defs {
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 2, length = 0),
Slice(start = 4, length = 0),
Slice(start = 6, length = 0),
Slice(start = 8, length = 0),
Slice(start = 10, length = 0),
Slice(start = 12, length = 0),
Slice(start = 14, length = 0),
Slice(start = 16, length = 0),
Slice(start = 18, length = 0),
Slice(start = 20, length = 0),
Slice(start = 23, length = 0),
Slice(start = 25, length = 0),
],
space_after: [
Slice(start = 0, length = 2),
Slice(start = 2, length = 2),
Slice(start = 4, length = 2),
@ -43,20 +57,6 @@ Defs {
Slice(start = 18, length = 2),
Slice(start = 20, length = 3),
Slice(start = 23, length = 2),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 2, length = 0),
Slice(start = 4, length = 0),
Slice(start = 6, length = 0),
Slice(start = 8, length = 0),
Slice(start = 10, length = 0),
Slice(start = 12, length = 0),
Slice(start = 14, length = 0),
Slice(start = 16, length = 0),
Slice(start = 18, length = 0),
Slice(start = 20, length = 0),
Slice(start = 23, length = 0),
Slice(start = 25, length = 0),
],
spaces: [

View File

@ -11,12 +11,12 @@ Defs {
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 1),
Slice(start = 1, length = 0),
Slice(start = 2, length = 0),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 1, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 1),
Slice(start = 2, length = 0),
],
spaces: [

View File

@ -9,10 +9,10 @@ Defs {
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 0),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
Slice(start = 1, length = 0),
],
spaces: [
@ -32,16 +32,18 @@ Defs {
item: ImportAsKeyword,
after: [],
},
item: @29-39 TypedIdent {
ident: @29-33 "file",
spaces_before_colon: [],
ann: @36-39 Apply(
item: @29-33 "file",
},
annotation: Some(
IngestedFileAnnotation {
before_colon: [],
annotation: @36-39 Apply(
"",
"Str",
[],
),
},
},
),
},
),
IngestedFileImport(
@ -56,10 +58,12 @@ Defs {
item: ImportAsKeyword,
after: [],
},
item: @70-85 TypedIdent {
ident: @70-74 "file",
spaces_before_colon: [],
ann: @78-85 Apply(
item: @70-74 "file",
},
annotation: Some(
IngestedFileAnnotation {
before_colon: [],
annotation: @78-85 Apply(
"",
"List",
[
@ -71,7 +75,7 @@ Defs {
],
),
},
},
),
},
),
],

View File

@ -9,15 +9,17 @@ Defs(
@27-51,
],
space_before: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 1),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 1, length = 0),
],
space_after: [
Slice(start = 0, length = 1),
Slice(start = 1, length = 2),
],
spaces: [
Newline,
Newline,
Newline,
],
type_defs: [],
value_defs: [
@ -73,33 +75,27 @@ Defs(
),
],
},
@53-71 SpaceBefore(
Apply(
@53-62 Var {
module_name: "JE",
ident: "encode",
},
[
@64-70 ParensAround(
Apply(
@64-67 Var {
module_name: "",
ident: "int",
},
[
@68-70 Num(
"42",
),
],
Space,
),
),
],
Space,
),
@53-71 Apply(
@53-62 Var {
module_name: "JE",
ident: "encode",
},
[
Newline,
Newline,
@64-70 ParensAround(
Apply(
@64-67 Var {
module_name: "",
ident: "int",
},
[
@68-70 Num(
"42",
),
],
Space,
),
),
],
Space,
),
)

View File

@ -10,9 +10,12 @@ Defs(
Slice(start = 0, length = 0),
],
space_after: [
Slice(start = 0, length = 0),
Slice(start = 0, length = 2),
],
spaces: [
Newline,
Newline,
],
spaces: [],
type_defs: [],
value_defs: [
IngestedFileImport(
@ -27,37 +30,33 @@ Defs(
item: ImportAsKeyword,
after: [],
},
item: @23-33 TypedIdent {
ident: @23-27 "data",
spaces_before_colon: [],
ann: @30-33 Apply(
item: @23-27 "data",
},
annotation: Some(
IngestedFileAnnotation {
before_colon: [],
annotation: @30-33 Apply(
"",
"Str",
[],
),
},
},
),
},
),
],
},
@35-49 SpaceBefore(
Apply(
@35-44 Var {
module_name: "",
ident: "parseJson",
},
[
@45-49 Var {
module_name: "",
ident: "data",
},
],
Space,
),
@35-49 Apply(
@35-44 Var {
module_name: "",
ident: "parseJson",
},
[
Newline,
Newline,
@45-49 Var {
module_name: "",
ident: "data",
},
],
Space,
),
)

View File

@ -0,0 +1,53 @@
Defs(
Defs {
tags: [
Index(2147483648),
],
regions: [
@0-27,
],
space_before: [
Slice(start = 0, length = 0),
],
space_after: [
Slice(start = 0, length = 2),
],
spaces: [
Newline,
Newline,
],
type_defs: [],
value_defs: [
IngestedFileImport(
IngestedFileImport {
before_path: [],
path: @7-19 PlainLine(
"users.json",
),
name: KeywordItem {
keyword: Spaces {
before: [],
item: ImportAsKeyword,
after: [],
},
item: @23-27 "data",
},
annotation: None,
},
),
],
},
@29-43 Apply(
@29-38 Var {
module_name: "",
ident: "parseJson",
},
[
@39-43 Var {
module_name: "",
ident: "data",
},
],
Space,
),
)

View File

@ -0,0 +1,3 @@
import "users.json" as data
parseJson data

View File

@ -46,10 +46,10 @@ Full {
],
space_before: [
Slice(start = 0, length = 2),
Slice(start = 2, length = 2),
Slice(start = 4, length = 0),
],
space_after: [
Slice(start = 2, length = 0),
Slice(start = 2, length = 2),
Slice(start = 4, length = 0),
],
spaces: [

View File

@ -302,9 +302,9 @@ mod test_snapshots {
pass/destructure_tag_assignment.expr,
pass/docs.expr,
pass/empty_app_header.header,
pass/empty_module_header.header,
pass/empty_hosted_header.header,
pass/empty_list.expr,
pass/empty_module_header.header,
pass/empty_package_header.header,
pass/empty_platform_header.header,
pass/empty_record.expr,
@ -333,6 +333,7 @@ mod test_snapshots {
pass/ingested_file.moduledefs,
pass/inline_import.expr,
pass/inline_ingested_file.expr,
pass/inline_ingested_file_no_ann.expr,
pass/int_with_underscore.expr,
pass/lambda_in_chain.expr,
pass/lambda_indent.expr,
@ -390,6 +391,7 @@ mod test_snapshots {
pass/not_multiline_string.expr,
pass/number_literal_suffixes.expr,
pass/old_app_header.full,
pass/old_interface_header.header,
pass/one_backpassing.expr,
pass/one_char_string.expr,
pass/one_def.expr,
@ -410,7 +412,6 @@ mod test_snapshots {
pass/outdented_colon_in_record.expr,
pass/outdented_list.expr,
pass/outdented_record.expr,
pass/old_interface_header.header,
pass/packed_singleton_list.expr,
pass/parens_in_type_def_apply.expr,
pass/parens_in_value_def_annotation.expr,

View File

@ -112,7 +112,7 @@ pub(crate) fn global_analysis(doc_info: DocInfo) -> Vec<AnalyzedDocument> {
src_dir,
roc_target::Target::LinuxX64,
roc_load::FunctionKind::LambdaSet,
roc_reporting::report::RenderTarget::Generic,
roc_reporting::report::RenderTarget::LanguageServer,
RocCacheDir::Persistent(cache::roc_cache_dir().as_path()),
roc_reporting::report::DEFAULT_PALETTE,
);

View File

@ -202,9 +202,8 @@ pub(crate) mod diag {
);
let severity = report.severity.into_lsp_severity();
let mut msg = String::new();
report.render_ci(&mut msg, fmt.alloc);
report.render_language_server(&mut msg, fmt.alloc);
Some(Diagnostic {
range,
@ -239,7 +238,7 @@ pub(crate) mod diag {
let severity = report.severity.into_lsp_severity();
let mut msg = String::new();
report.render_ci(&mut msg, fmt.alloc);
report.render_language_server(&mut msg, fmt.alloc);
Some(Diagnostic {
range,

View File

@ -286,7 +286,6 @@ pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
ExprParseOptions {
accept_multi_backpassing: true,
check_for_arrow: true,
suffixed_found: false,
},
0,
arena,
@ -311,7 +310,6 @@ pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
ExprParseOptions {
accept_multi_backpassing: true,
check_for_arrow: true,
suffixed_found: false,
},
0,
arena,
@ -324,6 +322,7 @@ pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)),
region,
spaces_before,
spaces_after: _,
}),
_,
)) if spaces_before.len() <= 1 => {
@ -360,7 +359,6 @@ pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
ExprParseOptions {
accept_multi_backpassing: true,
check_for_arrow: true,
suffixed_found: false,
},
0,
arena,
@ -373,6 +371,7 @@ pub fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
Either::Second(ValueDef::Body(loc_pattern, loc_def_expr)),
region,
spaces_before,
spaces_after: _,
}),
_,
)) if spaces_before.len() <= 1 => {

View File

@ -670,6 +670,9 @@ fn to_expr_report<'a>(
EExpr::Dbg(e_expect, _position) => {
to_dbg_or_expect_report(alloc, lines, filename, context, Node::Dbg, e_expect, start)
}
EExpr::Import(e_import, position) => {
to_import_report(alloc, lines, filename, e_import, *position)
}
EExpr::TrailingOperator(pos) => {
let surroundings = Region::new(start, *pos);
let region = LineColumnRegion::from_pos(lines.convert_pos(*pos));
@ -1439,6 +1442,190 @@ fn to_dbg_or_expect_report<'a>(
}
}
fn to_import_report<'a>(
alloc: &'a RocDocAllocator<'a>,
lines: &LineInfo,
filename: PathBuf,
parse_problem: &roc_parse::parser::EImport<'a>,
start: Position,
) -> Report<'a> {
use roc_parse::parser::EImport::*;
match parse_problem {
Import(_pos) => unreachable!("another branch would be taken"),
IndentStart(pos)
| PackageShorthand(pos)
| PackageShorthandDot(pos)
| ModuleName(pos)
| IndentIngestedPath(pos)
| IngestedPath(pos) => to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.stack([
alloc.reflow("I was expecting to see a module name, like:"),
alloc.parser_suggestion("import BigNum").indent(4),
alloc.reflow("Or a package module name, like:"),
alloc.parser_suggestion("import pf.Stdout").indent(4),
alloc.reflow("Or a file path to ingest, like:"),
alloc
.parser_suggestion("import \"users.json\" as users : Str")
.indent(4),
]),
),
IndentAs(pos) | As(pos) | IndentExposing(pos) | Exposing(pos) | EndNewline(pos) => {
to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.stack([
alloc.concat([
alloc.reflow("I was expecting to see the "),
alloc.keyword("as"),
alloc.reflow(" keyword, like:"),
]),
alloc
.parser_suggestion("import svg.Path as SvgPath")
.indent(4),
alloc.concat([
alloc.reflow("Or the "),
alloc.keyword("exposing"),
alloc.reflow(" keyword, like:"),
]),
alloc
.parser_suggestion("import svg.Path exposing [arc, rx]")
.indent(4),
]),
)
}
IndentAlias(pos) | Alias(pos) => to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.concat([
alloc.reflow("I just saw the "),
alloc.keyword("as"),
alloc.reflow(" keyword, so I was expecting to see an alias next."),
]),
),
LowercaseAlias(region) => {
let surroundings = Region::new(start, region.end());
let region = lines.convert_region(*region);
let doc = alloc.stack([
alloc.reflow(r"This import is using a lowercase alias:"),
alloc.region_with_subregion(lines.convert_region(surroundings), region),
alloc.reflow(r"Module names and aliases must start with an uppercase letter."),
]);
Report {
filename,
doc,
title: "LOWERCASE ALIAS".to_string(),
severity: Severity::RuntimeError,
}
}
ExposingListStart(pos) => to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.concat([
alloc.reflow("I just saw the "),
alloc.keyword("exposing"),
alloc.reflow(" keyword, so I was expecting to see "),
alloc.keyword("["),
alloc.reflow(" next."),
]),
),
ExposedName(pos) | ExposingListEnd(pos) => {
let surroundings = Region::new(start, *pos);
let region = LineColumnRegion::from_pos(lines.convert_pos(*pos));
let doc = alloc.stack([
alloc
.reflow(r"I'm partway through parsing an exposing list, but I got stuck here:"),
alloc.region_with_subregion(lines.convert_region(surroundings), region),
alloc.reflow(r"I was expecting a type, value, or function name next, like:"),
alloc
.parser_suggestion("import Svg exposing [Path, arc, rx]")
.indent(4),
]);
Report {
filename,
doc,
title: "WEIRD EXPOSING".to_string(),
severity: Severity::RuntimeError,
}
}
IndentIngestedName(pos) | IngestedName(pos) => to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.stack([
alloc.reflow("I was expecting to see a name next, like:"),
alloc
.parser_suggestion("import \"users.json\" as users : Str")
.indent(4),
]),
),
Annotation(problem, pos) => to_type_report(alloc, lines, filename, problem, *pos),
IndentAnnotation(pos) | IndentColon(pos) | Colon(pos) => to_unfinished_import_report(
alloc,
lines,
filename,
*pos,
start,
alloc.stack([
alloc.reflow("I was expecting to see an annotation next, like:"),
alloc
.parser_suggestion("import \"users.json\" as users : Str")
.indent(4),
]),
),
Space(problem, pos) => to_space_report(alloc, lines, filename, problem, *pos),
}
}
fn to_unfinished_import_report<'a>(
alloc: &'a RocDocAllocator<'a>,
lines: &LineInfo,
filename: PathBuf,
pos: Position,
start: Position,
message: RocDocBuilder<'a>,
) -> Report<'a> {
let surroundings = Region::new(start, pos);
let region = LineColumnRegion::from_pos(lines.convert_pos(pos));
let doc = alloc.stack([
alloc.concat([
alloc.reflow(r"I was partway through parsing an "),
alloc.keyword("import"),
alloc.reflow(r", but I got stuck here:"),
]),
alloc.region_with_subregion(lines.convert_region(surroundings), region),
message,
]);
Report {
filename,
doc,
title: "UNFINISHED IMPORT".to_string(),
severity: Severity::RuntimeError,
}
}
fn to_if_report<'a>(
alloc: &'a RocDocAllocator<'a>,
lines: &LineInfo,

View File

@ -112,6 +112,7 @@ pub fn pretty_header_with_path(title: &str, path: &Path) -> String {
pub enum RenderTarget {
ColorTerminal,
Generic,
LanguageServer,
}
/// A textual report.
@ -133,6 +134,7 @@ impl<'b> Report<'b> {
match target {
RenderTarget::Generic => self.render_ci(buf, alloc),
RenderTarget::ColorTerminal => self.render_color_terminal(buf, alloc, palette),
RenderTarget::LanguageServer => self.render_language_server(buf, alloc),
}
}
@ -176,6 +178,18 @@ impl<'b> Report<'b> {
}
}
/// Render report for the language server, where the window is narrower.
/// Path is not included, and the header is not emphasized with "─".
pub fn render_language_server(self, buf: &mut String, alloc: &'b RocDocAllocator<'b>) {
let err_msg = "<buffer is not a utf-8 encoded string>";
alloc
.stack([alloc.text(self.title), self.doc])
.1
.render_raw(60, &mut CiWrite::new(buf))
.expect(err_msg)
}
pub fn horizontal_rule(palette: &'b Palette) -> String {
format!("{}{}", palette.header, "".repeat(80))
}

View File

@ -0,0 +1,11 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" }
import pf.Stdout
import "../../LICENSE" as license
main =
license
|> List.map Num.toU64
|> List.sum
|> Num.toStr
|> Stdout.line!

View File

@ -15,7 +15,7 @@ nix develop
./www/build.sh
# make the roc command available
export PATH="$(pwd)/target/release/:$PATH"
bash build-dev-local.sh
bash ./www/build-dev-local.sh
```
Open http://0.0.0.0:8080 in your browser.

View File

@ -20,11 +20,19 @@ Here are some Getting Started guides for different operating systems:
- [Windows](https://github.com/roc-lang/roc/blob/main/getting_started/windows.md)
- [Other Systems](https://github.com/roc-lang/roc/blob/main/getting_started/other.md)
### [Editor Extensions](#editor-extensions) {#editor-extensions}
### [Editor Extensions/Plugins](#editor-extensions) {#editor-extensions}
[VS Code extension](https://marketplace.visualstudio.com/items?itemName=IvanDemchenko.roc-lang-unofficial) ❗ make sure to follow ["Configuring language server"](https://github.com/ivan-demchenko/roc-vscode-unofficial?tab=readme-ov-file#configuring-language-server).
It would be a fantastic contribution for the language server to be set up automatically. If you'd like to help with this, just make a post in [the "new contributors" topic on Zulip](https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/new.20contributors) and say hello!
- [Visual Studio Code](https://visualstudio.microsoft.com/#vscode-section)
- Features: syntax highlighting, completion, type hints, jump to source
- syntax highlighting, completion, type hints, jump to source
- install the [Roc Plugin](https://marketplace.visualstudio.com/items?itemName=IvanDemchenko.roc-lang-unofficial)
- ❗make sure to follow ["Configuring language server"](https://github.com/ivan-demchenko/roc-vscode-unofficial?tab=readme-ov-file#configuring-language-server).
- It would be a fantastic contribution for the language server to be set up automatically. If you'd like to help with this, just make a post in [the "new contributors" topic on Zulip](https://roc.zulipchat.com/#narrow/stream/316715-contributing/topic/new.20contributors) and say hello!
- [Zed](https://zed.dev/download), since Version 0.133.5
- Features: syntax highlighting, completion, type hints, jump to source
- search and install roc extension (action `zed: extensions`)
- in case of errors look into the Zed log (action `zed: open log`)
- For other editors like Vim, Helix or Emacs [see](https://github.com/faldor20/tree-sitter-roc)
### [Tutorial](#tutorial) {#tutorial}

View File

@ -48,7 +48,7 @@ So far, so good!
Try typing this in the REPL and pressing Enter:
<samp class="repl-prompt">"Hello, World!"</samp>
<pre><samp class="repl-prompt">"Hello, World!"</samp></pre>
The REPL should cheerfully display the following:

View File

@ -40,7 +40,6 @@
--header-link-hover: #222;
--h1-color: #8055e4;
--tutorial-h3-color: #8c5ce3; /* Slightly darker than --primary-1, which looks washed-out in <h3>s */
--repl-prompt: #0064ff;
}
html {
@ -1577,6 +1576,6 @@ code .dim {
}
.repl-prompt:before {
color: var(--repl-prompt);
color: var(--light-cyan);
content: "» ";
}