= ({
13 | expandNode,
14 | expandedNodes,
15 | hoveredNode,
16 | item,
17 | setHoveredNode,
18 | }) => {
19 | const hasChildren = item.node.childCount > 0;
20 | const isExpanded = expandedNodes.has(item.node);
21 | const isHovered = item.node === hoveredNode;
22 |
23 | const style = {
24 | paddingLeft: `${item.level * 16 + 4}px`,
25 | backgroundColor: isHovered ? 'rgba(59, 130, 246, 0.1)' : 'transparent',
26 | borderRadius: '2px',
27 | };
28 |
29 | return (
30 | setHoveredNode(item.node)}
34 | onMouseLeave={() => setHoveredNode()}
35 | onClick={() => hasChildren && expandNode(item.node)}
36 | >
37 |
38 | {hasChildren ? (
39 | isExpanded ? (
40 |
41 | ) : (
42 |
43 | )
44 | ) : (
45 |
46 | )}
47 |
48 | {item.node.type}
49 |
50 | [{item.node.startPosition.row}: {item.node.startPosition.column}] [
51 | {item.node.endPosition.row}: {item.node.endPosition.column}]
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/str_ext.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | pub(crate) trait StrExt {
4 | /// Returns a `Point` describing the tree-sitter point that would
5 | /// be reached after inserting this UTF-8 text.
6 | fn point_delta(&self) -> Point;
7 | }
8 |
9 | impl StrExt for str {
10 | fn point_delta(&self) -> Point {
11 | let (mut rows, mut column) = (0usize, 0usize);
12 |
13 | let mut chars = self.chars().peekable();
14 |
15 | while let Some(ch) = chars.next() {
16 | match ch {
17 | '\r' => {
18 | if matches!(chars.peek().copied(), Some('\n')) {
19 | chars.next();
20 | }
21 |
22 | rows += 1;
23 | column = 0;
24 | }
25 | '\n' | '\u{000B}' | '\u{000C}' | '\u{0085}' | '\u{2028}'
26 | | '\u{2029}' => {
27 | rows += 1;
28 | column = 0;
29 | }
30 | _ => {
31 | column += ch.len_utf8();
32 | }
33 | }
34 | }
35 |
36 | Point::new(rows, column)
37 | }
38 | }
39 |
40 | #[cfg(test)]
41 | mod tests {
42 | use super::*;
43 |
44 | #[test]
45 | fn empty_string_produces_origin() {
46 | assert_eq!("".point_delta(), Point::new(0, 0));
47 | }
48 |
49 | #[test]
50 | fn ascii_text_advances_column_by_bytes() {
51 | assert_eq!("abc".point_delta(), Point::new(0, 3));
52 | }
53 |
54 | #[test]
55 | fn multibyte_chars_count_their_utf8_width() {
56 | assert_eq!("😊é".point_delta(), Point::new(0, "😊é".len()));
57 | }
58 |
59 | #[test]
60 | fn newline_moves_to_next_row_and_resets_column() {
61 | assert_eq!("hi\n😊".point_delta(), Point::new(1, "😊".len()));
62 | }
63 |
64 | #[test]
65 | fn crlf_sequences_count_as_single_newline() {
66 | assert_eq!("\r\nabc".point_delta(), Point::new(1, "abc".len()));
67 | }
68 |
69 | #[test]
70 | fn bare_carriage_return_counts_as_line_break() {
71 | assert_eq!("foo\rbar".point_delta(), Point::new(1, "bar".len()));
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "just-lsp",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "build": "tsc && vite build",
8 | "dev": "vite",
9 | "format": "prettier --write .",
10 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-dialog": "^1.1.6",
15 | "@radix-ui/react-label": "^2.1.2",
16 | "@radix-ui/react-scroll-area": "^1.2.3",
17 | "@radix-ui/react-select": "^2.1.6",
18 | "@radix-ui/react-separator": "^1.1.2",
19 | "@radix-ui/react-slot": "^1.1.2",
20 | "@radix-ui/react-switch": "^1.1.3",
21 | "@radix-ui/react-tabs": "^1.1.3",
22 | "@radix-ui/react-tooltip": "^1.1.8",
23 | "@replit/codemirror-vim": "^6.2.1",
24 | "@tailwindcss/vite": "^4.0.10",
25 | "class-variance-authority": "^0.7.1",
26 | "clsx": "^2.1.1",
27 | "codemirror": "^6.0.1",
28 | "lodash": "^4.17.21",
29 | "lucide-react": "^0.477.0",
30 | "react": "^18.2.0",
31 | "react-dom": "^18.2.0",
32 | "react-resizable-panels": "^2.1.7",
33 | "tailwind-merge": "^3.0.2",
34 | "tailwindcss": "^4.0.10",
35 | "tailwindcss-animate": "^1.0.7",
36 | "web-tree-sitter": "^0.25.3"
37 | },
38 | "devDependencies": {
39 | "@trivago/prettier-plugin-sort-imports": "^5.2.2",
40 | "@types/bun": "^1.2.4",
41 | "@types/lodash": "^4.17.16",
42 | "@types/node": "^22.13.9",
43 | "@types/react": "^18.0.37",
44 | "@types/react-dom": "^18.0.11",
45 | "@typescript-eslint/eslint-plugin": "^5.59.0",
46 | "@typescript-eslint/parser": "^5.59.0",
47 | "@vitejs/plugin-react": "^4.0.0",
48 | "eslint": "^8.38.0",
49 | "eslint-plugin-react-hooks": "^4.6.0",
50 | "eslint-plugin-react-refresh": "^0.3.4",
51 | "prettier-plugin-tailwindcss": "^0.6.11",
52 | "typescript": "^5.0.2",
53 | "vite": "^4.3.9"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/rule/attribute_arguments.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | define_rule! {
4 | /// Reports attribute invocations whose argument counts don't match their
5 | /// builtin definitions.
6 | AttributeArgumentsRule {
7 | id: "attribute-arguments",
8 | message: "invalid attribute arguments",
9 | run(context) {
10 | let mut diagnostics = Vec::new();
11 |
12 | for attribute in context.attributes() {
13 | let attribute_name = &attribute.name.value;
14 |
15 | let matching = context.builtin_attributes(attribute_name);
16 |
17 | if matching.is_empty() {
18 | continue;
19 | }
20 |
21 | let argument_count = attribute.arguments.len();
22 | let has_arguments = argument_count > 0;
23 |
24 | let parameter_mismatch = matching.iter().copied().all(|attr| {
25 | if let Builtin::Attribute { parameters, .. } = attr {
26 | (parameters.is_some() && !has_arguments)
27 | || (parameters.is_none() && has_arguments)
28 | || (parameters.map_or(0, |_| 1) < argument_count)
29 | } else {
30 | false
31 | }
32 | });
33 |
34 | if parameter_mismatch {
35 | let required_argument_count = matching
36 | .iter()
37 | .copied()
38 | .find_map(|attr| {
39 | if let Builtin::Attribute { parameters, .. } = attr {
40 | parameters.map(|_| 1)
41 | } else {
42 | None
43 | }
44 | })
45 | .unwrap_or(0);
46 |
47 | diagnostics.push(Diagnostic::error(
48 | format!(
49 | "Attribute `{attribute_name}` got {argument_count} {} but takes {required_argument_count} {}",
50 | Count("argument", argument_count),
51 | Count("argument", required_argument_count),
52 | ),
53 | attribute.range,
54 | ));
55 | }
56 | }
57 |
58 | diagnostics
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/www/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as TabsPrimitive from '@radix-ui/react-tabs';
3 | import * as React from 'react';
4 |
5 | function Tabs({
6 | className,
7 | ...props
8 | }: React.ComponentProps) {
9 | return (
10 |
15 | );
16 | }
17 |
18 | function TabsList({
19 | className,
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
31 | );
32 | }
33 |
34 | function TabsTrigger({
35 | className,
36 | ...props
37 | }: React.ComponentProps) {
38 | return (
39 |
47 | );
48 | }
49 |
50 | function TabsContent({
51 | className,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
60 | );
61 | }
62 |
63 | export { Tabs, TabsList, TabsTrigger, TabsContent };
64 |
--------------------------------------------------------------------------------
/www/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3 | import * as React from 'react';
4 |
5 | function TooltipProvider({
6 | delayDuration = 0,
7 | ...props
8 | }: React.ComponentProps) {
9 | return (
10 |
15 | );
16 | }
17 |
18 | function Tooltip({
19 | ...props
20 | }: React.ComponentProps) {
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | function TooltipTrigger({
29 | ...props
30 | }: React.ComponentProps) {
31 | return ;
32 | }
33 |
34 | function TooltipContent({
35 | className,
36 | sideOffset = 0,
37 | children,
38 | ...props
39 | }: React.ComponentProps) {
40 | return (
41 |
42 |
51 | {children}
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
59 |
--------------------------------------------------------------------------------
/src/rule/dependency_arguments.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | define_rule! {
4 | /// Checks that dependency invocations supply the correct number of arguments
5 | /// for the referenced recipe's signature.
6 | DependencyArgumentRule {
7 | id: "dependency-arguments",
8 | message: "invalid dependency arguments",
9 | run(context) {
10 | let mut diagnostics = Vec::new();
11 |
12 | let recipe_parameters = context.recipe_parameters();
13 |
14 | for recipe in context.recipes() {
15 | for dependency in &recipe.dependencies {
16 | if let Some(params) = recipe_parameters.get(&dependency.name) {
17 | let required_params = params
18 | .iter()
19 | .filter(|p| {
20 | p.default_value.is_none()
21 | && !matches!(p.kind, ParameterKind::Variadic(_))
22 | })
23 | .count();
24 |
25 | let has_variadic = params
26 | .iter()
27 | .any(|p| matches!(p.kind, ParameterKind::Variadic(_)));
28 |
29 | let total_params = params.len();
30 | let arg_count = dependency.arguments.len();
31 |
32 | if arg_count < required_params {
33 | diagnostics.push(Diagnostic::error(
34 | format!(
35 | "Dependency `{}` requires {required_params} {}, but {arg_count} provided",
36 | dependency.name,
37 | Count("argument", required_params)
38 | ),
39 | dependency.range,
40 | ));
41 | } else if !has_variadic && arg_count > total_params {
42 | diagnostics.push(Diagnostic::error(
43 | format!(
44 | "Dependency `{}` accepts {total_params} {}, but {arg_count} provided",
45 | dependency.name,
46 | Count("argument", total_params)
47 | ),
48 | dependency.range,
49 | ));
50 | }
51 | }
52 | }
53 | }
54 |
55 | diagnostics
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/point_ext.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | pub(crate) trait PointExt {
4 | fn advance(self, delta: Point) -> Self;
5 | fn position(&self, document: &Document) -> lsp::Position;
6 | }
7 |
8 | impl PointExt for Point {
9 | /// Returns a new point shifted by `delta`, resetting the column when the
10 | /// delta moves to a different row.
11 | fn advance(self, delta: Point) -> Self {
12 | if delta.row == 0 {
13 | Point::new(self.row, self.column + delta.column)
14 | } else {
15 | Point::new(self.row + delta.row, delta.column)
16 | }
17 | }
18 |
19 | /// Tree-sitter points use a zero-based `row` plus UTF-8 byte offset
20 | /// `column`, while the LSP expects UTF-16 code-unit offsets.
21 | ///
22 | /// We take the document line for the point’s row, convert the byte column
23 | /// into a char index, and then into a UTF-16 offset to produce an `lsp::Position`.
24 | fn position(&self, document: &Document) -> lsp::Position {
25 | let line = document.content.line(self.row);
26 |
27 | let utf16_cu = line.char_to_utf16_cu(line.byte_to_char(self.column));
28 |
29 | lsp::Position {
30 | line: u32::try_from(self.row).expect("line index exceeds u32::MAX"),
31 | character: u32::try_from(utf16_cu)
32 | .expect("column index exceeds u32::MAX"),
33 | }
34 | }
35 | }
36 |
37 | #[cfg(test)]
38 | mod tests {
39 | use {super::*, pretty_assertions::assert_eq};
40 |
41 | #[test]
42 | fn advance_adds_columns_when_staying_on_same_row() {
43 | assert_eq!(Point::new(2, 3).advance(Point::new(0, 5)), Point::new(2, 8));
44 | }
45 |
46 | #[test]
47 | fn advance_moves_rows_and_resets_column_when_row_delta_positive() {
48 | assert_eq!(Point::new(1, 4).advance(Point::new(2, 3)), Point::new(3, 3));
49 | }
50 |
51 | #[test]
52 | fn converts_utf8_columns_to_utf16_offsets() {
53 | let document = Document::from("a𐐀b");
54 |
55 | assert_eq!(
56 | Point::new(0, document.content.line(0).char_to_byte(2))
57 | .position(&document),
58 | lsp::Position {
59 | line: 0,
60 | character: 3
61 | }
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/bindings/rust/lib.rs:
--------------------------------------------------------------------------------
1 | //! This crate provides just language support for the [tree-sitter][] parsing library.
2 | //!
3 | //! Typically, you will use the [language][language func] function to add this language to a
4 | //! tree-sitter [Parser][], and then use the parser to parse some code:
5 | //!
6 | //! ```
7 | //! let code = "";
8 | //! let mut parser = tree_sitter::Parser::new();
9 | //! parser.set_language(&tree_sitter_just::language()).expect("Error loading just grammar");
10 | //! let tree = parser.parse(code, None).unwrap();
11 | //! ```
12 | //!
13 | //! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
14 | //! [language func]: fn.language.html
15 | //! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
16 | //! [tree-sitter]: https://tree-sitter.github.io/
17 |
18 | use tree_sitter::Language;
19 |
20 | extern "C" {
21 | fn tree_sitter_just() -> Language;
22 | }
23 |
24 | /// Get the tree-sitter [Language][] for this grammar.
25 | ///
26 | /// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
27 | pub fn language() -> Language {
28 | unsafe { tree_sitter_just() }
29 | }
30 |
31 | /// The content of the [`node-types.json`][] file for this grammar.
32 | ///
33 | /// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
34 | pub const NODE_TYPES: &str = include_str!("../../src/node-types.json");
35 |
36 | pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries-flavored/helix/highlights.scm");
37 | pub const INJECTIONS_QUERY: &str = include_str!("../../queries-flavored/helix/injections.scm");
38 | pub const LOCALS_QUERY: &str = include_str!("../../queries-flavored/helix/locals.scm");
39 |
40 | // FIXME: add tags when available
41 | // pub const TAGS_QUERY: &'static str = include_str!("../../queries-src/tags.scm");
42 |
43 | #[cfg(test)]
44 | mod tests {
45 | #[test]
46 | fn test_can_load_grammar() {
47 | let mut parser = tree_sitter::Parser::new();
48 | parser
49 | .set_language(&super::language())
50 | .expect("Error loading just language");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/test/highlight/recipes.just:
--------------------------------------------------------------------------------
1 | #!/use/bin/env just
2 | # <- keyword.directive
3 | # ^^^^^^^^^^^^^^^^^ keyword.directive
4 |
5 | foo:
6 | # <- function
7 | # ^ operator
8 |
9 | @bar:
10 | # <- operator
11 | # ^ function
12 | # ^ operator
13 |
14 | baz: foo bar
15 | # <- function
16 | # ^ operator
17 | # ^ function.call
18 | # ^ function.call
19 |
20 | qux var1:
21 | # <- function
22 | # ^^^ variable.parameter
23 | # ^ operator
24 |
25 | quux var *var2:
26 | # <- function
27 | # ^^^ variable.parameter
28 | # ^ operator
29 | # ^^^ variable.parameter
30 | # ^ operator
31 |
32 | corge +quux: baz (quux quux)
33 | # <- function
34 | # ^ operator
35 | # ^^^^ variable.parameter
36 | # ^ operator
37 | # ^^^ function.call
38 | # ^ punctuation.bracket
39 | # ^^^^ function.call
40 | # ^^^ variable
41 | # ^ punctuation.bracket
42 |
43 | grault abc="def":
44 | # <- function
45 | # ^^^ variable.parameter
46 | # ^ operator
47 | # ^^^^ string
48 | # ^ operator
49 |
50 | garply: foo && bar
51 | # <- function
52 | # ^ operator
53 | # ^^^ function.call
54 | # ^^ operator
55 | # ^^^ function.call
56 |
57 | waldo a="b": foo bar && baz
58 | # <- function
59 | # ^ variable.parameter
60 | # ^ operator
61 | # ^^^ string
62 | # ^ operator
63 | # ^^^ function.call
64 | # ^^^ function.call
65 | # ^^ operator
66 | # ^^^ function.call
67 |
68 | fred: garply && (waldo "x")
69 | # <- function
70 | # ^^^^^^ function.call
71 | # ^^ operator
72 | # ^ punctuation.bracket
73 | # ^^^^ function.call
74 | # ^^^ string
75 | # ^ punctuation.bracket
76 |
77 | # plugh
78 | plugh:
79 | echo "plugh"
80 | # xyzzy
81 | xyzzy:
82 | echo "xyzzy"
83 |
84 | # FIXME: can't test these because we can't place comments between
85 | [private]
86 | [confirm, no-cd]
87 | attributes:
88 |
--------------------------------------------------------------------------------
/www/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { GripVerticalIcon } from 'lucide-react';
3 | import * as React from 'react';
4 | import * as ResizablePrimitive from 'react-resizable-panels';
5 |
6 | function ResizablePanelGroup({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
19 | );
20 | }
21 |
22 | function ResizablePanel({
23 | ...props
24 | }: React.ComponentProps) {
25 | return ;
26 | }
27 |
28 | function ResizableHandle({
29 | withHandle,
30 | className,
31 | ...props
32 | }: React.ComponentProps & {
33 | withHandle?: boolean;
34 | }) {
35 | return (
36 | div]:rotate-90',
40 | className
41 | )}
42 | {...props}
43 | >
44 | {withHandle && (
45 |
46 |
47 |
48 | )}
49 |
50 | );
51 | }
52 |
53 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
54 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/test/corpus/multiline.txt:
--------------------------------------------------------------------------------
1 | ================================================================================
2 | trailing whitespace
3 | ================================================================================
4 |
5 | a: #
6 | --------------------------------------------------------------------------------
7 |
8 | (source_file
9 | (recipe
10 | (recipe_header
11 | (identifier))
12 | (comment)))
13 |
14 | ================================================================================
15 | smooshed recipes
16 | ================================================================================
17 |
18 | foo:
19 | echo foo
20 | bar:
21 | echo bar
22 |
23 | --------------------------------------------------------------------------------
24 |
25 | (source_file
26 | (recipe
27 | (recipe_header
28 | (identifier))
29 | (recipe_body
30 | (recipe_line
31 | (text))))
32 | (recipe
33 | (recipe_header
34 | (identifier))
35 | (recipe_body
36 | (recipe_line
37 | (text)))))
38 |
39 | ================================================================================
40 | statement_wrap
41 | ================================================================================
42 |
43 | a := "foo" + \
44 | "bar"
45 |
46 | --------------------------------------------------------------------------------
47 |
48 | (source_file
49 | (assignment
50 | (identifier)
51 | (expression
52 | (value
53 | (string))
54 | (value
55 | (string)))))
56 |
57 | ================================================================================
58 | dependency_wrap
59 | ================================================================================
60 |
61 | baz: foo \
62 | bar
63 | echo baz {{ a }}
64 |
65 | --------------------------------------------------------------------------------
66 |
67 | (source_file
68 | (recipe
69 | (recipe_header
70 | (identifier)
71 | (dependencies
72 | (dependency
73 | (identifier))
74 | (dependency
75 | (identifier))))
76 | (recipe_body
77 | (recipe_line
78 | (text)
79 | (interpolation
80 | (expression
81 | (value
82 | (identifier))))))))
83 |
--------------------------------------------------------------------------------
/www/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { type VariantProps, cva } from 'class-variance-authority';
4 | import * as React from 'react';
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
15 | outline:
16 | 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-9 px-4 py-2 has-[>svg]:px-3',
24 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
25 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
26 | icon: 'size-9',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | }
34 | );
35 |
36 | function Button({
37 | className,
38 | variant,
39 | size,
40 | asChild = false,
41 | ...props
42 | }: React.ComponentProps<'button'> &
43 | VariantProps & {
44 | asChild?: boolean;
45 | }) {
46 | const Comp = asChild ? Slot : 'button';
47 |
48 | return (
49 |
54 | );
55 | }
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/src/rule/recipe_parameters.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | define_rule! {
4 | /// Validates recipe parameter lists for duplicate names, ordering mistakes, and
5 | /// illegal variadic/default combinations.
6 | RecipeParameterRule {
7 | id: "recipe-parameters",
8 | message: "invalid recipe parameters",
9 | run(context) {
10 | let mut diagnostics = Vec::new();
11 |
12 | for recipe in context.recipes() {
13 | let mut seen = HashSet::new();
14 |
15 | let (mut passed_default, mut passed_variadic) = (false, false);
16 |
17 | for (index, param) in recipe.parameters.iter().enumerate() {
18 | if !seen.insert(param.name.clone()) {
19 | diagnostics.push(Diagnostic::error(
20 | format!("Duplicate parameter `{}`", param.name),
21 | param.range,
22 | ));
23 | }
24 |
25 | let has_default = param.default_value.is_some();
26 |
27 | if matches!(param.kind, ParameterKind::Variadic(_)) {
28 | if index < recipe.parameters.len() - 1 {
29 | diagnostics.push(Diagnostic::error(
30 | format!(
31 | "Variadic parameter `{}` must be the last parameter",
32 | param.name
33 | ),
34 | param.range,
35 | ));
36 | }
37 |
38 | passed_variadic = true;
39 | }
40 |
41 | if passed_default
42 | && !has_default
43 | && !matches!(param.kind, ParameterKind::Variadic(_))
44 | {
45 | diagnostics.push(Diagnostic::error(
46 | format!(
47 | "Required parameter `{}` follows a parameter with a default value",
48 | param.name
49 | ),
50 | param.range,
51 | ));
52 | }
53 |
54 | if passed_variadic && index < recipe.parameters.len() - 1 {
55 | diagnostics.push(Diagnostic::error(
56 | format!("Parameter `{}` follows a variadic parameter", param.name),
57 | param.range,
58 | ));
59 | }
60 |
61 | if has_default {
62 | passed_default = true;
63 | }
64 | }
65 | }
66 |
67 | diagnostics
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "just-lsp"
3 | version = "0.2.8"
4 | description = "A language server for just"
5 | authors = ["Liam "]
6 | license = "CC0-1.0"
7 | homepage = "https://github.com/terror/just-lsp"
8 | repository = "https://github.com/terror/just-lsp"
9 | edition = "2024"
10 | exclude = ["/screenshot.png", "/www"]
11 | categories = ["development-tools"]
12 | keywords = ["productivity", "compilers", "language-servers", "just", "tree-sitter"]
13 | resolver = "2"
14 |
15 | include = [
16 | "/LICENSE",
17 | "/README.md",
18 | "/build.rs",
19 | "/src/",
20 | "/queries/**",
21 | "/vendor/*-src/**.c",
22 | "/vendor/*-src/**/**.h"
23 | ]
24 |
25 | [lints]
26 | workspace = true
27 |
28 | [profile.release]
29 | codegen-units = 1
30 | lto = true
31 |
32 | [workspace]
33 | members = [".", "crates/*"]
34 |
35 | [workspace.lints.rust]
36 | mismatched_lifetime_syntaxes = "allow"
37 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
38 |
39 | [workspace.lints.clippy]
40 | all = { level = "deny", priority = -1 }
41 | arbitrary-source-item-ordering = "deny"
42 | enum_glob_use = "allow"
43 | ignore_without_reason = "allow"
44 | needless_pass_by_value = "allow"
45 | pedantic = { level = "deny", priority = -1 }
46 | similar_names = "allow"
47 | struct_excessive_bools = "allow"
48 | struct_field_names = "allow"
49 | too_many_arguments = "allow"
50 | too_many_lines = "allow"
51 | type_complexity = "allow"
52 | undocumented_unsafe_blocks = "deny"
53 | unnecessary_wraps = "allow"
54 | wildcard_imports = "allow"
55 |
56 | [dependencies]
57 | anyhow = "1.0.100"
58 | ariadne = "0.5.1"
59 | clap = { version = "4.5.51", features = ["derive"] }
60 | env_logger = "0.11.8"
61 | log = "0.4.28"
62 | once_cell = "1.21.3"
63 | ropey = "1.6.1"
64 | serde = { version = "1.0.228", features = ["derive"] }
65 | serde_json = "1.0.145"
66 | target = "2.1.0"
67 | tempfile = "3.23.0"
68 | tokio = { version = "1.48.0", features = ["io-std", "io-util", "macros", "process", "rt-multi-thread"] }
69 | tokio-stream = { version = "0.1.17", features = ["io-util"] }
70 | tower-lsp = "0.20.0"
71 | tree-sitter = "0.25.10"
72 | tree-sitter-highlight = "0.25.10"
73 |
74 | [dev-dependencies]
75 | indoc = "2.0.7"
76 | pretty_assertions = "1.4.1"
77 | serde_json = "1.0.145"
78 | tower-test = "0.4.0"
79 |
80 | [build-dependencies]
81 | cc = "1.2.45"
82 |
--------------------------------------------------------------------------------
/src/rule/alias_recipe_conflict.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | enum Item<'a> {
4 | Alias(&'a Alias),
5 | Recipe(&'a Recipe),
6 | }
7 |
8 | impl Item<'_> {
9 | fn conflict_message(&self, name: &str) -> String {
10 | match self {
11 | Item::Alias(_) => format!("Alias `{name}` is redefined as a recipe"),
12 | Item::Recipe(_) => format!("Recipe `{name}` is redefined as an alias"),
13 | }
14 | }
15 |
16 | fn is_same_kind(&self, other: &Self) -> bool {
17 | matches!(
18 | (self, other),
19 | (Item::Alias(_), Item::Alias(_)) | (Item::Recipe(_), Item::Recipe(_))
20 | )
21 | }
22 |
23 | fn name(&self) -> &str {
24 | match self {
25 | Item::Alias(alias) => &alias.name.value,
26 | Item::Recipe(recipe) => &recipe.name.value,
27 | }
28 | }
29 |
30 | fn range(&self) -> lsp::Range {
31 | match self {
32 | Item::Alias(alias) => alias.name.range,
33 | Item::Recipe(recipe) => recipe.name.range,
34 | }
35 | }
36 | }
37 |
38 | define_rule! {
39 | /// Reports aliases and recipes that share the same name, since they shadow
40 | /// each other at runtime.
41 | AliasRecipeConflictRule {
42 | id: "alias-recipe-conflict",
43 | message: "name conflict",
44 | run(context) {
45 | let (aliases, recipes) = (context.aliases(), context.recipes());
46 |
47 | if aliases.is_empty() || recipes.is_empty() {
48 | return Vec::new();
49 | }
50 |
51 | let mut items = aliases
52 | .iter()
53 | .map(Item::Alias)
54 | .chain(recipes.iter().map(Item::Recipe))
55 | .collect::>();
56 |
57 | items.sort_by_key(|item| {
58 | let range = item.range();
59 | (range.start.line, range.start.character)
60 | });
61 |
62 | items
63 | .iter()
64 | .fold(
65 | (HashMap::<&str, &Item>::new(), Vec::new()),
66 | |(mut seen, mut diagnostics), item| {
67 | let name = item.name();
68 |
69 | match seen.get(name) {
70 | Some(first) if !first.is_same_kind(item) => {
71 | diagnostics.push(Diagnostic::error(first.conflict_message(name), item.range()));
72 | }
73 | None => {
74 | seen.insert(name, item);
75 | }
76 | _ => {}
77 | }
78 | (seen, diagnostics)
79 | },
80 | )
81 | .1
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | set dotenv-load
2 |
3 | export CARGO_MSG_LIMIT := '1'
4 |
5 | default:
6 | just --list
7 |
8 | alias f := fmt
9 | alias r := run
10 | alias t := test
11 |
12 | all: build test clippy fmt-check
13 |
14 | [group: 'dev']
15 | build:
16 | cargo build
17 |
18 | [group: 'dev']
19 | build-wasm:
20 | just -f vendor/tree-sitter-just/justfile build-wasm
21 | cp vendor/tree-sitter-just/tree-sitter-just.wasm www/public/tree-sitter-just.wasm
22 |
23 | [group: 'check']
24 | check:
25 | cargo check
26 |
27 | [group: 'check']
28 | ci: test clippy forbid
29 | cargo fmt --all -- --check
30 | cargo update --locked --package just-lsp
31 |
32 | [group: 'check']
33 | clippy:
34 | cargo clippy --all --all-targets
35 |
36 | [group: 'format']
37 | fmt:
38 | cargo fmt
39 |
40 | [group: 'format']
41 | fmt-web:
42 | cd www && bun run format
43 |
44 | [group: 'format']
45 | fmt-check:
46 | cargo fmt --all -- --check
47 |
48 | [group: 'check']
49 | forbid:
50 | ./bin/forbid
51 |
52 | [group: 'dev']
53 | install:
54 | cargo install -f just-lsp
55 |
56 | [group: 'dev']
57 | install-dev-deps:
58 | rustup install nightly
59 | rustup update nightly
60 | cargo install cargo-watch
61 |
62 | [group: 'release']
63 | publish:
64 | #!/usr/bin/env bash
65 | set -euxo pipefail
66 | rm -rf tmp/release
67 | gh repo clone https://github.com/terror/just-lsp tmp/release
68 | cd tmp/release
69 | VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1`
70 | git tag -a $VERSION -m "Release $VERSION"
71 | git push origin $VERSION
72 | cargo publish
73 | cd ../..
74 | rm -rf tmp/release
75 |
76 | [group: 'dev']
77 | run *args:
78 | cargo run -- --{{args}}
79 |
80 | [group: 'test']
81 | test:
82 | cargo test --all --all-targets
83 |
84 | [group: 'test']
85 | test-release-workflow:
86 | -git tag -d test-release
87 | -git push origin :test-release
88 | git tag test-release
89 | git push origin test-release
90 |
91 | [group: 'release']
92 | update-changelog:
93 | echo >> CHANGELOG.md
94 | git log --pretty='format:- %s' >> CHANGELOG.md
95 |
96 | [group: 'dev']
97 | update-parser:
98 | cd vendor/tree-sitter-just && npx tree-sitter generate
99 | cd vendor/tree-sitter-just && npx tree-sitter test
100 | cargo test
101 |
102 | [group: 'dev']
103 | watch +COMMAND='test':
104 | cargo watch --clear --exec "{{COMMAND}}"
105 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - master
10 |
11 | defaults:
12 | run:
13 | shell: bash
14 |
15 | env:
16 | RUSTFLAGS: --deny warnings
17 |
18 | jobs:
19 | coverage:
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 |
25 | - uses: actions-rust-lang/setup-rust-toolchain@v1
26 | with:
27 | components: llvm-tools-preview
28 |
29 | - uses: Swatinem/rust-cache@v2
30 |
31 | - uses: taiki-e/install-action@v2
32 | with:
33 | tool: cargo-llvm-cov
34 |
35 | - name: Generate coverage report
36 | run: cargo llvm-cov --workspace --all-features --all-targets --lcov --output-path lcov.info
37 |
38 | - name: Upload coverage reports to Codecov
39 | uses: codecov/codecov-action@v5
40 | with:
41 | token: ${{ secrets.CODECOV_TOKEN }}
42 | files: lcov.info
43 | flags: unit
44 | fail_ci_if_error: true
45 |
46 | lint:
47 | runs-on: ubuntu-latest
48 |
49 | steps:
50 | - uses: actions/checkout@v4
51 |
52 | - uses: Swatinem/rust-cache@v2
53 |
54 | - name: Clippy
55 | run: cargo clippy --all --all-targets
56 |
57 | - name: Format
58 | run: cargo fmt --all -- --check
59 |
60 | - name: Install Dependencies
61 | run: |
62 | sudo apt-get update
63 | sudo apt-get install ripgrep shellcheck
64 |
65 | - name: Check for Forbidden Words
66 | run: ./bin/forbid
67 |
68 | - name: Check /bin scripts
69 | run: shellcheck bin/*
70 |
71 | msrv:
72 | runs-on: ubuntu-latest
73 |
74 | steps:
75 | - uses: actions/checkout@v4
76 |
77 | - uses: actions-rust-lang/setup-rust-toolchain@v1
78 |
79 | - uses: Swatinem/rust-cache@v2
80 |
81 | - name: Check
82 | run: cargo check
83 |
84 | test:
85 | strategy:
86 | matrix:
87 | os:
88 | - ubuntu-latest
89 | - macos-latest
90 | - windows-latest
91 |
92 | runs-on: ${{matrix.os}}
93 |
94 | steps:
95 | - uses: actions/checkout@v4
96 |
97 | - name: Remove Broken WSL bash executable
98 | if: ${{ matrix.os == 'windows-latest' }}
99 | shell: cmd
100 | run: |
101 | takeown /F C:\Windows\System32\bash.exe
102 | icacls C:\Windows\System32\bash.exe /grant administrators:F
103 | del C:\Windows\System32\bash.exe
104 |
105 | - uses: Swatinem/rust-cache@v2
106 |
107 | - name: Test
108 | run: cargo test --all
109 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "cc"
16 | version = "1.2.1"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
19 | dependencies = [
20 | "shlex",
21 | ]
22 |
23 | [[package]]
24 | name = "memchr"
25 | version = "2.7.4"
26 | source = "registry+https://github.com/rust-lang/crates.io-index"
27 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
28 |
29 | [[package]]
30 | name = "regex"
31 | version = "1.10.6"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
34 | dependencies = [
35 | "aho-corasick",
36 | "memchr",
37 | "regex-automata",
38 | "regex-syntax",
39 | ]
40 |
41 | [[package]]
42 | name = "regex-automata"
43 | version = "0.4.7"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
46 | dependencies = [
47 | "aho-corasick",
48 | "memchr",
49 | "regex-syntax",
50 | ]
51 |
52 | [[package]]
53 | name = "regex-syntax"
54 | version = "0.8.4"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
57 |
58 | [[package]]
59 | name = "shlex"
60 | version = "1.3.0"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
63 |
64 | [[package]]
65 | name = "streaming-iterator"
66 | version = "0.1.9"
67 | source = "registry+https://github.com/rust-lang/crates.io-index"
68 | checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
69 |
70 | [[package]]
71 | name = "tree-sitter"
72 | version = "0.24.4"
73 | source = "registry+https://github.com/rust-lang/crates.io-index"
74 | checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
75 | dependencies = [
76 | "cc",
77 | "regex",
78 | "regex-syntax",
79 | "streaming-iterator",
80 | "tree-sitter-language",
81 | ]
82 |
83 | [[package]]
84 | name = "tree-sitter-just"
85 | version = "0.1.0"
86 | dependencies = [
87 | "cc",
88 | "tree-sitter",
89 | ]
90 |
91 | [[package]]
92 | name = "tree-sitter-language"
93 | version = "0.1.2"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600"
96 |
--------------------------------------------------------------------------------
/queries/highlights.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; This file specifies how matched syntax patterns should be highlighted
4 |
5 | [
6 | "export"
7 | "import"
8 | ] @keyword.import
9 |
10 | "mod" @module
11 |
12 | [
13 | "alias"
14 | "set"
15 | "shell"
16 | ] @keyword
17 |
18 | [
19 | "if"
20 | "else"
21 | ] @keyword.conditional
22 |
23 | ; Variables
24 |
25 | (value
26 | (identifier) @variable)
27 |
28 | (alias
29 | left: (identifier) @variable)
30 |
31 | (assignment
32 | left: (identifier) @variable)
33 |
34 | ; Functions
35 |
36 | (recipe_header
37 | name: (identifier) @function)
38 |
39 | (dependency
40 | name: (identifier) @function.call)
41 |
42 | (dependency_expression
43 | name: (identifier) @function.call)
44 |
45 | (function_call
46 | name: (identifier) @function.call)
47 |
48 | ; Parameters
49 |
50 | (parameter
51 | name: (identifier) @variable.parameter)
52 |
53 | ; Namespaces
54 |
55 | (module
56 | name: (identifier) @module)
57 |
58 | ; Operators
59 |
60 | [
61 | ":="
62 | "?"
63 | "=="
64 | "!="
65 | "=~"
66 | "@"
67 | "="
68 | "$"
69 | "*"
70 | "+"
71 | "&&"
72 | "@-"
73 | "-@"
74 | "-"
75 | "/"
76 | ":"
77 | ] @operator
78 |
79 | ; Punctuation
80 |
81 | "," @punctuation.delimiter
82 |
83 | [
84 | "{"
85 | "}"
86 | "["
87 | "]"
88 | "("
89 | ")"
90 | "{{"
91 | "}}"
92 | ] @punctuation.bracket
93 |
94 | [ "`" "```" ] @punctuation.special
95 |
96 | ; Literals
97 |
98 | (boolean) @boolean
99 |
100 | [
101 | (string)
102 | (external_command)
103 | ] @string
104 |
105 | (escape_sequence) @string.escape
106 |
107 | ; Comments
108 |
109 | (comment) @spell @comment
110 |
111 | (shebang) @keyword.directive
112 |
113 | ; highlight known settings (filtering does not always work)
114 | (setting
115 | left: (identifier) @keyword
116 | (#any-of? @keyword
117 | "allow-duplicate-recipes"
118 | "allow-duplicate-variables"
119 | "dotenv-filename"
120 | "dotenv-load"
121 | "dotenv-path"
122 | "dotenv-required"
123 | "export"
124 | "fallback"
125 | "ignore-comments"
126 | "positional-arguments"
127 | "shell"
128 | "shell-interpreter"
129 | "tempdir"
130 | "windows-powershell"
131 | "windows-shell"
132 | "working-directory"))
133 |
134 | ; highlight known attributes (filtering does not always work)
135 | (attribute
136 | (identifier) @attribute
137 | (#any-of? @attribute
138 | "confirm"
139 | "doc"
140 | "extension"
141 | "group"
142 | "linux"
143 | "macos"
144 | "no-cd"
145 | "no-exit-message"
146 | "no-quiet"
147 | "positional-arguments"
148 | "private"
149 | "script"
150 | "unix"
151 | "windows"))
152 |
153 | ; Numbers are part of the syntax tree, even if disallowed
154 | (numeric_error) @error
155 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-src/highlights.scm:
--------------------------------------------------------------------------------
1 | ; This file specifies how matched syntax patterns should be highlighted
2 |
3 | [
4 | "export"
5 | "import"
6 | ] @keyword.control.import
7 |
8 | "mod" @keyword.module
9 |
10 | [
11 | "alias"
12 | "set"
13 | "shell"
14 | ] @keyword
15 |
16 | [
17 | "if"
18 | "else"
19 | ] @keyword.control.conditional
20 |
21 | ; Variables
22 |
23 | (value
24 | (identifier) @variable)
25 |
26 | (alias
27 | left: (identifier) @variable)
28 |
29 | (assignment
30 | left: (identifier) @variable)
31 |
32 | ; Functions
33 |
34 | (recipe_header
35 | name: (identifier) @function)
36 |
37 | (dependency
38 | name: (identifier) @function.call)
39 |
40 | (dependency_expression
41 | name: (identifier) @function.call)
42 |
43 | (function_call
44 | name: (identifier) @function.call)
45 |
46 | ; Parameters
47 |
48 | (parameter
49 | name: (identifier) @variable.parameter)
50 |
51 | ; Namespaces
52 |
53 | (module
54 | name: (identifier) @namespace)
55 |
56 | ; Operators
57 |
58 | [
59 | ":="
60 | "?"
61 | "=="
62 | "!="
63 | "=~"
64 | "@"
65 | "="
66 | "$"
67 | "*"
68 | "+"
69 | "&&"
70 | "@-"
71 | "-@"
72 | "-"
73 | "/"
74 | ":"
75 | ] @operator
76 |
77 | ; Punctuation
78 |
79 | "," @punctuation.delimiter
80 |
81 | [
82 | "{"
83 | "}"
84 | "["
85 | "]"
86 | "("
87 | ")"
88 | "{{"
89 | "}}"
90 | ] @punctuation.bracket
91 |
92 | [ "`" "```" ] @punctuation.special
93 |
94 | ; Literals
95 |
96 | (boolean) @constant.builtin.boolean
97 |
98 | [
99 | (string)
100 | (external_command)
101 | ] @string
102 |
103 | (escape_sequence) @constant.character.escape
104 |
105 | ; Comments
106 |
107 | (comment) @spell @comment.line
108 |
109 | (shebang) @keyword.directive
110 |
111 | ; highlight known settings (filtering does not always work)
112 | (setting
113 | left: (identifier) @keyword
114 | (#any-of? @keyword
115 | "allow-duplicate-recipes"
116 | "allow-duplicate-variables"
117 | "dotenv-filename"
118 | "dotenv-load"
119 | "dotenv-path"
120 | "dotenv-required"
121 | "export"
122 | "fallback"
123 | "ignore-comments"
124 | "positional-arguments"
125 | "shell"
126 | "shell-interpreter"
127 | "tempdir"
128 | "windows-powershell"
129 | "windows-shell"
130 | "working-directory"))
131 |
132 | ; highlight known attributes (filtering does not always work)
133 | (attribute
134 | (identifier) @attribute
135 | (#any-of? @attribute
136 | "confirm"
137 | "doc"
138 | "extension"
139 | "group"
140 | "linux"
141 | "macos"
142 | "no-cd"
143 | "no-exit-message"
144 | "no-quiet"
145 | "positional-arguments"
146 | "private"
147 | "script"
148 | "unix"
149 | "windows"))
150 |
151 | ; Numbers are part of the syntax tree, even if disallowed
152 | (numeric_error) @error
153 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries/just/highlights.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; This file specifies how matched syntax patterns should be highlighted
4 |
5 | [
6 | "export"
7 | "import"
8 | ] @keyword.import
9 |
10 | "mod" @module
11 |
12 | [
13 | "alias"
14 | "set"
15 | "shell"
16 | ] @keyword
17 |
18 | [
19 | "if"
20 | "else"
21 | ] @keyword.conditional
22 |
23 | ; Variables
24 |
25 | (value
26 | (identifier) @variable)
27 |
28 | (alias
29 | left: (identifier) @variable)
30 |
31 | (assignment
32 | left: (identifier) @variable)
33 |
34 | ; Functions
35 |
36 | (recipe_header
37 | name: (identifier) @function)
38 |
39 | (dependency
40 | name: (identifier) @function.call)
41 |
42 | (dependency_expression
43 | name: (identifier) @function.call)
44 |
45 | (function_call
46 | name: (identifier) @function.call)
47 |
48 | ; Parameters
49 |
50 | (parameter
51 | name: (identifier) @variable.parameter)
52 |
53 | ; Namespaces
54 |
55 | (module
56 | name: (identifier) @module)
57 |
58 | ; Operators
59 |
60 | [
61 | ":="
62 | "?"
63 | "=="
64 | "!="
65 | "=~"
66 | "@"
67 | "="
68 | "$"
69 | "*"
70 | "+"
71 | "&&"
72 | "@-"
73 | "-@"
74 | "-"
75 | "/"
76 | ":"
77 | ] @operator
78 |
79 | ; Punctuation
80 |
81 | "," @punctuation.delimiter
82 |
83 | [
84 | "{"
85 | "}"
86 | "["
87 | "]"
88 | "("
89 | ")"
90 | "{{"
91 | "}}"
92 | ] @punctuation.bracket
93 |
94 | [ "`" "```" ] @punctuation.special
95 |
96 | ; Literals
97 |
98 | (boolean) @boolean
99 |
100 | [
101 | (string)
102 | (external_command)
103 | ] @string
104 |
105 | (escape_sequence) @string.escape
106 |
107 | ; Comments
108 |
109 | (comment) @spell @comment
110 |
111 | (shebang) @keyword.directive
112 |
113 | ; highlight known settings (filtering does not always work)
114 | (setting
115 | left: (identifier) @keyword
116 | (#any-of? @keyword
117 | "allow-duplicate-recipes"
118 | "allow-duplicate-variables"
119 | "dotenv-filename"
120 | "dotenv-load"
121 | "dotenv-path"
122 | "dotenv-required"
123 | "export"
124 | "fallback"
125 | "ignore-comments"
126 | "positional-arguments"
127 | "shell"
128 | "shell-interpreter"
129 | "tempdir"
130 | "windows-powershell"
131 | "windows-shell"
132 | "working-directory"))
133 |
134 | ; highlight known attributes (filtering does not always work)
135 | (attribute
136 | (identifier) @attribute
137 | (#any-of? @attribute
138 | "confirm"
139 | "doc"
140 | "extension"
141 | "group"
142 | "linux"
143 | "macos"
144 | "no-cd"
145 | "no-exit-message"
146 | "no-quiet"
147 | "positional-arguments"
148 | "private"
149 | "script"
150 | "unix"
151 | "windows"))
152 |
153 | ; Numbers are part of the syntax tree, even if disallowed
154 | (numeric_error) @error
155 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/zed/highlights.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; This file specifies how matched syntax patterns should be highlighted
4 |
5 | [
6 | "export"
7 | "import"
8 | ] @keyword.control.import
9 |
10 | "mod" @keyword.directive
11 |
12 | [
13 | "alias"
14 | "set"
15 | "shell"
16 | ] @keyword
17 |
18 | [
19 | "if"
20 | "else"
21 | ] @keyword.control.conditional
22 |
23 | ; Variables
24 |
25 | (value
26 | (identifier) @variable)
27 |
28 | (alias
29 | left: (identifier) @variable)
30 |
31 | (assignment
32 | left: (identifier) @variable)
33 |
34 | ; Functions
35 |
36 | (recipe_header
37 | name: (identifier) @function)
38 |
39 | (dependency
40 | name: (identifier) @function)
41 |
42 | (dependency_expression
43 | name: (identifier) @function)
44 |
45 | (function_call
46 | name: (identifier) @function)
47 |
48 | ; Parameters
49 |
50 | (parameter
51 | name: (identifier) @variable.parameter)
52 |
53 | ; Namespaces
54 |
55 | (module
56 | name: (identifier) @namespace)
57 |
58 | ; Operators
59 |
60 | [
61 | ":="
62 | "?"
63 | "=="
64 | "!="
65 | "=~"
66 | "@"
67 | "="
68 | "$"
69 | "*"
70 | "+"
71 | "&&"
72 | "@-"
73 | "-@"
74 | "-"
75 | "/"
76 | ":"
77 | ] @operator
78 |
79 | ; Punctuation
80 |
81 | "," @punctuation.delimiter
82 |
83 | [
84 | "{"
85 | "}"
86 | "["
87 | "]"
88 | "("
89 | ")"
90 | "{{"
91 | "}}"
92 | ] @punctuation.bracket
93 |
94 | [ "`" "```" ] @punctuation.special
95 |
96 | ; Literals
97 |
98 | (boolean) @constant.builtin.boolean
99 |
100 | [
101 | (string)
102 | (external_command)
103 | ] @string
104 |
105 | (escape_sequence) @constant.character.escape
106 |
107 | ; Comments
108 |
109 | (comment) @comment.line
110 |
111 | (shebang) @keyword.directive
112 |
113 | ; highlight known settings (filtering does not always work)
114 | (setting
115 | left: (identifier) @keyword
116 | (#any-of? @keyword
117 | "allow-duplicate-recipes"
118 | "allow-duplicate-variables"
119 | "dotenv-filename"
120 | "dotenv-load"
121 | "dotenv-path"
122 | "dotenv-required"
123 | "export"
124 | "fallback"
125 | "ignore-comments"
126 | "positional-arguments"
127 | "shell"
128 | "shell-interpreter"
129 | "tempdir"
130 | "windows-powershell"
131 | "windows-shell"
132 | "working-directory"))
133 |
134 | ; highlight known attributes (filtering does not always work)
135 | (attribute
136 | (identifier) @attribute
137 | (#any-of? @attribute
138 | "confirm"
139 | "doc"
140 | "extension"
141 | "group"
142 | "linux"
143 | "macos"
144 | "no-cd"
145 | "no-exit-message"
146 | "no-quiet"
147 | "positional-arguments"
148 | "private"
149 | "script"
150 | "unix"
151 | "windows"))
152 |
153 | ; Numbers are part of the syntax tree, even if disallowed
154 | (numeric_error) @error
155 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/helix/highlights.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; This file specifies how matched syntax patterns should be highlighted
4 |
5 | [
6 | "export"
7 | "import"
8 | ] @keyword.control.import
9 |
10 | "mod" @keyword.directive
11 |
12 | [
13 | "alias"
14 | "set"
15 | "shell"
16 | ] @keyword
17 |
18 | [
19 | "if"
20 | "else"
21 | ] @keyword.control.conditional
22 |
23 | ; Variables
24 |
25 | (value
26 | (identifier) @variable)
27 |
28 | (alias
29 | left: (identifier) @variable)
30 |
31 | (assignment
32 | left: (identifier) @variable)
33 |
34 | ; Functions
35 |
36 | (recipe_header
37 | name: (identifier) @function)
38 |
39 | (dependency
40 | name: (identifier) @function)
41 |
42 | (dependency_expression
43 | name: (identifier) @function)
44 |
45 | (function_call
46 | name: (identifier) @function)
47 |
48 | ; Parameters
49 |
50 | (parameter
51 | name: (identifier) @variable.parameter)
52 |
53 | ; Namespaces
54 |
55 | (module
56 | name: (identifier) @namespace)
57 |
58 | ; Operators
59 |
60 | [
61 | ":="
62 | "?"
63 | "=="
64 | "!="
65 | "=~"
66 | "@"
67 | "="
68 | "$"
69 | "*"
70 | "+"
71 | "&&"
72 | "@-"
73 | "-@"
74 | "-"
75 | "/"
76 | ":"
77 | ] @operator
78 |
79 | ; Punctuation
80 |
81 | "," @punctuation.delimiter
82 |
83 | [
84 | "{"
85 | "}"
86 | "["
87 | "]"
88 | "("
89 | ")"
90 | "{{"
91 | "}}"
92 | ] @punctuation.bracket
93 |
94 | [ "`" "```" ] @punctuation.special
95 |
96 | ; Literals
97 |
98 | (boolean) @constant.builtin.boolean
99 |
100 | [
101 | (string)
102 | (external_command)
103 | ] @string
104 |
105 | (escape_sequence) @constant.character.escape
106 |
107 | ; Comments
108 |
109 | (comment) @comment.line
110 |
111 | (shebang) @keyword.directive
112 |
113 | ; highlight known settings (filtering does not always work)
114 | (setting
115 | left: (identifier) @keyword
116 | (#any-of? @keyword
117 | "allow-duplicate-recipes"
118 | "allow-duplicate-variables"
119 | "dotenv-filename"
120 | "dotenv-load"
121 | "dotenv-path"
122 | "dotenv-required"
123 | "export"
124 | "fallback"
125 | "ignore-comments"
126 | "positional-arguments"
127 | "shell"
128 | "shell-interpreter"
129 | "tempdir"
130 | "windows-powershell"
131 | "windows-shell"
132 | "working-directory"))
133 |
134 | ; highlight known attributes (filtering does not always work)
135 | (attribute
136 | (identifier) @attribute
137 | (#any-of? @attribute
138 | "confirm"
139 | "doc"
140 | "extension"
141 | "group"
142 | "linux"
143 | "macos"
144 | "no-cd"
145 | "no-exit-message"
146 | "no-quiet"
147 | "positional-arguments"
148 | "private"
149 | "script"
150 | "unix"
151 | "windows"))
152 |
153 | ; Numbers are part of the syntax tree, even if disallowed
154 | (numeric_error) @error
155 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/lapce/highlights.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; This file specifies how matched syntax patterns should be highlighted
4 |
5 | [
6 | "export"
7 | "import"
8 | ] @keyword.control.import
9 |
10 | "mod" @keyword.directive
11 |
12 | [
13 | "alias"
14 | "set"
15 | "shell"
16 | ] @keyword
17 |
18 | [
19 | "if"
20 | "else"
21 | ] @keyword.control.conditional
22 |
23 | ; Variables
24 |
25 | (value
26 | (identifier) @variable)
27 |
28 | (alias
29 | left: (identifier) @variable)
30 |
31 | (assignment
32 | left: (identifier) @variable)
33 |
34 | ; Functions
35 |
36 | (recipe_header
37 | name: (identifier) @function)
38 |
39 | (dependency
40 | name: (identifier) @function)
41 |
42 | (dependency_expression
43 | name: (identifier) @function)
44 |
45 | (function_call
46 | name: (identifier) @function)
47 |
48 | ; Parameters
49 |
50 | (parameter
51 | name: (identifier) @variable.parameter)
52 |
53 | ; Namespaces
54 |
55 | (module
56 | name: (identifier) @namespace)
57 |
58 | ; Operators
59 |
60 | [
61 | ":="
62 | "?"
63 | "=="
64 | "!="
65 | "=~"
66 | "@"
67 | "="
68 | "$"
69 | "*"
70 | "+"
71 | "&&"
72 | "@-"
73 | "-@"
74 | "-"
75 | "/"
76 | ":"
77 | ] @operator
78 |
79 | ; Punctuation
80 |
81 | "," @punctuation.delimiter
82 |
83 | [
84 | "{"
85 | "}"
86 | "["
87 | "]"
88 | "("
89 | ")"
90 | "{{"
91 | "}}"
92 | ] @punctuation.bracket
93 |
94 | [ "`" "```" ] @punctuation.special
95 |
96 | ; Literals
97 |
98 | (boolean) @constant.builtin.boolean
99 |
100 | [
101 | (string)
102 | (external_command)
103 | ] @string
104 |
105 | (escape_sequence) @constant.character.escape
106 |
107 | ; Comments
108 |
109 | (comment) @comment.line
110 |
111 | (shebang) @keyword.directive
112 |
113 | ; highlight known settings (filtering does not always work)
114 | (setting
115 | left: (identifier) @keyword
116 | (#any-of? @keyword
117 | "allow-duplicate-recipes"
118 | "allow-duplicate-variables"
119 | "dotenv-filename"
120 | "dotenv-load"
121 | "dotenv-path"
122 | "dotenv-required"
123 | "export"
124 | "fallback"
125 | "ignore-comments"
126 | "positional-arguments"
127 | "shell"
128 | "shell-interpreter"
129 | "tempdir"
130 | "windows-powershell"
131 | "windows-shell"
132 | "working-directory"))
133 |
134 | ; highlight known attributes (filtering does not always work)
135 | (attribute
136 | (identifier) @attribute
137 | (#any-of? @attribute
138 | "confirm"
139 | "doc"
140 | "extension"
141 | "group"
142 | "linux"
143 | "macos"
144 | "no-cd"
145 | "no-exit-message"
146 | "no-quiet"
147 | "positional-arguments"
148 | "private"
149 | "script"
150 | "unix"
151 | "windows"))
152 |
153 | ; Numbers are part of the syntax tree, even if disallowed
154 | (numeric_error) @error
155 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_dispatch:
3 | pull_request:
4 | push:
5 | branches: [main]
6 |
7 | env:
8 | JUST_VERBOSE: 1
9 | RUST_BACKTRACE: 1
10 | CI: 1
11 |
12 | jobs:
13 | codestyle:
14 | name: codestyle & generated files
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: taiki-e/install-action@just
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 20
23 | - run: pip install ruff
24 | - name: Get npm cache directory
25 | id: npm-cache-dir
26 | shell: bash
27 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
28 | - uses: actions/cache@v4
29 | id: npm-cache
30 | with:
31 | path: ${{ steps.npm-cache-dir.outputs.dir }}
32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
33 | restore-keys: ${{ runner.os }}-node-
34 | - run: just setup
35 | - name: Verify generated files are up to date (error)
36 | run: just ci-validate-generated-files
37 | - name: Check codestyle
38 | run: just ci-codestyle
39 |
40 | test:
41 | runs-on: ${{ matrix.os }}
42 | timeout-minutes: 15
43 | strategy:
44 | fail-fast: true
45 | matrix:
46 | os: [macos-latest, ubuntu-latest, windows-latest]
47 | steps:
48 | - uses: actions/checkout@v4
49 | - uses: taiki-e/install-action@just
50 | - uses: actions/setup-node@v4
51 | - uses: mymindstorm/setup-emsdk@v14
52 | with:
53 | node-version: 20
54 | - name: Get npm cache directory
55 | id: npm-cache-dir
56 | shell: bash
57 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
58 | - uses: actions/cache@v4
59 | id: npm-cache
60 | with:
61 | path: ${{ steps.npm-cache-dir.outputs.dir }}
62 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
63 | restore-keys: ${{ runner.os }}-node-
64 | - run: just setup --locked
65 | - name: Configure
66 | run: just configure-tree-sitter
67 | - name: Run tests
68 | run: just test
69 | - name: Check if generated files are up to date (warn only)
70 | run: just ci-validate-generated-files 0
71 | - name: Test WASM build
72 | run: just build-wasm
73 |
74 | success:
75 | needs:
76 | - codestyle
77 | - test
78 | runs-on: ubuntu-latest
79 | # GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency
80 | # failed" as success. So we have to do some contortions to ensure the job fails if any of its
81 | # dependencies fails.
82 | if: always() # make sure this is never "skipped"
83 | steps:
84 | # Manually check the status of all dependencies. `if: failure()` does not work.
85 | - name: check if any dependency failed
86 | run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
87 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/helix/injections.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; Specify nested languages that live within a `justfile`
4 |
5 | ; FIXME: these are not compatible with helix due to precedence
6 |
7 | ; ================ Always applicable ================
8 |
9 | ((comment) @injection.content
10 | (#set! injection.language "comment"))
11 |
12 | ; Highlight the RHS of `=~` as regex
13 | ((regex_literal
14 | (_) @injection.content)
15 | (#set! injection.language "regex"))
16 |
17 | ; ================ Global defaults ================
18 |
19 | ; Default everything to be bash
20 | (recipe_body
21 | !shebang
22 | (#set! injection.language "bash")
23 | (#set! injection.include-children)) @injection.content
24 |
25 | (external_command
26 | (command_body) @injection.content
27 | (#set! injection.language "bash"))
28 |
29 | ; ================ Global language specified ================
30 | ; Global language is set with something like one of the following:
31 | ;
32 | ; set shell := ["bash", "-c", ...]
33 | ; set shell := ["pwsh.exe"]
34 | ;
35 | ; We can extract the first item of the array, but we can't extract the language
36 | ; name from the string with something like regex. So instead we special case
37 | ; two things: powershell, which is likely to come with a `.exe` attachment that
38 | ; we need to strip, and everything else which hopefully has no extension. We
39 | ; separate this with a `#match?`.
40 | ;
41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or
42 | ; alternatively set "global" capture variables. So we can set this for item-
43 | ; level external commands, but not for e.g. external commands within an
44 | ; expression without getting _really_ annoying. Should at least look fine since
45 | ; they default to bash. Limitations...
46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that.
47 |
48 | (source_file
49 | (setting "shell" ":=" "[" (string) @_langstr
50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*")
51 | (#set! injection.language "powershell"))
52 | [
53 | (recipe
54 | (recipe_body
55 | !shebang
56 | (#set! injection.include-children)) @injection.content)
57 |
58 | (assignment
59 | (expression
60 | (value
61 | (external_command
62 | (command_body) @injection.content))))
63 | ])
64 |
65 | (source_file
66 | (setting "shell" ":=" "[" (string) @injection.language
67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*"))
68 | [
69 | (recipe
70 | (recipe_body
71 | !shebang
72 | (#set! injection.include-children)) @injection.content)
73 |
74 | (assignment
75 | (expression
76 | (value
77 | (external_command
78 | (command_body) @injection.content))))
79 | ])
80 |
81 | ; ================ Recipe language specified - Helix only ================
82 |
83 | ; Set highlighting for recipes that specify a language using builtin shebang matching
84 | (recipe_body
85 | (shebang) @injection.shebang
86 | (#set! injection.include-children)) @injection.content
87 |
--------------------------------------------------------------------------------
/src/rule.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | macro_rules! define_rule {
4 | (
5 | $(#[$doc:meta])*
6 | $name:ident {
7 | id: $id:literal,
8 | message: $message:literal,
9 | run($context:ident) $body:block
10 | }
11 | ) => {
12 | $(#[$doc])*
13 | pub(crate) struct $name;
14 |
15 | impl Rule for $name {
16 | fn id(&self) -> &'static str {
17 | $id
18 | }
19 |
20 | fn message(&self) -> &'static str {
21 | $message
22 | }
23 |
24 | fn run(&self, $context: &RuleContext<'_>) -> Vec {
25 | $body
26 | }
27 | }
28 | };
29 | }
30 |
31 | pub(crate) use {
32 | alias_recipe_conflict::AliasRecipeConflictRule,
33 | attribute_arguments::AttributeArgumentsRule,
34 | attribute_invalid_target::AttributeInvalidTargetRule,
35 | attribute_target_support::AttributeTargetSupportRule,
36 | dependency_arguments::DependencyArgumentRule,
37 | duplicate_alias::DuplicateAliasRule,
38 | duplicate_attribute::DuplicateAttributeRule,
39 | duplicate_recipes::DuplicateRecipeRule,
40 | duplicate_setting::DuplicateSettingRule,
41 | duplicate_variables::DuplicateVariableRule,
42 | function_arguments::FunctionArgumentsRule,
43 | inconsistent_indentation::InconsistentIndentationRule,
44 | invalid_setting_kind::InvalidSettingKindRule,
45 | missing_dependencies::MissingDependencyRule,
46 | missing_recipe_for_alias::MissingRecipeForAliasRule,
47 | mixed_indentation::MixedIndentationRule,
48 | parallel_dependencies::ParallelDependenciesRule,
49 | recipe_dependency_cycles::RecipeDependencyCycleRule,
50 | recipe_parameters::RecipeParameterRule,
51 | script_shebang_conflict::ScriptShebangConflictRule, syntax::SyntaxRule,
52 | undefined_identifiers::UndefinedIdentifierRule,
53 | unknown_attribute::UnknownAttributeRule,
54 | unknown_function::UnknownFunctionRule, unknown_setting::UnknownSettingRule,
55 | unused_parameters::UnusedParameterRule, unused_variables::UnusedVariableRule,
56 | working_directory_conflict::WorkingDirectoryConflictRule,
57 | };
58 |
59 | mod alias_recipe_conflict;
60 | mod attribute_arguments;
61 | mod attribute_invalid_target;
62 | mod attribute_target_support;
63 | mod dependency_arguments;
64 | mod duplicate_alias;
65 | mod duplicate_attribute;
66 | mod duplicate_recipes;
67 | mod duplicate_setting;
68 | mod duplicate_variables;
69 | mod function_arguments;
70 | mod inconsistent_indentation;
71 | mod invalid_setting_kind;
72 | mod missing_dependencies;
73 | mod missing_recipe_for_alias;
74 | mod mixed_indentation;
75 | mod parallel_dependencies;
76 | mod recipe_dependency_cycles;
77 | mod recipe_parameters;
78 | mod script_shebang_conflict;
79 | mod syntax;
80 | mod undefined_identifiers;
81 | mod unknown_attribute;
82 | mod unknown_function;
83 | mod unknown_setting;
84 | mod unused_parameters;
85 | mod unused_variables;
86 | mod working_directory_conflict;
87 |
88 | pub(crate) trait Rule: Sync {
89 | /// Unique identifier for the rule.
90 | fn id(&self) -> &'static str;
91 |
92 | /// What to show the user in the header of the diagnostics.
93 | fn message(&self) -> &'static str;
94 |
95 | /// Execute the rule and return diagnostics.
96 | fn run(&self, context: &RuleContext<'_>) -> Vec;
97 | }
98 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use {
2 | alias::Alias,
3 | analyzer::Analyzer,
4 | anyhow::{Error, anyhow, bail},
5 | arguments::Arguments,
6 | ariadne::{Color, Label, Report, ReportKind, sources},
7 | attribute::Attribute,
8 | attribute_target::AttributeTarget,
9 | builtin::Builtin,
10 | builtins::BUILTINS,
11 | clap::Parser as Clap,
12 | command::Command,
13 | count::Count,
14 | dependency::Dependency,
15 | diagnostic::Diagnostic,
16 | document::Document,
17 | env_logger::Env,
18 | function_call::FunctionCall,
19 | group::Group,
20 | node_ext::NodeExt,
21 | once_cell::sync::{Lazy, OnceCell},
22 | parameter::{Parameter, ParameterJson, ParameterKind},
23 | point_ext::PointExt,
24 | recipe::Recipe,
25 | resolver::Resolver,
26 | rope_ext::RopeExt,
27 | ropey::Rope,
28 | rule::*,
29 | rule_context::RuleContext,
30 | serde::{Deserialize, Serialize},
31 | server::Server,
32 | setting::{Setting, SettingKind},
33 | std::{
34 | backtrace::BacktraceStatus,
35 | collections::{BTreeMap, HashMap, HashSet},
36 | env,
37 | fmt::{self, Debug, Display, Formatter, Write},
38 | fs,
39 | path::PathBuf,
40 | process,
41 | sync::{Arc, atomic::AtomicBool},
42 | time::Instant,
43 | },
44 | str_ext::StrExt,
45 | subcommand::Subcommand,
46 | tempfile::tempdir,
47 | text_node::TextNode,
48 | tokio::{io::AsyncBufReadExt, sync::RwLock},
49 | tokio_stream::{StreamExt, wrappers::LinesStream},
50 | tower_lsp::{Client, LanguageServer, LspService, jsonrpc, lsp_types as lsp},
51 | tree_sitter::{InputEdit, Language, Node, Parser, Point, Tree, TreeCursor},
52 | tree_sitter_highlight::{
53 | Highlight, HighlightConfiguration, HighlightEvent, Highlighter,
54 | },
55 | variable::Variable,
56 | };
57 |
58 | mod alias;
59 | mod analyzer;
60 | mod arguments;
61 | mod attribute;
62 | mod attribute_target;
63 | mod builtin;
64 | mod builtins;
65 | mod command;
66 | mod count;
67 | mod dependency;
68 | mod diagnostic;
69 | mod document;
70 | mod function_call;
71 | mod group;
72 | mod node_ext;
73 | mod parameter;
74 | mod point_ext;
75 | mod position_ext;
76 | mod recipe;
77 | mod resolver;
78 | mod rope_ext;
79 | mod rule;
80 | mod rule_context;
81 | mod server;
82 | mod setting;
83 | mod str_ext;
84 | mod subcommand;
85 | mod text_node;
86 | mod tokenizer;
87 | mod variable;
88 |
89 | type Result = std::result::Result;
90 |
91 | unsafe extern "C" {
92 | pub(crate) fn tree_sitter_just() -> Language;
93 | }
94 |
95 | #[tokio::main]
96 | async fn main() {
97 | let env = Env::default().default_filter_or("info");
98 |
99 | env_logger::Builder::from_env(env).init();
100 |
101 | if let Err(error) = Arguments::parse().run().await {
102 | eprintln!("error: {error}");
103 |
104 | for (i, error) in error.chain().skip(1).enumerate() {
105 | if i == 0 {
106 | eprintln!();
107 | eprintln!("because:");
108 | }
109 |
110 | eprintln!("- {error}");
111 | }
112 |
113 | let backtrace = error.backtrace();
114 |
115 | if backtrace.status() == BacktraceStatus::Captured {
116 | eprintln!("backtrace:");
117 | eprintln!("{backtrace}");
118 | }
119 |
120 | process::exit(1);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/GRAMMAR.md:
--------------------------------------------------------------------------------
1 | # justfile grammar
2 |
3 | Justfiles are processed by a mildly context-sensitive tokenizer
4 | and a recursive descent parser. The grammar is LL(k), for an
5 | unknown but hopefully reasonable value of k.
6 |
7 | ## tokens
8 |
9 | ````
10 | BACKTICK = `[^`]*`
11 | INDENTED_BACKTICK = ```[^(```)]*```
12 | COMMENT = #([^!].*)?$
13 | DEDENT = emitted when indentation decreases
14 | EOF = emitted at the end of the file
15 | INDENT = emitted when indentation increases
16 | LINE = emitted before a recipe line
17 | NAME = [a-zA-Z_][a-zA-Z0-9_-]*
18 | NEWLINE = \n|\r\n
19 | RAW_STRING = '[^']*'
20 | INDENTED_RAW_STRING = '''[^(''')]*'''
21 | STRING = "[^"]*" # also processes \n \r \t \" \\ escapes
22 | INDENTED_STRING = """[^("""]*""" # also processes \n \r \t \" \\ escapes
23 | TEXT = recipe text, only matches in a recipe body
24 | ````
25 |
26 | ## grammar syntax
27 |
28 | ```
29 | | alternation
30 | () grouping
31 | _? option (0 or 1 times)
32 | _* repetition (0 or more times)
33 | _+ repetition (1 or more times)
34 | ```
35 |
36 | ## grammar
37 |
38 | ```
39 | justfile : item* EOF
40 |
41 | item : recipe
42 | | alias
43 | | assignment
44 | | export
45 | | import
46 | | module
47 | | setting
48 | | eol
49 |
50 | eol : NEWLINE
51 | | COMMENT NEWLINE
52 |
53 | alias : 'alias' NAME ':=' NAME
54 |
55 | assignment : NAME ':=' expression eol
56 |
57 | export : 'export' assignment
58 |
59 | setting : 'set' 'dotenv-load' boolean?
60 | | 'set' 'export' boolean?
61 | | 'set' 'positional-arguments' boolean?
62 | | 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
63 |
64 | import : 'import' '?'? string?
65 |
66 | module : 'mod' '?'? NAME string?
67 |
68 | boolean : ':=' ('true' | 'false')
69 |
70 | expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
71 | | value '/' expression
72 | | value '+' expression
73 | | value
74 |
75 | condition : expression '==' expression
76 | | expression '!=' expression
77 | | expression '=~' expression
78 |
79 | value : NAME '(' sequence? ')'
80 | | BACKTICK
81 | | INDENTED_BACKTICK
82 | | NAME
83 | | string
84 | | '(' expression ')'
85 |
86 | string : STRING
87 | | INDENTED_STRING
88 | | RAW_STRING
89 | | INDENTED_RAW_STRING
90 |
91 | sequence : expression ',' sequence
92 | | expression ','?
93 |
94 | recipe : attribute? '@'? NAME parameter* variadic? ':' dependency* body?
95 |
96 | attribute : '[' NAME ']' eol
97 |
98 | parameter : '$'? NAME
99 | | '$'? NAME '=' value
100 |
101 | variadic : '*' parameter
102 | | '+' parameter
103 |
104 | dependency : NAME
105 | | '(' NAME expression* ')'
106 |
107 | body : INDENT line+ DEDENT
108 |
109 | line : LINE (TEXT | interpolation)+ NEWLINE
110 | | NEWLINE
111 |
112 | interpolation : '{{' expression '}}'
113 | ```
114 |
--------------------------------------------------------------------------------
/queries/injections.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; Specify nested languages that live within a `justfile`
4 |
5 | ; ================ Always applicable ================
6 |
7 | ((comment) @injection.content
8 | (#set! injection.language "comment"))
9 |
10 | ; Highlight the RHS of `=~` as regex
11 | ((regex_literal
12 | (_) @injection.content)
13 | (#set! injection.language "regex"))
14 |
15 | ; ================ Global defaults ================
16 |
17 | ; Default everything to be bash
18 | (recipe_body
19 | !shebang
20 | (#set! injection.language "bash")
21 | (#set! injection.include-children)) @injection.content
22 |
23 | (external_command
24 | (command_body) @injection.content
25 | (#set! injection.language "bash"))
26 |
27 | ; ================ Global language specified ================
28 | ; Global language is set with something like one of the following:
29 | ;
30 | ; set shell := ["bash", "-c", ...]
31 | ; set shell := ["pwsh.exe"]
32 | ;
33 | ; We can extract the first item of the array, but we can't extract the language
34 | ; name from the string with something like regex. So instead we special case
35 | ; two things: powershell, which is likely to come with a `.exe` attachment that
36 | ; we need to strip, and everything else which hopefully has no extension. We
37 | ; separate this with a `#match?`.
38 | ;
39 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or
40 | ; alternatively set "global" capture variables. So we can set this for item-
41 | ; level external commands, but not for e.g. external commands within an
42 | ; expression without getting _really_ annoying. Should at least look fine since
43 | ; they default to bash. Limitations...
44 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that.
45 |
46 | (source_file
47 | (setting "shell" ":=" "[" (string) @_langstr
48 | (#match? @_langstr ".*(powershell|pwsh|cmd).*")
49 | (#set! injection.language "powershell"))
50 | [
51 | (recipe
52 | (recipe_body
53 | !shebang
54 | (#set! injection.include-children)) @injection.content)
55 |
56 | (assignment
57 | (expression
58 | (value
59 | (external_command
60 | (command_body) @injection.content))))
61 | ])
62 |
63 | (source_file
64 | (setting "shell" ":=" "[" (string) @injection.language
65 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*"))
66 | [
67 | (recipe
68 | (recipe_body
69 | !shebang
70 | (#set! injection.include-children)) @injection.content)
71 |
72 | (assignment
73 | (expression
74 | (value
75 | (external_command
76 | (command_body) @injection.content))))
77 | ])
78 |
79 | ; ================ Recipe language specified ================
80 |
81 | ; Set highlighting for recipes that specify a language, using the exact name by default
82 | (recipe_body ;
83 | (shebang ;
84 | (language) @injection.language)
85 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv")
86 | (#set! injection.include-children)) @injection.content
87 |
88 | ; Transform some known executables
89 |
90 | ; python3/uv -> python
91 | (recipe_body
92 | (shebang
93 | (language) @_lang)
94 | (#any-of? @_lang "python3" "uv")
95 | (#set! injection.language "python")
96 | (#set! injection.include-children)) @injection.content
97 |
98 | ; node/nodejs -> javascript
99 | (recipe_body
100 | (shebang
101 | (language) @_lang)
102 | (#any-of? @_lang "node" "nodejs")
103 | (#set! injection.language "javascript")
104 | (#set! injection.include-children)) @injection.content
105 |
--------------------------------------------------------------------------------
/src/rule/recipe_dependency_cycles.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | define_rule! {
4 | /// Detects circular dependency chains between recipes to prevent infinite
5 | /// execution loops.
6 | RecipeDependencyCycleRule {
7 | id: "recipe-dependency-cycles",
8 | message: "circular dependency",
9 | run(context) {
10 | let mut dependency_graph = HashMap::new();
11 | let mut diagnostics = Vec::new();
12 |
13 | for recipe in context.recipes() {
14 | dependency_graph.insert(
15 | recipe.name.value.clone(),
16 | recipe
17 | .dependencies
18 | .iter()
19 | .map(|dep| dep.name.clone())
20 | .collect::>(),
21 | );
22 | }
23 |
24 | let mut reported_recipes = HashSet::new();
25 |
26 | for recipe in context.recipes() {
27 | let mut path = Vec::new();
28 | let mut visited = HashSet::new();
29 |
30 | let mut traversal_state = TraversalState {
31 | visited: &mut visited,
32 | path: &mut path,
33 | reported_recipes: &mut reported_recipes,
34 | };
35 |
36 | RecipeDependencyCycleRule::detect_cycle(
37 | &recipe.name.value,
38 | &dependency_graph,
39 | &mut diagnostics,
40 | context,
41 | &mut traversal_state,
42 | );
43 | }
44 |
45 | diagnostics
46 | }
47 | }
48 | }
49 |
50 | struct TraversalState<'a> {
51 | path: &'a mut Vec,
52 | reported_recipes: &'a mut HashSet,
53 | visited: &'a mut HashSet,
54 | }
55 |
56 | impl RecipeDependencyCycleRule {
57 | fn detect_cycle(
58 | recipe_name: &str,
59 | graph: &HashMap>,
60 | diagnostics: &mut Vec,
61 | context: &RuleContext<'_>,
62 | traversal: &mut TraversalState<'_>,
63 | ) {
64 | if traversal.visited.contains(recipe_name) {
65 | return;
66 | }
67 |
68 | if traversal.path.iter().any(|r| r == recipe_name) {
69 | let cycle_start_idx = traversal
70 | .path
71 | .iter()
72 | .position(|r| r == recipe_name)
73 | .unwrap();
74 |
75 | let mut cycle = traversal.path[cycle_start_idx..].to_vec();
76 | cycle.push(recipe_name.to_string());
77 |
78 | if let Some(recipe) = context.recipe(recipe_name) {
79 | let message = if cycle.len() == 2 && cycle[0] == cycle[1] {
80 | format!("Recipe `{}` depends on itself", cycle[0])
81 | } else if cycle[0] == recipe_name {
82 | format!(
83 | "Recipe `{}` has circular dependency `{}`",
84 | recipe_name,
85 | cycle.join(" -> ")
86 | )
87 | } else {
88 | traversal.path.push(recipe_name.to_string());
89 | return;
90 | };
91 |
92 | if !traversal.reported_recipes.insert(recipe_name.to_string()) {
93 | return;
94 | }
95 |
96 | diagnostics.push(Diagnostic::error(message, recipe.range));
97 | }
98 |
99 | return;
100 | }
101 |
102 | if !graph.contains_key(recipe_name) {
103 | return;
104 | }
105 |
106 | traversal.path.push(recipe_name.to_string());
107 |
108 | if let Some(dependencies) = graph.get(recipe_name) {
109 | for dependency in dependencies {
110 | RecipeDependencyCycleRule::detect_cycle(
111 | dependency,
112 | graph,
113 | diagnostics,
114 | context,
115 | traversal,
116 | );
117 | }
118 | }
119 |
120 | traversal.visited.insert(recipe_name.to_string());
121 |
122 | traversal.path.pop();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries/just/injections.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; Specify nested languages that live within a `justfile`
4 |
5 | ; ================ Always applicable ================
6 |
7 | ((comment) @injection.content
8 | (#set! injection.language "comment"))
9 |
10 | ; Highlight the RHS of `=~` as regex
11 | ((regex_literal
12 | (_) @injection.content)
13 | (#set! injection.language "regex"))
14 |
15 | ; ================ Global defaults ================
16 |
17 | ; Default everything to be bash
18 | (recipe_body
19 | !shebang
20 | (#set! injection.language "bash")
21 | (#set! injection.include-children)) @injection.content
22 |
23 | (external_command
24 | (command_body) @injection.content
25 | (#set! injection.language "bash"))
26 |
27 | ; ================ Global language specified ================
28 | ; Global language is set with something like one of the following:
29 | ;
30 | ; set shell := ["bash", "-c", ...]
31 | ; set shell := ["pwsh.exe"]
32 | ;
33 | ; We can extract the first item of the array, but we can't extract the language
34 | ; name from the string with something like regex. So instead we special case
35 | ; two things: powershell, which is likely to come with a `.exe` attachment that
36 | ; we need to strip, and everything else which hopefully has no extension. We
37 | ; separate this with a `#match?`.
38 | ;
39 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or
40 | ; alternatively set "global" capture variables. So we can set this for item-
41 | ; level external commands, but not for e.g. external commands within an
42 | ; expression without getting _really_ annoying. Should at least look fine since
43 | ; they default to bash. Limitations...
44 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that.
45 |
46 | (source_file
47 | (setting "shell" ":=" "[" (string) @_langstr
48 | (#match? @_langstr ".*(powershell|pwsh|cmd).*")
49 | (#set! injection.language "powershell"))
50 | [
51 | (recipe
52 | (recipe_body
53 | !shebang
54 | (#set! injection.include-children)) @injection.content)
55 |
56 | (assignment
57 | (expression
58 | (value
59 | (external_command
60 | (command_body) @injection.content))))
61 | ])
62 |
63 | (source_file
64 | (setting "shell" ":=" "[" (string) @injection.language
65 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*"))
66 | [
67 | (recipe
68 | (recipe_body
69 | !shebang
70 | (#set! injection.include-children)) @injection.content)
71 |
72 | (assignment
73 | (expression
74 | (value
75 | (external_command
76 | (command_body) @injection.content))))
77 | ])
78 |
79 | ; ================ Recipe language specified ================
80 |
81 | ; Set highlighting for recipes that specify a language, using the exact name by default
82 | (recipe_body ;
83 | (shebang ;
84 | (language) @injection.language)
85 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv")
86 | (#set! injection.include-children)) @injection.content
87 |
88 | ; Transform some known executables
89 |
90 | ; python3/uv -> python
91 | (recipe_body
92 | (shebang
93 | (language) @_lang)
94 | (#any-of? @_lang "python3" "uv")
95 | (#set! injection.language "python")
96 | (#set! injection.include-children)) @injection.content
97 |
98 | ; node/nodejs -> javascript
99 | (recipe_body
100 | (shebang
101 | (language) @_lang)
102 | (#any-of? @_lang "node" "nodejs")
103 | (#set! injection.language "javascript")
104 | (#set! injection.include-children)) @injection.content
105 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/lapce/injections.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; Specify nested languages that live within a `justfile`
4 |
5 | ; FIXME: these are not compatible with helix due to precedence
6 |
7 | ; ================ Always applicable ================
8 |
9 | ((comment) @injection.content
10 | (#set! injection.language "comment"))
11 |
12 | ; Highlight the RHS of `=~` as regex
13 | ((regex_literal
14 | (_) @injection.content)
15 | (#set! injection.language "regex"))
16 |
17 | ; ================ Global defaults ================
18 |
19 | ; Default everything to be bash
20 | (recipe_body
21 | !shebang
22 | (#set! injection.language "bash")
23 | (#set! injection.include-children)) @injection.content
24 |
25 | (external_command
26 | (command_body) @injection.content
27 | (#set! injection.language "bash"))
28 |
29 | ; ================ Global language specified ================
30 | ; Global language is set with something like one of the following:
31 | ;
32 | ; set shell := ["bash", "-c", ...]
33 | ; set shell := ["pwsh.exe"]
34 | ;
35 | ; We can extract the first item of the array, but we can't extract the language
36 | ; name from the string with something like regex. So instead we special case
37 | ; two things: powershell, which is likely to come with a `.exe` attachment that
38 | ; we need to strip, and everything else which hopefully has no extension. We
39 | ; separate this with a `#match?`.
40 | ;
41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or
42 | ; alternatively set "global" capture variables. So we can set this for item-
43 | ; level external commands, but not for e.g. external commands within an
44 | ; expression without getting _really_ annoying. Should at least look fine since
45 | ; they default to bash. Limitations...
46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that.
47 |
48 | (source_file
49 | (setting "shell" ":=" "[" (string) @_langstr
50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*")
51 | (#set! injection.language "powershell"))
52 | [
53 | (recipe
54 | (recipe_body
55 | !shebang
56 | (#set! injection.include-children)) @injection.content)
57 |
58 | (assignment
59 | (expression
60 | (value
61 | (external_command
62 | (command_body) @injection.content))))
63 | ])
64 |
65 | (source_file
66 | (setting "shell" ":=" "[" (string) @injection.language
67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*"))
68 | [
69 | (recipe
70 | (recipe_body
71 | !shebang
72 | (#set! injection.include-children)) @injection.content)
73 |
74 | (assignment
75 | (expression
76 | (value
77 | (external_command
78 | (command_body) @injection.content))))
79 | ])
80 |
81 | ; ================ Recipe language specified ================
82 |
83 | ; Set highlighting for recipes that specify a language, using the exact name by default
84 | (recipe_body ;
85 | (shebang ;
86 | (language) @injection.language)
87 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv")
88 | (#set! injection.include-children)) @injection.content
89 |
90 | ; Transform some known executables
91 |
92 | ; python3/uv -> python
93 | (recipe_body
94 | (shebang
95 | (language) @_lang)
96 | (#any-of? @_lang "python3" "uv")
97 | (#set! injection.language "python")
98 | (#set! injection.include-children)) @injection.content
99 |
100 | ; node/nodejs -> javascript
101 | (recipe_body
102 | (shebang
103 | (language) @_lang)
104 | (#any-of? @_lang "node" "nodejs")
105 | (#set! injection.language "javascript")
106 | (#set! injection.include-children)) @injection.content
107 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/queries-flavored/zed/injections.scm:
--------------------------------------------------------------------------------
1 | ; File autogenerated by build-queries-nvim.py; do not edit
2 |
3 | ; Specify nested languages that live within a `justfile`
4 |
5 | ; FIXME: these are not compatible with helix due to precedence
6 |
7 | ; ================ Always applicable ================
8 |
9 | ((comment) @injection.content
10 | (#set! injection.language "comment"))
11 |
12 | ; Highlight the RHS of `=~` as regex
13 | ((regex_literal
14 | (_) @injection.content)
15 | (#set! injection.language "regex"))
16 |
17 | ; ================ Global defaults ================
18 |
19 | ; Default everything to be bash
20 | (recipe_body
21 | !shebang
22 | (#set! injection.language "bash")
23 | (#set! injection.include-children)) @injection.content
24 |
25 | (external_command
26 | (command_body) @injection.content
27 | (#set! injection.language "bash"))
28 |
29 | ; ================ Global language specified ================
30 | ; Global language is set with something like one of the following:
31 | ;
32 | ; set shell := ["bash", "-c", ...]
33 | ; set shell := ["pwsh.exe"]
34 | ;
35 | ; We can extract the first item of the array, but we can't extract the language
36 | ; name from the string with something like regex. So instead we special case
37 | ; two things: powershell, which is likely to come with a `.exe` attachment that
38 | ; we need to strip, and everything else which hopefully has no extension. We
39 | ; separate this with a `#match?`.
40 | ;
41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or
42 | ; alternatively set "global" capture variables. So we can set this for item-
43 | ; level external commands, but not for e.g. external commands within an
44 | ; expression without getting _really_ annoying. Should at least look fine since
45 | ; they default to bash. Limitations...
46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that.
47 |
48 | (source_file
49 | (setting "shell" ":=" "[" (string) @_langstr
50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*")
51 | (#set! injection.language "powershell"))
52 | [
53 | (recipe
54 | (recipe_body
55 | !shebang
56 | (#set! injection.include-children)) @injection.content)
57 |
58 | (assignment
59 | (expression
60 | (value
61 | (external_command
62 | (command_body) @injection.content))))
63 | ])
64 |
65 | (source_file
66 | (setting "shell" ":=" "[" (string) @injection.language
67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*"))
68 | [
69 | (recipe
70 | (recipe_body
71 | !shebang
72 | (#set! injection.include-children)) @injection.content)
73 |
74 | (assignment
75 | (expression
76 | (value
77 | (external_command
78 | (command_body) @injection.content))))
79 | ])
80 |
81 | ; ================ Recipe language specified ================
82 |
83 | ; Set highlighting for recipes that specify a language, using the exact name by default
84 | (recipe_body ;
85 | (shebang ;
86 | (language) @injection.language)
87 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv")
88 | (#set! injection.include-children)) @injection.content
89 |
90 | ; Transform some known executables
91 |
92 | ; python3/uv -> python
93 | (recipe_body
94 | (shebang
95 | (language) @_lang)
96 | (#any-of? @_lang "python3" "uv")
97 | (#set! injection.language "python")
98 | (#set! injection.include-children)) @injection.content
99 |
100 | ; node/nodejs -> javascript
101 | (recipe_body
102 | (shebang
103 | (language) @_lang)
104 | (#any-of? @_lang "node" "nodejs")
105 | (#set! injection.language "javascript")
106 | (#set! injection.include-children)) @injection.content
107 |
--------------------------------------------------------------------------------
/src/subcommand/analyze.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | #[derive(Debug, Clap)]
4 | pub(crate) struct Analyze {
5 | #[arg(
6 | value_name = "PATH",
7 | help = "Path to the justfile to analyze",
8 | value_hint = clap::ValueHint::FilePath
9 | )]
10 | path: Option,
11 | }
12 |
13 | impl Analyze {
14 | pub(crate) fn run(self) -> Result<()> {
15 | let path = match self.path {
16 | Some(path) => path,
17 | None => Subcommand::find_justfile()?,
18 | };
19 |
20 | let content = fs::read_to_string(&path)?;
21 |
22 | let absolute_path = if path.is_absolute() {
23 | path.clone()
24 | } else {
25 | env::current_dir()?.join(&path)
26 | };
27 |
28 | let uri = lsp::Url::from_file_path(&absolute_path).map_err(|()| {
29 | anyhow!("failed to convert `{}` to file url", path.display())
30 | })?;
31 |
32 | let document = Document::try_from(lsp::DidOpenTextDocumentParams {
33 | text_document: lsp::TextDocumentItem {
34 | language_id: "just".to_string(),
35 | text: content.clone(),
36 | uri,
37 | version: 1,
38 | },
39 | })?;
40 |
41 | let analyzer = Analyzer::new(&document);
42 |
43 | let mut diagnostics = analyzer.analyze();
44 |
45 | if diagnostics.is_empty() {
46 | return Ok(());
47 | }
48 |
49 | diagnostics.sort_by_key(|diagnostic| {
50 | (
51 | diagnostic.range.start.line,
52 | diagnostic.range.start.character,
53 | diagnostic.range.end.line,
54 | diagnostic.range.end.character,
55 | )
56 | });
57 |
58 | let any_error = diagnostics.iter().any(|diagnostic| {
59 | matches!(diagnostic.severity, lsp::DiagnosticSeverity::ERROR)
60 | });
61 |
62 | let source_id = path.to_string_lossy().to_string();
63 |
64 | let mut cache = sources(vec![(source_id.clone(), content.as_str())]);
65 |
66 | let source_len = document.content.len_chars();
67 |
68 | for diagnostic in diagnostics {
69 | let (severity_label, color) =
70 | Self::severity_to_style(diagnostic.severity)?;
71 |
72 | let kind_label = format!("{severity_label}[{}]", diagnostic.id.trim());
73 |
74 | let start = document
75 | .content
76 | .lsp_position_to_position(diagnostic.range.start)
77 | .char
78 | .min(source_len);
79 |
80 | let end = document
81 | .content
82 | .lsp_position_to_position(diagnostic.range.end)
83 | .char
84 | .min(source_len);
85 |
86 | let (start, end) = (start.min(end), start.max(end));
87 |
88 | let span = (source_id.clone(), start..end);
89 |
90 | let report = Report::build(
91 | ReportKind::Custom(kind_label.as_str(), color),
92 | span.clone(),
93 | )
94 | .with_message(&diagnostic.display)
95 | .with_label(
96 | Label::new(span.clone())
97 | .with_message(diagnostic.message.trim().to_string())
98 | .with_color(color),
99 | );
100 |
101 | let report = report.finish();
102 |
103 | report
104 | .print(&mut cache)
105 | .map_err(|error| anyhow!("failed to render diagnostic: {error}"))?;
106 | }
107 |
108 | if any_error {
109 | process::exit(1);
110 | }
111 |
112 | Ok(())
113 | }
114 |
115 | fn severity_to_style(
116 | severity: lsp::DiagnosticSeverity,
117 | ) -> Result<(&'static str, Color)> {
118 | match severity {
119 | lsp::DiagnosticSeverity::ERROR => Ok(("error", Color::Red)),
120 | lsp::DiagnosticSeverity::WARNING => Ok(("warning", Color::Yellow)),
121 | lsp::DiagnosticSeverity::INFORMATION => Ok(("info", Color::Blue)),
122 | lsp::DiagnosticSeverity::HINT => Ok(("hint", Color::Cyan)),
123 | _ => bail!("failed to map unknown severity {severity:?}"),
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/www/src/lib/just-syntax-highlighting.ts:
--------------------------------------------------------------------------------
1 | import { type Extension, RangeSetBuilder } from '@codemirror/state';
2 | import {
3 | Decoration,
4 | EditorView,
5 | ViewPlugin,
6 | type ViewUpdate,
7 | } from '@codemirror/view';
8 | import type { Language, Parser, Query } from 'web-tree-sitter';
9 | import { Parser as TreeSitterParser } from 'web-tree-sitter';
10 |
11 | import highlightsQuerySource from '../../../queries/highlights.scm?raw';
12 |
13 | const BASE_CAPTURE_TO_CLASSES: Record = {
14 | attribute: ['cm-just-attribute'],
15 | boolean: ['cm-just-boolean'],
16 | comment: ['cm-just-comment'],
17 | error: ['cm-just-error'],
18 | function: ['cm-just-function'],
19 | keyword: ['cm-just-keyword'],
20 | module: ['cm-just-namespace'],
21 | operator: ['cm-just-operator'],
22 | punctuation: ['cm-just-punctuation'],
23 | string: ['cm-just-string'],
24 | variable: ['cm-just-variable'],
25 | };
26 |
27 | const captureNameToClasses = (name: string): string[] => {
28 | const [base] = name.split('.');
29 | return BASE_CAPTURE_TO_CLASSES[base] ?? [];
30 | };
31 |
32 | const buildDecorations = (parser: Parser, query: Query, content: string) => {
33 | const tree = parser.parse(content);
34 |
35 | if (!tree) {
36 | return Decoration.none;
37 | }
38 |
39 | const captures = query.captures(tree.rootNode);
40 | const ranges = new Map>();
41 |
42 | for (const { name, node } of captures) {
43 | const from = node.startIndex;
44 | const to = node.endIndex;
45 |
46 | if (from === to) {
47 | continue;
48 | }
49 |
50 | const classes = captureNameToClasses(name);
51 |
52 | if (classes.length === 0) {
53 | continue;
54 | }
55 |
56 | const key = `${from}:${to}`;
57 | const classSet = ranges.get(key) ?? new Set();
58 |
59 | classes.forEach((cls) => classSet.add(cls));
60 | ranges.set(key, classSet);
61 | }
62 |
63 | const builder = new RangeSetBuilder();
64 |
65 | Array.from(ranges.entries())
66 | .map(([key, classSet]) => {
67 | const [from, to] = key.split(':').map(Number);
68 | return { from, to, className: Array.from(classSet).join(' ') };
69 | })
70 | .sort((a, b) => a.from - b.from || a.to - b.to)
71 | .forEach(({ from, to, className }) => {
72 | builder.add(from, to, Decoration.mark({ class: className }));
73 | });
74 |
75 | tree.delete();
76 |
77 | return builder.finish();
78 | };
79 |
80 | export const createJustSyntaxHighlightingExtension = (
81 | language: Language | undefined
82 | ): Extension[] => {
83 | if (!language) {
84 | return [];
85 | }
86 |
87 | let query: Query;
88 |
89 | try {
90 | query = language.query(highlightsQuerySource);
91 | } catch (error) {
92 | console.error('Failed to compile Just highlight query', error);
93 | return [];
94 | }
95 | const lang = language;
96 |
97 | const plugin = ViewPlugin.fromClass(
98 | class {
99 | decorations = Decoration.none;
100 | private parser: Parser;
101 |
102 | constructor(view: EditorView) {
103 | this.parser = new TreeSitterParser();
104 | this.parser.setLanguage(lang);
105 | this.decorations = buildDecorations(
106 | this.parser,
107 | query,
108 | view.state.doc.toString()
109 | );
110 | }
111 |
112 | update(update: ViewUpdate) {
113 | if (update.docChanged) {
114 | this.decorations = buildDecorations(
115 | this.parser,
116 | query,
117 | update.state.doc.toString()
118 | );
119 | }
120 | }
121 |
122 | destroy() {
123 | this.parser.delete();
124 | }
125 | },
126 | {
127 | decorations: (v) => v.decorations,
128 | }
129 | );
130 |
131 | return [plugin];
132 | };
133 |
--------------------------------------------------------------------------------
/src/rule/mixed_indentation.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | define_rule! {
4 | /// Detects recipes that mix tabs and spaces for indentation, which often
5 | /// results in confusing or invalid `just` bodies.
6 | MixedIndentationRule {
7 | id: "mixed-recipe-indentation",
8 | message: "mixed indentation",
9 | run(context) {
10 | let mut diagnostics = Vec::new();
11 |
12 | let Some(tree) = context.tree() else {
13 | return diagnostics;
14 | };
15 |
16 | let document = context.document();
17 |
18 | for recipe_node in tree.root_node().find_all("recipe") {
19 | if recipe_node.find("recipe_body").is_none() {
20 | continue;
21 | }
22 |
23 | if let Some(diagnostic) = MixedIndentationRule::inspect_recipe(document, &recipe_node) {
24 | diagnostics.push(diagnostic);
25 | }
26 | }
27 |
28 | diagnostics
29 | }
30 | }
31 | }
32 |
33 | impl MixedIndentationRule {
34 | fn diagnostic_for_line(
35 | recipe_name: &str,
36 | line: u32,
37 | indent_length: usize,
38 | ) -> Diagnostic {
39 | let indent = u32::try_from(indent_length).unwrap_or(u32::MAX);
40 |
41 | let range = lsp::Range {
42 | start: lsp::Position { line, character: 0 },
43 | end: lsp::Position {
44 | line,
45 | character: indent,
46 | },
47 | };
48 |
49 | Diagnostic::error(
50 | format!("Recipe `{recipe_name}` mixes tabs and spaces for indentation"),
51 | range,
52 | )
53 | }
54 |
55 | fn inspect_recipe(
56 | document: &Document,
57 | recipe_node: &Node<'_>,
58 | ) -> Option {
59 | let recipe_name =
60 | recipe_node.find("recipe_header > identifier").map_or_else(
61 | || "recipe".to_string(),
62 | |node| document.get_node_text(&node),
63 | );
64 |
65 | let mut indent_style: Option = None;
66 |
67 | for line_node in recipe_node.find_all("recipe_line") {
68 | let line_range = line_node.get_range(document);
69 |
70 | let Ok(line_idx) = usize::try_from(line_range.start.line) else {
71 | continue;
72 | };
73 |
74 | if line_idx >= document.content.len_lines() {
75 | continue;
76 | }
77 |
78 | let line = document.content.line(line_idx).to_string();
79 |
80 | if line.trim().is_empty() {
81 | continue;
82 | }
83 |
84 | let mut indent_length = 0usize;
85 |
86 | let (mut has_space, mut has_tab) = (false, false);
87 |
88 | for ch in line.chars() {
89 | match ch {
90 | ' ' => {
91 | indent_length += 1;
92 | has_space = true;
93 | }
94 | '\t' => {
95 | indent_length += 1;
96 | has_tab = true;
97 | }
98 | _ => break,
99 | }
100 | }
101 |
102 | if indent_length == 0 {
103 | continue;
104 | }
105 |
106 | if has_space && has_tab {
107 | return Some(MixedIndentationRule::diagnostic_for_line(
108 | &recipe_name,
109 | line_range.start.line,
110 | indent_length,
111 | ));
112 | }
113 |
114 | let current_style = if has_space {
115 | IndentStyle::Spaces
116 | } else if has_tab {
117 | IndentStyle::Tabs
118 | } else {
119 | continue;
120 | };
121 |
122 | match indent_style {
123 | None => indent_style = Some(current_style),
124 | Some(style) if style != current_style => {
125 | return Some(MixedIndentationRule::diagnostic_for_line(
126 | &recipe_name,
127 | line_range.start.line,
128 | indent_length,
129 | ));
130 | }
131 | _ => {}
132 | }
133 | }
134 |
135 | None
136 | }
137 | }
138 |
139 | #[derive(Clone, Copy, PartialEq, Eq)]
140 | enum IndentStyle {
141 | Spaces,
142 | Tabs,
143 | }
144 |
--------------------------------------------------------------------------------
/vendor/tree-sitter-just/Makefile:
--------------------------------------------------------------------------------
1 | VERSION := 0.0.1
2 |
3 | LANGUAGE_NAME := tree-sitter-just
4 |
5 | # repository
6 | SRC_DIR := src
7 |
8 | PARSER_REPO_URL := $(shell git -C $(SRC_DIR) remote get-url origin 2>/dev/null)
9 |
10 | ifeq ($(PARSER_URL),)
11 | PARSER_URL := $(subst .git,,$(PARSER_REPO_URL))
12 | ifeq ($(shell echo $(PARSER_URL) | grep '^[a-z][-+.0-9a-z]*://'),)
13 | PARSER_URL := $(subst :,/,$(PARSER_URL))
14 | PARSER_URL := $(subst git@,https://,$(PARSER_URL))
15 | endif
16 | endif
17 |
18 | TS ?= tree-sitter
19 |
20 | # ABI versioning
21 | SONAME_MAJOR := $(word 1,$(subst ., ,$(VERSION)))
22 | SONAME_MINOR := $(word 2,$(subst ., ,$(VERSION)))
23 |
24 | # install directory layout
25 | PREFIX ?= /usr/local
26 | INCLUDEDIR ?= $(PREFIX)/include
27 | LIBDIR ?= $(PREFIX)/lib
28 | PCLIBDIR ?= $(LIBDIR)/pkgconfig
29 |
30 | # source/object files
31 | PARSER := $(SRC_DIR)/parser.c
32 | EXTRAS := $(filter-out $(PARSER),$(wildcard $(SRC_DIR)/*.c))
33 | OBJS := $(patsubst %.c,%.o,$(PARSER) $(EXTRAS))
34 |
35 | # flags
36 | ARFLAGS ?= rcs
37 | override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC
38 |
39 | # OS-specific bits
40 | ifeq ($(OS),Windows_NT)
41 | $(error "Windows is not supported")
42 | else ifeq ($(shell uname),Darwin)
43 | SOEXT = dylib
44 | SOEXTVER_MAJOR = $(SONAME_MAJOR).dylib
45 | SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).dylib
46 | LINKSHARED := $(LINKSHARED)-dynamiclib -Wl,
47 | ifneq ($(ADDITIONAL_LIBS),)
48 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS),
49 | endif
50 | LINKSHARED := $(LINKSHARED)-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SONAME_MAJOR).dylib,-rpath,@executable_path/../Frameworks
51 | else
52 | SOEXT = so
53 | SOEXTVER_MAJOR = so.$(SONAME_MAJOR)
54 | SOEXTVER = so.$(SONAME_MAJOR).$(SONAME_MINOR)
55 | LINKSHARED := $(LINKSHARED)-shared -Wl,
56 | ifneq ($(ADDITIONAL_LIBS),)
57 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS)
58 | endif
59 | LINKSHARED := $(LINKSHARED)-soname,lib$(LANGUAGE_NAME).so.$(SONAME_MAJOR)
60 | endif
61 | ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),)
62 | PCLIBDIR := $(PREFIX)/libdata/pkgconfig
63 | endif
64 |
65 | all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc
66 |
67 | lib$(LANGUAGE_NAME).a: $(OBJS)
68 | $(AR) $(ARFLAGS) $@ $^
69 |
70 | lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS)
71 | $(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@
72 | ifneq ($(STRIP),)
73 | $(STRIP) $@
74 | endif
75 |
76 | $(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in
77 | sed -e 's|@URL@|$(PARSER_URL)|' \
78 | -e 's|@VERSION@|$(VERSION)|' \
79 | -e 's|@LIBDIR@|$(LIBDIR)|' \
80 | -e 's|@INCLUDEDIR@|$(INCLUDEDIR)|' \
81 | -e 's|@REQUIRES@|$(REQUIRES)|' \
82 | -e 's|@ADDITIONAL_LIBS@|$(ADDITIONAL_LIBS)|' \
83 | -e 's|=$(PREFIX)|=$${prefix}|' \
84 | -e 's|@PREFIX@|$(PREFIX)|' $< > $@
85 |
86 | $(PARSER): $(SRC_DIR)/grammar.json
87 | $(TS) generate --no-bindings $^
88 |
89 | install: all
90 | install -d '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)'
91 | install -m644 bindings/c/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h
92 | install -m644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
93 | install -m644 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a
94 | install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER)
95 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR)
96 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT)
97 |
98 | uninstall:
99 | $(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \
100 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \
101 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \
102 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \
103 | '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \
104 | '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
105 |
106 | clean:
107 | $(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT)
108 |
109 | test:
110 | $(TS) test
111 |
112 | .PHONY: all install uninstall clean test
113 |
--------------------------------------------------------------------------------