mirror of
https://github.com/casey/just.git
synced 2025-12-23 11:37:29 +00:00
Merge 2c794364e9 into 05e9dafc07
This commit is contained in:
commit
7bb59f0c2b
17 changed files with 379 additions and 15 deletions
25
README.md
25
README.md
|
|
@ -1032,6 +1032,7 @@ foo:
|
|||
| `export` | boolean | `false` | Export all variables as environment variables. |
|
||||
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
|
||||
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
|
||||
| `named-parameters` | boolean | `false` | Using named-parameters feature for all recipes in `justfile`. |
|
||||
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
|
||||
| `quiet` | boolean | `false` | Disable echoing recipe lines before executing. |
|
||||
| `script-interpreter`<sup>1.33.0</sup> | `[COMMAND, ARGS…]` | `['sh', '-eu']` | Set command used to invoke recipes with empty `[script]` attribute. |
|
||||
|
|
@ -2151,6 +2152,7 @@ change their behavior.
|
|||
| `[linux]`<sup>1.8.0</sup> | recipe | Enable recipe on Linux. |
|
||||
| `[macos]`<sup>1.8.0</sup> | recipe | Enable recipe on MacOS. |
|
||||
| `[metadata(METADATA)]`<sup>1.42.0</sup> | recipe | Attach `METADATA` to recipe. |
|
||||
| `[named-parameters]`<sup>1.44.0<sup> | recipe | Turn on [named_parameters](#named_parameters) for this recipe. |
|
||||
| `[no-cd]`<sup>1.9.0</sup> | recipe | Don't change directory before executing recipe. |
|
||||
| `[no-exit-message]`<sup>1.7.0</sup> | recipe | Don't print an error message if recipe fails. |
|
||||
| `[no-quiet]`<sup>1.23.0</sup> | recipe | Override globally quiet recipes and always echo out the recipe. |
|
||||
|
|
@ -4013,6 +4015,29 @@ This preserves `just`'s ability to catch variable name typos before running,
|
|||
for example if you were to write `{{argument}}`, but will not do what you want
|
||||
if the value of `argument` contains single quotes.
|
||||
|
||||
#### Named Parameters
|
||||
|
||||
The `named-arguments` setting enables python like keyword arguments for recipe calls.
|
||||
Take for instance the following
|
||||
|
||||
``` just
|
||||
set named-parameters
|
||||
|
||||
foo default1="changes_often" default2="changes_sometimes" default3="changes_rarely":
|
||||
echo {{ default1 }} {{ default2 }} {{ default3 }}
|
||||
```
|
||||
|
||||
Calling `just foo default2="x"` would normally result in stdout of
|
||||
`default2=x changes_sometimes changes_rarely`. while with `named-arguments`
|
||||
one would get `changes_often x changes_rarely`. Notice it is still possible
|
||||
to have positional parameters such as the recipe:
|
||||
|
||||
``` just
|
||||
foo a b="default" c=b:
|
||||
echo {{ a }} {{ b }} {{ c }}
|
||||
```
|
||||
|
||||
|
||||
#### Positional Arguments
|
||||
|
||||
The `positional-arguments` setting causes all arguments to be passed as
|
||||
|
|
|
|||
20
shell.nix
Normal file
20
shell.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
let
|
||||
rust-overlay = builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz";
|
||||
pkgs = import <nixpkgs> {
|
||||
overlays = [(import rust-overlay)];
|
||||
};
|
||||
env = {
|
||||
};
|
||||
toolchain = pkgs.rust-bin.fromRustupToolchainFile ./toolchain.toml;
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = [
|
||||
toolchain
|
||||
# If the dependencies need system libs, you usually need pkg-config + the lib
|
||||
pkgs.pkg-config
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
RUST_BACKTRACE = "full";
|
||||
TMPDIR = "/tmp";
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ pub(crate) enum Attribute<'src> {
|
|||
Linux,
|
||||
Macos,
|
||||
Metadata(Vec<StringLiteral<'src>>),
|
||||
NamedParameters,
|
||||
NoCd,
|
||||
NoExitMessage,
|
||||
NoQuiet,
|
||||
|
|
@ -44,6 +45,7 @@ impl AttributeDiscriminant {
|
|||
| Self::ExitMessage
|
||||
| Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NamedParameters
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
|
|
@ -117,6 +119,7 @@ impl<'src> Attribute<'src> {
|
|||
AttributeDiscriminant::Linux => Self::Linux,
|
||||
AttributeDiscriminant::Macos => Self::Macos,
|
||||
AttributeDiscriminant::Metadata => Self::Metadata(arguments),
|
||||
AttributeDiscriminant::NamedParameters => Self::NamedParameters,
|
||||
AttributeDiscriminant::NoCd => Self::NoCd,
|
||||
AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,
|
||||
AttributeDiscriminant::NoQuiet => Self::NoQuiet,
|
||||
|
|
@ -186,6 +189,7 @@ impl Display for Attribute<'_> {
|
|||
| Self::ExitMessage
|
||||
| Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NamedParameters
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
|
|
|
|||
16
src/error.rs
16
src/error.rs
|
|
@ -93,6 +93,13 @@ pub(crate) enum Error<'src> {
|
|||
editor: OsString,
|
||||
status: ExitStatus,
|
||||
},
|
||||
EvalNamedParameterDuplicate {
|
||||
parameter: String,
|
||||
},
|
||||
EvalUnknownNamedParameter {
|
||||
parameter: String,
|
||||
suggestion: Option<Suggestion<'src>>,
|
||||
},
|
||||
EvalUnknownVariable {
|
||||
variable: String,
|
||||
suggestion: Option<Suggestion<'src>>,
|
||||
|
|
@ -424,6 +431,15 @@ impl ColorDisplay for Error<'_> {
|
|||
let editor = editor.to_string_lossy();
|
||||
write!(f, "Editor `{editor}` failed: {status}")?;
|
||||
}
|
||||
EvalNamedParameterDuplicate { parameter } => {
|
||||
write!(f, "`{parameter}` defined multiple times.")?;
|
||||
}
|
||||
EvalUnknownNamedParameter { parameter, suggestion} => {
|
||||
write!(f, "Recipe does not contain parameter `{parameter}`.")?;
|
||||
if let Some(suggestion) = suggestion {
|
||||
write!(f, "\n{suggestion}")?;
|
||||
}
|
||||
}
|
||||
EvalUnknownVariable { variable, suggestion} => {
|
||||
write!(f, "Justfile does not contain variable `{variable}`.")?;
|
||||
if let Some(suggestion) = suggestion {
|
||||
|
|
|
|||
|
|
@ -342,12 +342,20 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||
parameters: &[Parameter<'src>],
|
||||
recipe: &Recipe<'src>,
|
||||
scope: &'run Scope<'src, 'run>,
|
||||
use_named_parameters: bool,
|
||||
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
|
||||
let mut evaluator = Self::new(context, is_dependency, scope);
|
||||
|
||||
let mut positional = Vec::new();
|
||||
|
||||
let mut rest = arguments;
|
||||
let mut defaults = Table::new();
|
||||
for parameter in parameters {
|
||||
if parameter.default.is_some() {
|
||||
defaults.insert(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
for parameter in parameters {
|
||||
let value = if rest.is_empty() {
|
||||
if let Some(ref default) = parameter.default {
|
||||
|
|
@ -369,9 +377,24 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||
rest = &[];
|
||||
value
|
||||
} else {
|
||||
let value = rest[0].clone();
|
||||
let mut value = rest[0].clone();
|
||||
if use_named_parameters {
|
||||
// We stop handling of positional parameters once we hit a named parameter
|
||||
// if it is escaped we continue treating it as positional but unescape it
|
||||
if value.starts_with(|c| c == '\'' || c == '"') {
|
||||
let quote = value.chars().next().unwrap();
|
||||
if value.ends_with(quote) && value.len() >= 2 {
|
||||
value = value[1..value.len() - 1].to_string();
|
||||
} else if value.contains('=') {
|
||||
break;
|
||||
}
|
||||
} else if value.contains('=') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
positional.push(value.clone());
|
||||
rest = &rest[1..];
|
||||
defaults.remove(parameter.name.lexeme());
|
||||
value
|
||||
};
|
||||
|
||||
|
|
@ -383,10 +406,71 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||
file_depth: 0,
|
||||
name: parameter.name,
|
||||
private: false,
|
||||
value,
|
||||
value: value.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// invariantly rest is only non-empty if we have named parameters
|
||||
if !rest.is_empty() {
|
||||
assert!(use_named_parameters);
|
||||
for arg in rest {
|
||||
let mut split = arg.splitn(2, '=');
|
||||
// name and value should always be present. Earlier static analysis
|
||||
// ensures once we hit default values no more positional parameters exist
|
||||
let name = split.next().ok_or_else(||
|
||||
Error::Internal {
|
||||
message: "named parameter missing name".to_string(),
|
||||
})?;
|
||||
let value = split.next().ok_or_else(|| Error::Internal {
|
||||
message: "named parameter missing value".to_string(),
|
||||
})?;
|
||||
match defaults.remove(name) {
|
||||
None => {
|
||||
if parameters.iter().any(|p| p.name.lexeme() == name) {
|
||||
return Err(Error::EvalNamedParameterDuplicate {
|
||||
parameter: name.to_string(),
|
||||
});
|
||||
}
|
||||
let candidates = parameters
|
||||
.iter()
|
||||
.filter(|p| p.default.is_some())
|
||||
.map(|p| Suggestion {
|
||||
name: p.name.lexeme(),
|
||||
target: None,
|
||||
});
|
||||
let suggestion = Suggestion::find_suggestion(name, candidates);
|
||||
return Err(Error::EvalUnknownNamedParameter {
|
||||
parameter: name.to_string(),
|
||||
suggestion,
|
||||
});
|
||||
},
|
||||
Some(parameter) => {
|
||||
evaluator.scope.bind(Binding {
|
||||
constant: false,
|
||||
export: parameter.export,
|
||||
file_depth: 0,
|
||||
name: parameter.name,
|
||||
private: false,
|
||||
value: value.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for parameter in defaults.values() {
|
||||
let default = parameter.default.as_ref().expect("Default value expected for parameter");
|
||||
let value = evaluator.evaluate_expression(default)?;
|
||||
evaluator.scope.bind(Binding {
|
||||
constant: false,
|
||||
export: parameter.export,
|
||||
file_depth: 0,
|
||||
name: parameter.name,
|
||||
private: false,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok((evaluator.scope, positional))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,19 +33,8 @@ pub(crate) struct Justfile<'src> {
|
|||
}
|
||||
|
||||
impl<'src> Justfile<'src> {
|
||||
fn find_suggestion(
|
||||
input: &str,
|
||||
candidates: impl Iterator<Item = Suggestion<'src>>,
|
||||
) -> Option<Suggestion<'src>> {
|
||||
candidates
|
||||
.map(|suggestion| (edit_distance(input, suggestion.name), suggestion))
|
||||
.filter(|(distance, _suggestion)| *distance < 3)
|
||||
.min_by_key(|(distance, _suggestion)| *distance)
|
||||
.map(|(_distance, suggestion)| suggestion)
|
||||
}
|
||||
|
||||
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
|
||||
Self::find_suggestion(
|
||||
Suggestion::find_suggestion(
|
||||
input,
|
||||
self
|
||||
.recipes
|
||||
|
|
@ -69,7 +58,7 @@ impl<'src> Justfile<'src> {
|
|||
}
|
||||
|
||||
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
|
||||
Self::find_suggestion(
|
||||
Suggestion::find_suggestion(
|
||||
input,
|
||||
self
|
||||
.assignments
|
||||
|
|
@ -343,6 +332,11 @@ impl<'src> Justfile<'src> {
|
|||
search,
|
||||
};
|
||||
|
||||
let use_named_parameters = recipe
|
||||
.attributes
|
||||
.contains(AttributeDiscriminant::NamedParameters)
|
||||
|| module.settings.named_parameters;
|
||||
|
||||
let (outer, positional) = Evaluator::evaluate_parameters(
|
||||
arguments,
|
||||
&context,
|
||||
|
|
@ -350,6 +344,7 @@ impl<'src> Justfile<'src> {
|
|||
&recipe.parameters,
|
||||
recipe,
|
||||
scope,
|
||||
use_named_parameters,
|
||||
)?;
|
||||
|
||||
let scope = outer.child();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub(crate) enum Keyword {
|
|||
IgnoreComments,
|
||||
Import,
|
||||
Mod,
|
||||
NamedParameters,
|
||||
NoExitMessage,
|
||||
PositionalArguments,
|
||||
Quiet,
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ impl<'src> Node<'src> for Set<'src> {
|
|||
| Setting::Export(value)
|
||||
| Setting::Fallback(value)
|
||||
| Setting::NoExitMessage(value)
|
||||
| Setting::NamedParameters(value)
|
||||
| Setting::PositionalArguments(value)
|
||||
| Setting::Quiet(value)
|
||||
| Setting::Unstable(value)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ impl<'src> Parameter<'src> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'src> Keyed<'src> for Parameter<'src> {
|
||||
fn key(&self) -> &'src str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> Keyed<'src> for &Parameter<'src> {
|
||||
fn key(&self) -> &'src str {
|
||||
self.name.lexeme()
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorDisplay for Parameter<'_> {
|
||||
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||
if let Some(prefix) = self.kind.prefix() {
|
||||
|
|
|
|||
|
|
@ -1275,6 +1275,9 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
|
||||
Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),
|
||||
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
|
||||
Keyword::NamedParameters => {
|
||||
Some(Setting::NamedParameters(self.parse_set_bool()?))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub(crate) enum Setting<'src> {
|
|||
Export(bool),
|
||||
Fallback(bool),
|
||||
IgnoreComments(bool),
|
||||
NamedParameters(bool),
|
||||
NoExitMessage(bool),
|
||||
PositionalArguments(bool),
|
||||
Quiet(bool),
|
||||
|
|
@ -35,6 +36,7 @@ impl Display for Setting<'_> {
|
|||
| Self::Export(value)
|
||||
| Self::Fallback(value)
|
||||
| Self::IgnoreComments(value)
|
||||
| Self::NamedParameters(value)
|
||||
| Self::NoExitMessage(value)
|
||||
| Self::PositionalArguments(value)
|
||||
| Self::Quiet(value)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub(crate) struct Settings<'src> {
|
|||
pub(crate) export: bool,
|
||||
pub(crate) fallback: bool,
|
||||
pub(crate) ignore_comments: bool,
|
||||
pub(crate) named_parameters: bool,
|
||||
pub(crate) no_exit_message: bool,
|
||||
pub(crate) positional_arguments: bool,
|
||||
pub(crate) quiet: bool,
|
||||
|
|
@ -66,6 +67,9 @@ impl<'src> Settings<'src> {
|
|||
Setting::IgnoreComments(ignore_comments) => {
|
||||
settings.ignore_comments = ignore_comments;
|
||||
}
|
||||
Setting::NamedParameters(named_parameters) => {
|
||||
settings.named_parameters = named_parameters;
|
||||
}
|
||||
Setting::NoExitMessage(no_exit_message) => {
|
||||
settings.no_exit_message = no_exit_message;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@ pub(crate) struct Suggestion<'src> {
|
|||
pub(crate) target: Option<&'src str>,
|
||||
}
|
||||
|
||||
impl<'src> Suggestion<'src> {
|
||||
pub fn find_suggestion(
|
||||
input: &str,
|
||||
candidates: impl Iterator<Item = Suggestion<'src>>,
|
||||
) -> Option<Suggestion<'src>> {
|
||||
candidates
|
||||
.map(|suggestion| (edit_distance(input, suggestion.name), suggestion))
|
||||
.filter(|(distance, _suggestion)| *distance < 3)
|
||||
.min_by_key(|(distance, _suggestion)| *distance)
|
||||
.map(|(_distance, suggestion)| suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Suggestion<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "Did you mean `{}`", self.name)?;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ struct Settings<'a> {
|
|||
windows_powershell: bool,
|
||||
windows_shell: Option<&'a str>,
|
||||
working_directory: Option<&'a str>,
|
||||
named_parameters: bool,
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ mod man;
|
|||
mod misc;
|
||||
mod modules;
|
||||
mod multibyte_char;
|
||||
mod named_parameters;
|
||||
mod newline_escape;
|
||||
mod no_aliases;
|
||||
mod no_cd;
|
||||
|
|
|
|||
179
tests/named_parameters.rs
Normal file
179
tests/named_parameters.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_named_parameters_original_order() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("a=1")
|
||||
.arg("b=2")
|
||||
.arg("c=3")
|
||||
.justfile(
|
||||
r#"
|
||||
[named-parameters]
|
||||
foo a="a" b="b" c="c":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
echo {{ c }}
|
||||
"#,
|
||||
)
|
||||
.stdout(
|
||||
"
|
||||
1
|
||||
2
|
||||
3
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
echo 1
|
||||
echo 2
|
||||
echo 3
|
||||
"#,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_named_parameters_mixed_order() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("c=3")
|
||||
.arg("b=2")
|
||||
.arg("a=1")
|
||||
.justfile(
|
||||
r#"
|
||||
[named-parameters]
|
||||
foo a="a" b="b" c="c":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
echo {{ c }}
|
||||
"#,
|
||||
)
|
||||
.stdout(
|
||||
"
|
||||
1
|
||||
2
|
||||
3
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
echo 1
|
||||
echo 2
|
||||
echo 3
|
||||
"#,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_and_named_parameters() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("\"a=1\"")
|
||||
.arg("c=3")
|
||||
.arg("b=2")
|
||||
.justfile(
|
||||
r#"
|
||||
[named-parameters]
|
||||
foo a b="b" c="c":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
echo {{ c }}
|
||||
"#,
|
||||
)
|
||||
.stdout(
|
||||
"
|
||||
a=1
|
||||
2
|
||||
3
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
echo a=1
|
||||
echo 2
|
||||
echo 3
|
||||
"#,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_on_duplicate_assignment_of_named_param() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("b=2")
|
||||
.arg("b=2")
|
||||
.justfile(
|
||||
r#"
|
||||
[named-parameters]
|
||||
foo a="1" b="b":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
"#,
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
error: `b` defined multiple times.
|
||||
"#,
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_on_unknown_named_param() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("c=2")
|
||||
.justfile(
|
||||
r#"
|
||||
[named-parameters]
|
||||
foo a="1" b="b":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
"#,
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
error: Recipe does not contain parameter `c`.
|
||||
Did you mean `a`?
|
||||
"#,
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_parameters_setting() {
|
||||
Test::new()
|
||||
.arg("foo")
|
||||
.arg("a=1")
|
||||
.arg("c=3")
|
||||
.justfile(
|
||||
r#"
|
||||
set named-parameters
|
||||
|
||||
foo a="a" b="b" c="c":
|
||||
echo {{ a }}
|
||||
echo {{ b }}
|
||||
echo {{ c }}
|
||||
"#,
|
||||
)
|
||||
.stdout(
|
||||
"
|
||||
1
|
||||
b
|
||||
3
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
echo 1
|
||||
echo b
|
||||
echo 3
|
||||
"#,
|
||||
)
|
||||
.run();
|
||||
}
|
||||
3
toolchain.toml
Normal file
3
toolchain.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["clippy", "rust-analyzer"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue