plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
0aa74fb
— Peter John
2026-10-26T21:20:45+05:30
docs(editors): iteration cheat sheet, value enum docs; highlight/LSP for for/extend/Seq
- README.md +35 -0
- plum-lsp/src/completion.rs +25 -1
- plum-lsp/src/main.rs +139 -44
- plum-tooling/tree-sitter-plum/queries/plum/highlights.scm +10 -1
- plum-tooling/tree-sitter-plum/queries/plum/indents.scm +2 -0
- plum-tooling/tree-sitter-plum/queries/plum/tags.scm +3 -0
- plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm +2 -0
- plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json +21 -2
README.md
CHANGED
|
@@ -285,6 +285,24 @@ while i < units.length()
|
|
|
285
285
|
|
|
286
286
|
List and Map have no closure-taking iteration methods; closures remain in the language for passing behavior around as values (`Num` below, `Try.map`). See [`plum-examples/seq.plum`](plum-examples/seq.plum) (one test per adapter and terminal), [`plum-examples/chains.plum`](plum-examples/chains.plum) and [`plum-examples/forms.plum`](plum-examples/forms.plum).
|
|
287
287
|
|
|
288
|
+
#### Iteration cheat sheet
|
|
289
|
+
|
|
290
|
+
| Go / Rust / streams | Plum |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `for _, u := range units` / `for u in &units` | `for u := units` |
|
|
293
|
+
| `for i, u := range units` / `.iter().enumerate()` | `for i, u := units` |
|
|
294
|
+
| `for k, v := range m` / `for (k, v) in &m` | `for k, v := m` |
|
|
295
|
+
| `for i := 0; i < n; i++` / `for i in 0..n` | `for i := range(0, n)` |
|
|
296
|
+
| `.iter().filter(p).collect()` / `stream.filter(p)` | `for u := units.filter()` then the predicate as the body |
|
|
297
|
+
| `.iter().map(f).collect()` / `stream.map(f)` | `for u := units.map()` then the mapped value as the body |
|
|
298
|
+
| `.iter().find(p)` / `findFirst(p)` | `for u := units.find()` (`-> T?`) |
|
|
299
|
+
| `.iter().fold(0, f)` / `reduce(0, f)` | `for acc, u := units.fold(0)` |
|
|
300
|
+
| `.iter().take(n).skip(m)` / `limit(n).skip(m)` | `units.skip(m).take(n)` |
|
|
301
|
+
| `.iter().step_by(n)` / `.rev()` / `.zip(o)` / `chunks(n)` | `stepBy(n)` / `reversed()` / `zip(o)` / `chunks(n)` |
|
|
302
|
+
| `.count()` / `.any(p)` / `.all(p)` / `forEach(f)` | `count()` / `any()` / `all()` / `each()` |
|
|
303
|
+
| `if p(x) { continue }` | `, if cond` clause after the source |
|
|
304
|
+
| generator / `yield` | `fun* name(..) *T` with `yield` |
|
|
305
|
+
|
|
288
306
|
#### `extend`
|
|
289
307
|
|
|
290
308
|
`extend <Type> =` followed by indented methods adds methods (`fun` and `fun*`) to a compiler primitive without declaring a dummy `enum X = | X` just to hang them on. It is only allowed in `plum-std`, and only for built-in types (`Seq`, `Int`, `Float`, `Bool`, `Str`, `Byte`, `ByteSlice`, `Array`); anywhere else it is an error (`extend is only allowed for built-in types in std`).
|
|
@@ -343,6 +361,23 @@ enum Step(n: Int) = # a shared field on every variant ...
|
|
|
343
361
|
| ReadMax(20)
|
|
344
362
|
```
|
|
345
363
|
|
|
364
|
+
### `value enum`
|
|
365
|
+
|
|
366
|
+
`value enum` declares a small, copy-semantics value type (a vector, a color, a rectangle) that is flattened into wasm locals instead of being heap-allocated:
|
|
367
|
+
|
|
368
|
+
```plum
|
|
369
|
+
value enum Vec2 =
|
|
370
|
+
| Vec2(x: Float, y: Float)
|
|
371
|
+
|
|
372
|
+
fun add(self, o: Vec2) -> Vec2 =
|
|
373
|
+
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
374
|
+
|
|
375
|
+
value enum Rect =
|
|
376
|
+
| Rect(pos: Vec2, size: Vec2) # a field may be another value enum
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
It has exactly one constructor, named like the type; no generics; fields are only `Int`, `Float`, `Bool` or other value enums, and a value enum can't contain itself. Values are copied on assignment and when passed, and are boxed only inside generic containers (`List[Vec2]`, `Vec2?`). Methods, `match` and `?` work as for any enum. See [`plum-examples/values.plum`](plum-examples/values.plum).
|
|
380
|
+
|
|
346
381
|
## Generics
|
|
347
382
|
|
|
348
383
|
```plum
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -26,7 +26,7 @@ use tower_lsp::lsp_types::*;
|
|
|
26
26
|
|
|
27
27
|
const KEYWORDS: &[&str] = &[
|
|
28
28
|
"fun", "fun*", "yield", "crash", "??", "value", "enum", "extend", "import", "module", "extern", "if", "else", "while", "for", "match",
|
|
29
|
-
"
|
|
29
|
+
"when", "break", "continue", "return", "todo", "assert", "test", "self", "True",
|
|
30
30
|
"False",
|
|
31
31
|
];
|
|
32
32
|
|
|
@@ -250,6 +250,7 @@ pub fn memberItems(
|
|
|
250
250
|
receiver: &str,
|
|
251
251
|
global_env: &TypeEnv,
|
|
252
252
|
ctx: &CheckCtx,
|
|
253
|
+
stars: &[ast::Fn],
|
|
253
254
|
) -> Vec<CompletionItem> {
|
|
254
255
|
let Some(f) = cursor_fn else {
|
|
255
256
|
return Vec::new();
|
|
@@ -297,6 +298,29 @@ pub fn memberItems(
|
|
|
297
298
|
..Default::default()
|
|
298
299
|
});
|
|
299
300
|
}
|
|
301
|
+
// `fun*` methods are expanded by the loop lowering and are not in the
|
|
302
|
+
// checker's method tables; they come from the source's `stars`. An
|
|
303
|
+
// iterable (`length()`+`at(i)`, or an `entries()` method) also reaches the
|
|
304
|
+
// `extend Seq[T]` ones through the checker's method fallback.
|
|
305
|
+
let has = |m: &str| {
|
|
306
|
+
ctx.methods.contains_key(&(recv_name.clone(), m.to_string()))
|
|
307
|
+
|| stars.iter().any(|f| f.type_param.as_deref() == Some(recv_name.as_str()) && f.name == m)
|
|
308
|
+
};
|
|
309
|
+
let iterable = recv_name != "Seq" && ((has("length") && has("at")) || has("entries"));
|
|
310
|
+
for f in stars {
|
|
311
|
+
let owner = f.type_param.as_deref();
|
|
312
|
+
let own = owner == Some(recv_name.as_str());
|
|
313
|
+
let seq = iterable && owner == Some("Seq");
|
|
314
|
+
if (!own && !seq) || items.iter().any(|i| i.label == f.name) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
items.push(CompletionItem {
|
|
318
|
+
label: f.name.clone(),
|
|
319
|
+
kind: Some(CompletionItemKind::METHOD),
|
|
320
|
+
detail: Some(if own { "fun*".to_string() } else { "fun* (Seq)".to_string() }),
|
|
321
|
+
..Default::default()
|
|
322
|
+
});
|
|
323
|
+
}
|
|
300
324
|
for info in ctx.enum_variants.values() {
|
|
301
325
|
if info.enum_name != recv_name {
|
|
302
326
|
continue;
|
plum-lsp/src/main.rs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
#![allow(non_snake_case)]
|
|
24
24
|
|
|
25
25
|
use std::collections::HashMap;
|
|
26
|
-
use std::path::PathBuf;
|
|
26
|
+
use std::path::{Path, PathBuf};
|
|
27
27
|
use std::sync::OnceLock;
|
|
28
28
|
|
|
29
29
|
use tokio::sync::Mutex;
|
|
@@ -104,37 +104,7 @@ impl Backend {
|
|
|
104
104
|
/// flash a bogus diagnostic.
|
|
105
105
|
fn typeDiagnostics(&self, uri: &Url, text: &str) -> Option<Vec<Diagnostic>> {
|
|
106
106
|
let path = uri.to_file_path().ok()?;
|
|
107
|
-
let canon = std::fs::canonicalize(&path).unwrap_or(path.clone());
|
|
108
|
-
let mut overrides = HashMap::new();
|
|
109
|
-
overrides.insert(canon, text.to_string());
|
|
110
|
-
|
|
107
|
+
typeDiagnosticsFor(&path, &self.libPath, text)
|
|
111
|
-
let lastLine = text.lines().count().max(1) as u32 - 1;
|
|
112
|
-
match plum_checker::checkSource(&ast) {
|
|
113
|
-
Ok(()) => Some(Vec::new()),
|
|
114
|
-
Err(errors) => Some(
|
|
115
|
-
errors
|
|
116
|
-
.into_iter()
|
|
117
|
-
.map(|e| {
|
|
118
|
-
// `CheckError`'s position is 1-based and points at the
|
|
119
|
-
// enclosing declaration, not a sub-expression — clamp
|
|
120
|
-
// to the document and cover the whole line so an
|
|
121
|
-
// editor still shows something sensible even if the
|
|
122
|
-
// line itself is short/blank.
|
|
123
|
-
let line = e.line.saturating_sub(1).min(lastLine as usize) as u32;
|
|
124
|
-
Diagnostic {
|
|
125
|
-
range: Range {
|
|
126
|
-
start: Position::new(line, 0),
|
|
127
|
-
end: Position::new(line, u32::MAX),
|
|
128
|
-
},
|
|
129
|
-
severity: Some(DiagnosticSeverity::ERROR),
|
|
130
|
-
source: Some("plum-check".into()),
|
|
131
|
-
message: e.message,
|
|
132
|
-
..Default::default()
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
.collect(),
|
|
136
|
-
),
|
|
137
|
-
}
|
|
138
108
|
}
|
|
139
109
|
|
|
140
110
|
/// Builds `plum_checker`'s global tables for the live buffer, the same
|
|
@@ -160,20 +130,16 @@ impl Backend {
|
|
|
160
130
|
let own_source = plum_core::AstParser::new(&sanitized).parseSource(tree.root_node());
|
|
161
131
|
|
|
162
132
|
if let Some(receiver) = completion::receiverBeforeDot(line_text, pos.character as usize) {
|
|
163
|
-
let
|
|
133
|
+
let Ok(path) = uri.to_file_path() else {
|
|
164
134
|
return Vec::new();
|
|
165
135
|
};
|
|
166
|
-
let (global_env, methods, enum_variants, enum_params) =
|
|
167
|
-
plum_checker::buildGlobalTables(&merged);
|
|
168
|
-
let min_required = plum_checker::buildMinRequiredArgs(&merged);
|
|
169
|
-
let ctx = plum_checker::CheckCtx {
|
|
170
|
-
methods: &methods,
|
|
171
|
-
enum_variants: &enum_variants,
|
|
172
|
-
|
|
136
|
+
return memberCompletionFor(
|
|
137
|
+
&path,
|
|
138
|
+
&self.libPath,
|
|
139
|
+
&sanitized,
|
|
173
|
-
|
|
140
|
+
pos.line as usize + 1,
|
|
141
|
+
&receiver,
|
|
174
|
-
|
|
142
|
+
);
|
|
175
|
-
let cursor_fn = completion::enclosingFn(&own_source, pos.line as usize + 1);
|
|
176
|
-
return completion::memberItems(cursor_fn, &receiver, &global_env, &ctx);
|
|
177
143
|
}
|
|
178
144
|
|
|
179
145
|
let mut items = completion::keywordItems();
|
|
@@ -303,6 +269,72 @@ impl LanguageServer for Backend {
|
|
|
303
269
|
}
|
|
304
270
|
}
|
|
305
271
|
|
|
272
|
+
/// `Backend::typeDiagnostics` without the `Backend` (so it is unit-testable):
|
|
273
|
+
/// loads `path` with `text` overriding what's on disk, then runs the checker.
|
|
274
|
+
fn typeDiagnosticsFor(path: &Path, libPath: &Path, text: &str) -> Option<Vec<Diagnostic>> {
|
|
275
|
+
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
276
|
+
let mut overrides = HashMap::new();
|
|
277
|
+
overrides.insert(canon, text.to_string());
|
|
278
|
+
let ast = plum_core::loadAndMergeWithOverrides(path, libPath, &overrides).ok()?;
|
|
279
|
+
let lastLine = text.lines().count().max(1) as u32 - 1;
|
|
280
|
+
match plum_checker::checkSource(&ast) {
|
|
281
|
+
Ok(()) => Some(Vec::new()),
|
|
282
|
+
Err(errors) => Some(
|
|
283
|
+
errors
|
|
284
|
+
.into_iter()
|
|
285
|
+
.map(|e| {
|
|
286
|
+
// `CheckError`'s position is 1-based and points at the
|
|
287
|
+
// enclosing declaration, not a sub-expression — clamp
|
|
288
|
+
// to the document and cover the whole line so an
|
|
289
|
+
// editor still shows something sensible even if the
|
|
290
|
+
// line itself is short/blank.
|
|
291
|
+
let line = e.line.saturating_sub(1).min(lastLine as usize) as u32;
|
|
292
|
+
Diagnostic {
|
|
293
|
+
range: Range {
|
|
294
|
+
start: Position::new(line, 0),
|
|
295
|
+
end: Position::new(line, u32::MAX),
|
|
296
|
+
},
|
|
297
|
+
severity: Some(DiagnosticSeverity::ERROR),
|
|
298
|
+
source: Some("plum-check".into()),
|
|
299
|
+
message: e.message,
|
|
300
|
+
..Default::default()
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.collect(),
|
|
304
|
+
),
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Member completion for `receiver.` at `cursor_line` (1-based) of the
|
|
309
|
+
/// (sanitized) buffer `text`; `Backend::completionItems`' member branch.
|
|
310
|
+
fn memberCompletionFor(
|
|
311
|
+
path: &Path,
|
|
312
|
+
libPath: &Path,
|
|
313
|
+
text: &str,
|
|
314
|
+
cursor_line: usize,
|
|
315
|
+
receiver: &str,
|
|
316
|
+
) -> Vec<CompletionItem> {
|
|
317
|
+
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
318
|
+
let mut overrides = HashMap::new();
|
|
319
|
+
overrides.insert(canon, text.to_string());
|
|
320
|
+
let Ok(merged) = plum_core::loadAndMergeWithOverrides(path, libPath, &overrides) else {
|
|
321
|
+
return Vec::new();
|
|
322
|
+
};
|
|
323
|
+
let tree = Backend::parseTree(text);
|
|
324
|
+
let own_source = plum_core::AstParser::new(text).parseSource(tree.root_node());
|
|
325
|
+
let (global_env, methods, enum_variants, enum_params) =
|
|
326
|
+
plum_checker::buildGlobalTables(&merged);
|
|
327
|
+
let min_required = plum_checker::buildMinRequiredArgs(&merged);
|
|
328
|
+
let ctx = plum_checker::CheckCtx {
|
|
329
|
+
methods: &methods,
|
|
330
|
+
enum_variants: &enum_variants,
|
|
331
|
+
enum_params: &enum_params,
|
|
332
|
+
min_required: &min_required,
|
|
333
|
+
};
|
|
334
|
+
let cursor_fn = completion::enclosingFn(&own_source, cursor_line);
|
|
335
|
+
completion::memberItems(cursor_fn, receiver, &global_env, &ctx, &merged.stars)
|
|
336
|
+
}
|
|
337
|
+
|
|
306
338
|
#[tokio::main]
|
|
307
339
|
async fn main() {
|
|
308
340
|
// `--lib-path` mirrors `plum-cli`'s own required flag (see
|
|
@@ -329,3 +361,66 @@ async fn main() {
|
|
|
329
361
|
});
|
|
330
362
|
Server::new(stdin, stdout, socket).serve(service).await;
|
|
331
363
|
}
|
|
364
|
+
|
|
365
|
+
#[cfg(test)]
|
|
366
|
+
mod tests {
|
|
367
|
+
use super::*;
|
|
368
|
+
|
|
369
|
+
fn repoRoot() -> PathBuf {
|
|
370
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// The whole diagnostic path (`publishAll`: syntax diagnostics, then the
|
|
374
|
+
/// two-pass checker) must find nothing wrong in the Seq/fun*/for example.
|
|
375
|
+
#[test]
|
|
376
|
+
fn seqExampleHasNoDiagnostics() {
|
|
377
|
+
let path = repoRoot().join("plum-examples/seq.plum");
|
|
378
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
379
|
+
let tree = Backend::parseTree(&text);
|
|
380
|
+
assert!(Backend::syntaxDiagnostics(&tree).is_empty());
|
|
381
|
+
let diags = typeDiagnosticsFor(&path, &repoRoot(), &text).expect("seq.plum loads");
|
|
382
|
+
assert!(diags.is_empty(), "unexpected diagnostics: {:?}", diags.iter().map(|d| &d.message).collect::<Vec<_>>());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn valuesAndLoopsExamplesHaveNoDiagnostics() {
|
|
387
|
+
for f in ["values.plum", "loops.plum", "chains.plum", "forms.plum"] {
|
|
388
|
+
let path = repoRoot().join("plum-examples").join(f);
|
|
389
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
390
|
+
let tree = Backend::parseTree(&text);
|
|
391
|
+
assert!(Backend::syntaxDiagnostics(&tree).is_empty(), "{f}: syntax");
|
|
392
|
+
let diags = typeDiagnosticsFor(&path, &repoRoot(), &text).expect("loads");
|
|
393
|
+
assert!(diags.is_empty(), "{f}: {:?}", diags.iter().map(|d| &d.message).collect::<Vec<_>>());
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// `xs.` on a List offers List's own methods AND the Seq methods reached
|
|
398
|
+
/// through the iterable fallback.
|
|
399
|
+
#[test]
|
|
400
|
+
fn memberCompletionOnListIncludesSeqMethods() {
|
|
401
|
+
let dir = std::env::temp_dir().join(format!("plum-lsp-test-{}", std::process::id()));
|
|
402
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
403
|
+
let path = dir.join("t.plum");
|
|
404
|
+
let text = "module t\nimport std/Number\nimport std/List\nimport std/Seq\n\nfun main() =\n xs := List(1, 2, 3)\n xs.zzzzzz\n";
|
|
405
|
+
std::fs::write(&path, text).unwrap();
|
|
406
|
+
let items = memberCompletionFor(&path, &repoRoot(), text, 8, "xs");
|
|
407
|
+
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
|
408
|
+
for want in ["length", "at", "filter", "map", "take", "fold", "toList"] {
|
|
409
|
+
assert!(labels.contains(&want), "missing {want} in {labels:?}");
|
|
410
|
+
}
|
|
411
|
+
let n = labels.iter().filter(|l| **l == "length").count();
|
|
412
|
+
assert_eq!(n, 1, "no duplicates");
|
|
413
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[test]
|
|
417
|
+
fn keywordsCurrent() {
|
|
418
|
+
let labels: Vec<String> = completion::keywordItems().into_iter().map(|i| i.label).collect();
|
|
419
|
+
for want in ["extend", "value", "fun*", "yield"] {
|
|
420
|
+
assert!(labels.iter().any(|l| l == want), "missing {want}");
|
|
421
|
+
}
|
|
422
|
+
for gone in ["inline", "block", "indexed", "case"] {
|
|
423
|
+
assert!(!labels.iter().any(|l| l == gone), "stale {gone}");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
plum-tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
; Earlier patterns win on the same range (tree-sitter-highlight / Helix), so the
|
|
2
|
+
; specific rules come first.
|
|
3
|
+
; `*T` / `*(A, B)` after a `fun*` signature: what each `yield` hands to the `for`.
|
|
4
|
+
(yield_type "*" @punctuation.special)
|
|
5
|
+
|
|
6
|
+
; `for _, v := m`: `_` is aliased to a var_identifier, so match it by text.
|
|
7
|
+
((var_identifier) @variable.builtin
|
|
8
|
+
(#eq? @variable.builtin "_"))
|
|
9
|
+
|
|
1
10
|
; (mod_identifier) @constant
|
|
2
11
|
(type_identifier) @type
|
|
3
12
|
(fn_identifier) @function
|
|
@@ -112,4 +121,4 @@
|
|
|
112
121
|
|
|
113
122
|
(tag_type (type_identifier) @constructor)
|
|
114
123
|
|
|
115
|
-
"match" @keyword
|
|
124
|
+
"match" @keyword
|
plum-tooling/tree-sitter-plum/queries/plum/indents.scm
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
(enum)
|
|
3
|
+
(extend)
|
|
3
4
|
(fn)
|
|
4
5
|
(for)
|
|
5
6
|
(while)
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
(while)
|
|
16
17
|
(fn)
|
|
17
18
|
(enum)
|
|
19
|
+
(extend)
|
|
18
20
|
] @extend
|
|
19
21
|
|
|
20
22
|
[
|
plum-tooling/tree-sitter-plum/queries/plum/tags.scm
CHANGED
|
@@ -12,3 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
(fn_call
|
|
14
14
|
function: (type_identifier) @name) @reference.call
|
|
15
|
+
|
|
16
|
+
(extend
|
|
17
|
+
name: (type_identifier) @name) @definition.class
|
plum-tooling/tree-sitter-plum/queries/plum/textobjects.scm
CHANGED
|
@@ -9,3 +9,5 @@
|
|
|
9
9
|
|
|
10
10
|
(comment) @comment.inside
|
|
11
11
|
(comment) @comment.around
|
|
12
|
+
|
|
13
|
+
(extend) @class.around
|
plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
{
|
|
6
6
|
"include": "#comments"
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
"include": "#yield-type"
|
|
10
|
+
},
|
|
8
11
|
{
|
|
9
12
|
"include": "#keywords"
|
|
10
13
|
},
|
|
@@ -36,6 +39,22 @@
|
|
|
36
39
|
}
|
|
37
40
|
]
|
|
38
41
|
},
|
|
42
|
+
"yield-type": {
|
|
43
|
+
"comment": "the `*T` / `*(A, B)` yield type after a `fun*` signature (so it is not styled as multiplication)",
|
|
44
|
+
"match": "^(\\s*(?:fun\\*)\\s+[A-Za-z_][A-Za-z0-9_]*\\s*\\(.*\\)\\s*)(\\*)",
|
|
45
|
+
"captures": {
|
|
46
|
+
"1": {
|
|
47
|
+
"patterns": [
|
|
48
|
+
{
|
|
49
|
+
"include": "$self"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"2": {
|
|
54
|
+
"name": "storage.type.yield.plum"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
39
58
|
"keywords": {
|
|
40
59
|
"patterns": [
|
|
41
60
|
{
|
|
@@ -44,7 +63,7 @@
|
|
|
44
63
|
},
|
|
45
64
|
{
|
|
46
65
|
"name": "keyword.control.plum",
|
|
47
|
-
"match": "\\b(import|
|
|
66
|
+
"match": "\\b(import|return|continue|break|match|when|if|else|while|for|yield|assert|todo|crash)\\b"
|
|
48
67
|
},
|
|
49
68
|
{
|
|
50
69
|
"name": "keyword.operator.range.plum",
|
|
@@ -83,7 +102,7 @@
|
|
|
83
102
|
},
|
|
84
103
|
{
|
|
85
104
|
"name": "keyword.operator.assignment.plum",
|
|
86
|
-
"match": "="
|
|
105
|
+
"match": ":?="
|
|
87
106
|
}
|
|
88
107
|
]
|
|
89
108
|
},
|