├── .env.example ├── www ├── .prettierignore ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── lib │ │ ├── types.ts │ │ ├── cm-highlight-extension.ts │ │ └── just-syntax-highlighting.ts │ ├── components │ │ ├── ui │ │ │ ├── label.tsx │ │ │ ├── separator.tsx │ │ │ ├── switch.tsx │ │ │ ├── badge.tsx │ │ │ ├── card.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── resizable.tsx │ │ │ └── button.tsx │ │ └── tree-node.tsx │ ├── hooks │ │ └── use-persisted-state.tsx │ └── providers │ │ └── editor-settings-provider.tsx ├── public │ ├── tree-sitter.wasm │ ├── tree-sitter-just.wasm │ └── icon.svg ├── tsconfig.node.json ├── .gitignore ├── .prettierrc ├── index.html ├── vite.config.ts ├── .eslintrc.cjs ├── components.json ├── tsconfig.json └── package.json ├── .gitattributes ├── vendor ├── tree-sitter-just-src └── tree-sitter-just │ ├── .envrc │ ├── bindings │ ├── python │ │ └── tree_sitter_just │ │ │ ├── py.typed │ │ │ ├── __init__.pyi │ │ │ ├── __init__.py │ │ │ └── binding.c │ ├── go │ │ ├── go.mod │ │ ├── binding.go │ │ └── binding_test.go │ ├── node │ │ ├── index.js │ │ ├── index.d.ts │ │ └── binding.cc │ ├── c │ │ ├── tree-sitter-just.pc.in │ │ └── tree-sitter-just.h │ ├── swift │ │ └── TreeSitterJust │ │ │ └── just.h │ ├── rust │ │ ├── build.rs │ │ └── lib.rs │ └── debug.c │ ├── .prettierignore │ ├── .npmignore │ ├── .git-blame-ignore-revs │ ├── ftdetect │ └── just.vim │ ├── queries-src │ ├── folds.scm │ ├── indents.scm │ ├── textobjects.scm │ ├── locals.scm │ └── highlights.scm │ ├── test │ ├── timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e │ ├── crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion │ ├── highlight │ │ ├── injections-global-pwsh.just │ │ ├── injections-global-py.just │ │ ├── multiline.just │ │ ├── invalid-syntax.just │ │ ├── injections.just │ │ └── recipes.just │ ├── corpus │ │ ├── injections.txt │ │ └── multiline.txt │ └── issue69-segfault.just │ ├── queries │ └── just │ │ ├── folds.scm │ │ ├── indents.scm │ │ ├── textobjects.scm │ │ ├── locals.scm │ │ ├── highlights.scm │ │ └── injections.scm │ ├── queries-flavored │ ├── zed │ │ ├── folds.scm │ │ ├── indents.scm │ │ ├── textobjects.scm │ │ ├── locals.scm │ │ ├── highlights.scm │ │ └── injections.scm │ ├── helix │ │ ├── folds.scm │ │ ├── indents.scm │ │ ├── textobjects.scm │ │ ├── locals.scm │ │ ├── highlights.scm │ │ └── injections.scm │ └── lapce │ │ ├── folds.scm │ │ ├── indents.scm │ │ ├── textobjects.scm │ │ ├── locals.scm │ │ ├── highlights.scm │ │ └── injections.scm │ ├── .clang-tidy │ ├── fuzzer │ ├── entry.c │ └── build-corpus.py │ ├── .gitattributes │ ├── Cargo.toml │ ├── .gitignore │ ├── binding.gyp │ ├── .editorconfig │ ├── pyproject.toml │ ├── eslint.config.mjs │ ├── flake.nix │ ├── src │ └── tree_sitter │ │ └── alloc.h │ ├── tree-sitter.json │ ├── lua │ └── tree-sitter-just │ │ └── init.lua │ ├── .github │ └── workflows │ │ ├── fuzz.yaml │ │ └── ci.yaml │ ├── package.json │ ├── Package.swift │ ├── flake.lock │ ├── Cargo.lock │ ├── GRAMMAR.md │ └── Makefile ├── .gitignore ├── screenshot.png ├── clippy.toml ├── rustfmt.toml ├── src ├── text_node.rs ├── alias.rs ├── dependency.rs ├── function_call.rs ├── variable.rs ├── attribute.rs ├── arguments.rs ├── count.rs ├── rule │ ├── undefined_identifiers.rs │ ├── duplicate_alias.rs │ ├── unknown_setting.rs │ ├── duplicate_setting.rs │ ├── missing_recipe_for_alias.rs │ ├── unknown_attribute.rs │ ├── unknown_function.rs │ ├── missing_dependencies.rs │ ├── attribute_invalid_target.rs │ ├── script_shebang_conflict.rs │ ├── invalid_setting_kind.rs │ ├── duplicate_variables.rs │ ├── unused_variables.rs │ ├── working_directory_conflict.rs │ ├── parallel_dependencies.rs │ ├── attribute_target_support.rs │ ├── function_arguments.rs │ ├── duplicate_recipes.rs │ ├── attribute_arguments.rs │ ├── dependency_arguments.rs │ ├── recipe_parameters.rs │ ├── alias_recipe_conflict.rs │ ├── recipe_dependency_cycles.rs │ └── mixed_indentation.rs ├── subcommand.rs ├── group.rs ├── command.rs ├── attribute_target.rs ├── diagnostic.rs ├── position_ext.rs ├── str_ext.rs ├── point_ext.rs ├── rule.rs ├── main.rs └── subcommand │ └── analyze.rs ├── .editorconfig ├── crates ├── just-lsp-wasm │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── just-lsp-changelog │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── main.rs ├── CONTRIBUTING ├── bin ├── forbid ├── start └── package ├── queries ├── locals.scm ├── highlights.scm └── injections.scm ├── .github └── workflows │ ├── web.yaml │ └── ci.yaml ├── Cargo.toml └── justfile /.env.example: -------------------------------------------------------------------------------- 1 | RUST_LOG= 2 | -------------------------------------------------------------------------------- /www/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | vendor/** linguist-vendored 2 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just-src: -------------------------------------------------------------------------------- 1 | tree-sitter-just/src -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /.env 3 | /notes 4 | target/ 5 | -------------------------------------------------------------------------------- /www/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/python/tree_sitter_just/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/just-lsp/HEAD/screenshot.png -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | repositories/ 4 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /examples 3 | /build 4 | /script 5 | /target 6 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/python/tree_sitter_just/__init__.pyi: -------------------------------------------------------------------------------- 1 | def language() -> int: ... 2 | -------------------------------------------------------------------------------- /www/public/tree-sitter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/just-lsp/HEAD/www/public/tree-sitter.wasm -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 1337 2 | source-item-ordering = ['enum', 'struct', 'trait', 'impl'] 3 | -------------------------------------------------------------------------------- /www/public/tree-sitter-just.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/just-lsp/HEAD/www/public/tree-sitter-just.wasm -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Reformatting with prettier 2 | 2cf6e8a21f247adcfd1b54e0043183057880cdee 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | max_width = 80 3 | newline_style = "Unix" 4 | tab_spaces = 2 5 | use_field_init_shorthand = true 6 | use_try_shorthand = true 7 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/ftdetect/just.vim: -------------------------------------------------------------------------------- 1 | au VimEnter,BufWinEnter,BufRead,BufNewFile {.,}justfile\c,*.just setlocal filetype=just | setlocal commentstring=#\ %s 2 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/python/tree_sitter_just/__init__.py: -------------------------------------------------------------------------------- 1 | "Just grammar for tree-sitter" 2 | 3 | from ._binding import language 4 | 5 | __all__ = ["language"] 6 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-src/folds.scm: -------------------------------------------------------------------------------- 1 | ; Define collapse points 2 | 3 | ([ 4 | (recipe) 5 | (string) 6 | (external_command) 7 | ] @fold 8 | (#trim! @fold)) 9 | -------------------------------------------------------------------------------- /src/text_node.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 4 | pub(crate) struct TextNode { 5 | pub(crate) range: lsp::Range, 6 | pub(crate) value: String, 7 | } 8 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tree-sitter/tree-sitter-just 2 | 3 | go 1.22 4 | 5 | require github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /crates/just-lsp-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "just-lsp-wasm" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /src/alias.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub(crate) struct Alias { 5 | pub(crate) name: TextNode, 6 | pub(crate) range: lsp::Range, 7 | pub(crate) value: TextNode, 8 | } 9 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/just-lsp/HEAD/vendor/tree-sitter-just/test/timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terror/just-lsp/HEAD/vendor/tree-sitter-just/test/crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion -------------------------------------------------------------------------------- /crates/just-lsp-changelog/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 = "changelog-generator" 7 | version = "0.0.0" 8 | -------------------------------------------------------------------------------- /src/dependency.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub(crate) struct Dependency { 5 | pub(crate) arguments: Vec, 6 | pub(crate) name: String, 7 | pub(crate) range: lsp::Range, 8 | } 9 | -------------------------------------------------------------------------------- /src/function_call.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub(crate) struct FunctionCall { 5 | pub(crate) arguments: Vec, 6 | pub(crate) name: TextNode, 7 | pub(crate) range: lsp::Range, 8 | } 9 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/highlight/injections-global-pwsh.just: -------------------------------------------------------------------------------- 1 | set shell := ["pwsh", "-c"] 2 | set dotenv-filename := ".env-local" 3 | set dotenv-load 4 | 5 | recipe: 6 | Write-Host "Hello, world!" 7 | Get-ChildItem -Path C:\ 8 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/highlight/injections-global-py.just: -------------------------------------------------------------------------------- 1 | set shell := ["python3", "-c"] 2 | set dotenv-filename := ".env-local" 3 | set dotenv-load 4 | 5 | recipe: 6 | if True: print(100) 7 | if "foo" != None: print("foo") 8 | -------------------------------------------------------------------------------- /crates/just-lsp-changelog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "just-lsp-changelog" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [lints] 8 | workspace = true 9 | 10 | [dependencies] 11 | regex = "1.12.2" 12 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Unless you explicitly state otherwise, any contribution intentionally 5 | submitted for inclusion in the work by you shall be licensed as in 6 | LICENSE, without any additional terms or conditions. 7 | -------------------------------------------------------------------------------- /bin/forbid: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | which rg 6 | 7 | ! rg \ 8 | --color always \ 9 | --ignore-case \ 10 | --glob !bin/forbid \ 11 | --glob !vendor \ 12 | --glob !www/bun.lock \ 13 | 'dbg!|fixme|todo|xxx' 14 | -------------------------------------------------------------------------------- /src/variable.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub(crate) struct Variable { 5 | pub(crate) content: String, 6 | pub(crate) export: bool, 7 | pub(crate) name: TextNode, 8 | pub(crate) range: lsp::Range, 9 | } 10 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries/just/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/zed/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: "clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type" 2 | WarningsAsErrors: true 3 | HeaderFilterRegex: "" 4 | FormatStyle: google 5 | ExtraArgsBefore: ["--std=c11"] 6 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/helix/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/lapce/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/node/index.js: -------------------------------------------------------------------------------- 1 | const root = require("path").join(__dirname, "..", ".."); 2 | 3 | module.exports = require("node-gyp-build")(root); 4 | 5 | try { 6 | module.exports.nodeTypeInfo = require("../../src/node-types.json"); 7 | } catch (_) {} 8 | -------------------------------------------------------------------------------- /www/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/attribute.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone, Default, PartialEq)] 4 | pub(crate) struct Attribute { 5 | pub(crate) arguments: Vec, 6 | pub(crate) name: TextNode, 7 | pub(crate) range: lsp::Range, 8 | pub(crate) target: Option, 9 | } 10 | -------------------------------------------------------------------------------- /crates/just-lsp-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[must_use] 2 | pub fn add(left: u64, right: u64) -> u64 { 3 | left + right 4 | } 5 | 6 | #[cfg(test)] 7 | mod tests { 8 | use super::*; 9 | 10 | #[test] 11 | fn it_works() { 12 | let result = add(2, 2); 13 | assert_eq!(result, 4); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries/just/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | 6 | [ 7 | (recipe) 8 | (string) 9 | (external_command) 10 | ] @indent.begin 11 | 12 | (comment) @indent.auto 13 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/c/tree-sitter-just.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@PREFIX@ 2 | libdir=@LIBDIR@ 3 | includedir=@INCLUDEDIR@ 4 | 5 | Name: tree-sitter-just 6 | Description: Just grammar for tree-sitter 7 | URL: @URL@ 8 | Version: @VERSION@ 9 | Requires: @REQUIRES@ 10 | Libs: -L${libdir} @ADDITIONAL_LIBS@ -ltree-sitter-just 11 | Cflags: -I${includedir} 12 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/c/tree-sitter-just.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_JUST_H_ 2 | #define TREE_SITTER_JUST_H_ 3 | 4 | typedef struct TSLanguage TSLanguage; 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | const TSLanguage *tree_sitter_just(void); 11 | 12 | #ifdef __cplusplus 13 | } 14 | #endif 15 | 16 | #endif // TREE_SITTER_JUST_H_ 17 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/highlight/multiline.just: -------------------------------------------------------------------------------- 1 | # Wrapped expression 2 | a := "foo" + \ 3 | "bar" 4 | # ^^^^^ string 5 | 6 | # Smooshed recipes 7 | foo: 8 | echo foo {{ a }} 9 | bar: 10 | echo bar {{ os() }} 11 | # ^^ function.call 12 | 13 | # Wrapped dependencies 14 | baz: foo \ 15 | bar 16 | # ^^^ function.call 17 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/swift/TreeSitterJust/just.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_JUST_H_ 2 | #define TREE_SITTER_JUST_H_ 3 | 4 | typedef struct TSLanguage TSLanguage; 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | const TSLanguage *tree_sitter_just(void); 11 | 12 | #ifdef __cplusplus 13 | } 14 | #endif 15 | 16 | #endif // TREE_SITTER_JUST_H_ 17 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/zed/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/helix/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/lapce/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /www/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/go/binding.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_just 2 | 3 | // #cgo CFLAGS: -std=c11 -fPIC 4 | // #include "../../src/parser.c" 5 | // // NOTE: if your language has an external scanner, add it here. 6 | import "C" 7 | 8 | import "unsafe" 9 | 10 | // Get the tree-sitter Language for this grammar. 11 | func Language() unsafe.Pointer { 12 | return unsafe.Pointer(C.tree_sitter_just()) 13 | } 14 | -------------------------------------------------------------------------------- /www/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "importOrder": ["^[./]"], 3 | "importOrderSeparation": true, 4 | "importOrderSortSpecifiers": true, 5 | "jsxSingleQuote": true, 6 | "plugins": [ 7 | "@trivago/prettier-plugin-sort-imports", 8 | "prettier-plugin-tailwindcss" 9 | ], 10 | "proseWrap": "always", 11 | "semi": true, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "es5" 15 | } 16 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | just 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-src/indents.scm: -------------------------------------------------------------------------------- 1 | ; This query specifies how to auto-indent logical blocks. 2 | ; 3 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html SKIP-NVIM SKIP-NVIM-NEXT 4 | 5 | [ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @indent @extend 10 | 11 | (comment) @indent.auto ; SKIP-HELIX SKIP-ZED SKIP-LAPCE 12 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/go/binding_test.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_just_test 2 | 3 | import ( 4 | "testing" 5 | 6 | tree_sitter "github.com/smacker/go-tree-sitter" 7 | "github.com/tree-sitter/tree-sitter-just" 8 | ) 9 | 10 | func TestCanLoadGrammar(t *testing.T) { 11 | language := tree_sitter.NewLanguage(tree_sitter_just.Language()) 12 | if language == nil { 13 | t.Errorf("Error loading Just grammar") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clap)] 4 | #[clap(author, version, about, long_about = None)] 5 | pub(crate) struct Arguments { 6 | #[clap(subcommand)] 7 | subcommand: Option, 8 | } 9 | 10 | impl Arguments { 11 | pub(crate) async fn run(self) -> Result { 12 | match self.subcommand { 13 | Some(subcommand) => subcommand.run(), 14 | None => Server::run().await, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /www/vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import { defineConfig } from 'vite'; 5 | 6 | export default defineConfig({ 7 | base: '/just-lsp', 8 | css: { devSourcemap: true }, 9 | plugins: [react(), tailwindcss()], 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(__dirname, './src'), 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/highlight/invalid-syntax.just: -------------------------------------------------------------------------------- 1 | # Test that numbers are parsed as errors but do not mess up the syntax 2 | 3 | a := 100 4 | # <- variable 5 | # ^^ operator 6 | # ^^^ error 7 | 8 | foo: 9 | # <- function 10 | # ^ operator 11 | 12 | b := "a" + 1.234 13 | # <- variable 14 | # ^^ operator 15 | # ^^^ string 16 | # ^ operator 17 | # ^^^^ error 18 | 19 | bar: 20 | # <- function 21 | # ^ operator 22 | -------------------------------------------------------------------------------- /www/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /www/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import App from './App.tsx'; 5 | import './index.css'; 6 | import { EditorSettingsProvider } from './providers/editor-settings-provider.tsx'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /www/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Position = { 2 | start: number; 3 | end: number; 4 | }; 5 | 6 | export interface SyntaxNode { 7 | type: string; 8 | text: string; 9 | startPosition: { row: number; column: number }; 10 | endPosition: { row: number; column: number }; 11 | childCount: number; 12 | children: SyntaxNode[]; 13 | } 14 | 15 | export interface TreeNode { 16 | text: string; 17 | node: SyntaxNode; 18 | level: number; 19 | type: string; 20 | } 21 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-src/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; Specify how to navigate around logical blocks in code 2 | 3 | (recipe 4 | (recipe_body) @function.inside) @function.around 5 | 6 | (parameters 7 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 8 | 9 | (dependency_expression 10 | (_) @parameter.inside) @parameter.around 11 | 12 | (function_call 13 | arguments: (sequence 14 | (expression) @parameter.inside) @parameter.around) @function.around 15 | 16 | (comment) @comment.around 17 | -------------------------------------------------------------------------------- /www/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries/just/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inner) @function.outer 7 | 8 | (parameters 9 | ((_) @parameter.inner . ","? @parameter.outer)) @parameter.outer 10 | 11 | (dependency_expression 12 | (_) @parameter.inner) @parameter.outer 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inner) @parameter.outer) @function.outer 17 | 18 | (comment) @comment.outer 19 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/helix/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/lapce/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/zed/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /bin/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Used for testing out the language server using Neovim 4 | # 5 | # local configs = require('lspconfig.configs') 6 | # 7 | # if not configs.just_lsp then 8 | # configs.just_lsp = { 9 | # default_config = { 10 | # cmd = { '/.../just-lsp/bin/start' }, 11 | # filetypes = { 'just' }, 12 | # root_dir = function(name) 13 | # return lsp.util.find_git_ancestor(name) 14 | # end, 15 | # settings = {}, 16 | # }, 17 | # on_attach = on_attach 18 | # } 19 | # end 20 | # 21 | # lsp.just_lsp.setup({}) 22 | 23 | cd ~/src/just-lsp && cargo run 24 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/node/index.d.ts: -------------------------------------------------------------------------------- 1 | type BaseNode = { 2 | type: string; 3 | named: boolean; 4 | }; 5 | 6 | type ChildNode = { 7 | multiple: boolean; 8 | required: boolean; 9 | types: BaseNode[]; 10 | }; 11 | 12 | type NodeInfo = 13 | | (BaseNode & { 14 | subtypes: BaseNode[]; 15 | }) 16 | | (BaseNode & { 17 | fields: { [name: string]: ChildNode }; 18 | children: ChildNode[]; 19 | }); 20 | 21 | type Language = { 22 | name: string; 23 | language: unknown; 24 | nodeTypeInfo: NodeInfo[]; 25 | }; 26 | 27 | declare const language: Language; 28 | export = language; 29 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/fuzzer/entry.c: -------------------------------------------------------------------------------- 1 | #include "tree_sitter/api.h" 2 | #include 3 | #include 4 | 5 | TSLanguage *tree_sitter_just(void); 6 | 7 | int LLVMFuzzerTestOneInput(const uint8_t *data, const size_t len) { 8 | TSParser *parser = ts_parser_new(); 9 | ts_parser_set_language(parser, tree_sitter_just()); 10 | 11 | // Build a syntax tree based on source code stored in a string. 12 | TSTree *tree = ts_parser_parse_string(parser, NULL, (const char *)data, len); 13 | // Free all of the heap-allocated memory. 14 | ts_tree_delete(tree); 15 | ts_parser_delete(parser); 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /src/count.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub(crate) struct Count(pub(crate) T, pub(crate) usize); 4 | 5 | impl Display for Count { 6 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 7 | if self.1 == 1 { 8 | write!(f, "{}", self.0) 9 | } else { 10 | write!(f, "{}s", self.0) 11 | } 12 | } 13 | } 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | #[test] 20 | fn count() { 21 | assert_eq!(Count("dog", 0).to_string(), "dogs"); 22 | assert_eq!(Count("dog", 1).to_string(), "dog"); 23 | assert_eq!(Count("dog", 2).to_string(), "dogs"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.gitattributes: -------------------------------------------------------------------------------- 1 | /src/** linguist-vendored 2 | /examples/* linguist-vendored 3 | 4 | src/grammar.json linguist-generated 5 | src/node-types.json linguist-generated 6 | src/parser.c linguist-generated 7 | src/tree_sitter/alloc.h linguist-generated 8 | src/tree_sitter/array.h linguist-generated 9 | src/tree_sitter/parser.h linguist-generated 10 | 11 | src/grammar.json -diff 12 | src/node-types.json -diff 13 | src/parser.c -diff 14 | src/tree_sitter/alloc.h -diff 15 | src/tree_sitter/array.h -diff 16 | src/tree_sitter/parser.h -diff 17 | 18 | # These test files are from the fuzzer 19 | test/**timeout-* binary 20 | test/**crash-* binary 21 | -------------------------------------------------------------------------------- /src/rule/undefined_identifiers.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Reports expressions that reference variables or parameters which are not 5 | /// defined and aren't builtins. 6 | UndefinedIdentifierRule { 7 | id: "undefined-identifiers", 8 | message: "undefined identifier", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | for unresolved in context.unresolved_identifiers() { 13 | diagnostics.push(Diagnostic::error( 14 | format!("Variable `{}` not found", unresolved.name), 15 | unresolved.range, 16 | )); 17 | } 18 | 19 | diagnostics 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tree-sitter-just" 3 | description = "just grammar for the tree-sitter parsing library" 4 | version = "0.1.0" 5 | keywords = ["incremental", "parsing", "just"] 6 | categories = ["parsing", "text-editors"] 7 | repository = "https://github.com/IndianBoy42/tree-sitter-just" 8 | edition = "2021" 9 | license = "MIT" 10 | autoexamples = false 11 | 12 | build = "bindings/rust/build.rs" 13 | include = ["bindings/rust/*", "grammar.js", "queries-flavored/helix/*", "src/*"] 14 | 15 | [lib] 16 | path = "bindings/rust/lib.rs" 17 | 18 | [dependencies] 19 | tree-sitter = "~0.24.4" 20 | 21 | [build-dependencies] 22 | cc = "1.2.1" 23 | -------------------------------------------------------------------------------- /src/rule/duplicate_alias.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Flags alias declarations that reuse the same name multiple times. 5 | DuplicateAliasRule { 6 | id: "duplicate-alias", 7 | message: "duplicate alias", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | let mut seen = HashSet::new(); 12 | 13 | for alias in context.aliases() { 14 | if !seen.insert(alias.name.value.clone()) { 15 | diagnostics.push(Diagnostic::error( 16 | format!("Duplicate alias `{}`", alias.name.value), 17 | alias.range, 18 | )); 19 | } 20 | } 21 | 22 | diagnostics 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/node/binding.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct TSLanguage TSLanguage; 4 | 5 | extern "C" TSLanguage *tree_sitter_just(); 6 | 7 | // "tree-sitter", "language" hashed with BLAKE2 8 | const napi_type_tag LANGUAGE_TYPE_TAG = { 9 | 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16 10 | }; 11 | 12 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 13 | exports["name"] = Napi::String::New(env, "just"); 14 | auto language = Napi::External::New(env, tree_sitter_just()); 15 | language.TypeTag(&LANGUAGE_TYPE_TAG); 16 | exports["language"] = language; 17 | return exports; 18 | } 19 | 20 | NODE_API_MODULE(tree_sitter_just_binding, Init) 21 | -------------------------------------------------------------------------------- /src/rule/unknown_setting.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Emits diagnostics for `set` directives targeting settings that don't exist 5 | /// in the builtin catalog. 6 | UnknownSettingRule { 7 | id: "unknown-setting", 8 | message: "unknown setting", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | for setting in context.settings() { 13 | if context.builtin_setting(&setting.name).is_none() { 14 | diagnostics.push(Diagnostic::error( 15 | format!("Unknown setting `{}`", setting.name), 16 | setting.range, 17 | )); 18 | } 19 | } 20 | 21 | diagnostics 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/rule/duplicate_setting.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Emits diagnostics when the same `set` option is declared more than once. 5 | DuplicateSettingRule { 6 | id: "duplicate-setting", 7 | message: "duplicate setting", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | let mut seen = HashSet::new(); 12 | 13 | for setting in context.settings() { 14 | if !seen.insert(setting.name.clone()) { 15 | diagnostics.push(Diagnostic::error( 16 | format!("Duplicate setting `{}`", setting.name), 17 | setting.range, 18 | )); 19 | } 20 | } 21 | 22 | diagnostics 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build 2 | target/** 3 | 4 | # C/C++ language server cache 5 | .ccls-cache/** 6 | 7 | # Node cache 8 | node_modules 9 | 10 | # Tree-sitter ouptut 11 | build/ 12 | .build/ 13 | log.html 14 | *.wasm 15 | *.dylib 16 | fuzzer-out/ 17 | 18 | # Fuzzer items 19 | tree-sitter-src/ 20 | fuzzer/failures/ 21 | fuzzer/corpus 22 | fuzzer/artifacts 23 | **.log 24 | **.out 25 | **.dSYM 26 | 27 | # Windows things 28 | *.exp 29 | *.lib 30 | *.obj 31 | 32 | # Other repos 33 | repositories/ 34 | 35 | # Editor files 36 | compile_commands.json 37 | .vscode/ 38 | *.swp 39 | 40 | # No need to support ancient python 41 | setup.py 42 | 43 | # Configuring includes for autocompletion in C files 44 | .clangd 45 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "baseUrl": ".", 5 | "isolatedModules": true, 6 | "jsx": "react-jsx", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "paths": { "@/*": ["./src/*"] }, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2020", 19 | "useDefineForClassFields": true 20 | }, 21 | "include": ["src"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /www/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import * as React from 'react'; 6 | 7 | function Label({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | ); 21 | } 22 | 23 | export { Label }; 24 | -------------------------------------------------------------------------------- /src/rule/missing_recipe_for_alias.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Flags aliases that point to recipes which aren't defined. 5 | MissingRecipeForAliasRule { 6 | id: "missing-recipe-for-alias", 7 | message: "alias target not found", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | let recipe_names = context.recipe_names(); 12 | 13 | for alias in context.aliases() { 14 | if !recipe_names.contains(&alias.value.value) { 15 | diagnostics.push(Diagnostic::error( 16 | format!("Recipe `{}` not found", alias.value.value), 17 | alias.value.range, 18 | )); 19 | } 20 | } 21 | 22 | diagnostics 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rule/unknown_attribute.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Warns when an attribute name isn't part of the known builtin attribute set. 5 | UnknownAttributeRule { 6 | id: "unknown-attribute", 7 | message: "unknown attribute", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | for attribute in context.attributes() { 12 | let attribute_name = &attribute.name.value; 13 | 14 | if context.builtin_attributes(attribute_name).is_empty() { 15 | diagnostics.push(Diagnostic::error( 16 | format!("Unknown attribute `{attribute_name}`"), 17 | attribute.name.range, 18 | )); 19 | } 20 | } 21 | 22 | diagnostics 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "tree_sitter_just_binding", 5 | "dependencies": [ 6 | " Result { 12 | let mut current_dir = env::current_dir()?; 13 | 14 | loop { 15 | let candidate = current_dir.join("justfile"); 16 | 17 | if candidate.exists() { 18 | return Ok(candidate); 19 | } 20 | 21 | if !current_dir.pop() { 22 | bail!( 23 | "could not find `justfile` in current directory or any parent directory" 24 | ); 25 | } 26 | } 27 | } 28 | 29 | pub(crate) fn run(self) -> Result { 30 | match self { 31 | Self::Analyze(analyze) => analyze.run(), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/group.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 2 | pub(crate) enum Group { 3 | Any, 4 | Linux, 5 | Macos, 6 | Openbsd, 7 | Windows, 8 | } 9 | 10 | impl Group { 11 | #[must_use] 12 | pub(crate) fn conflicts_with(self, other: Group) -> bool { 13 | matches!((self, other), (Group::Any, _) | (_, Group::Any)) || self == other 14 | } 15 | 16 | #[must_use] 17 | pub(crate) fn targets(attribute: &str) -> Option> { 18 | match attribute { 19 | "windows" => Some(vec![Group::Windows]), 20 | "linux" => Some(vec![Group::Linux]), 21 | "macos" => Some(vec![Group::Macos]), 22 | "openbsd" => Some(vec![Group::Openbsd]), 23 | "unix" => Some(vec![Group::Linux, Group::Macos, Group::Openbsd]), 24 | _ => None, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq)] 4 | pub(crate) enum Command { 5 | RunRecipe, 6 | } 7 | 8 | impl Command { 9 | pub(crate) fn all() -> Vec { 10 | vec![Command::RunRecipe.to_string()] 11 | } 12 | } 13 | 14 | impl Display for Command { 15 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 16 | write!( 17 | f, 18 | "{}", 19 | match self { 20 | Command::RunRecipe => "just-lsp.run_recipe", 21 | } 22 | ) 23 | } 24 | } 25 | 26 | impl TryFrom<&str> for Command { 27 | type Error = Error; 28 | 29 | fn try_from(value: &str) -> Result { 30 | match value { 31 | "just-lsp.run_recipe" => Ok(Command::RunRecipe), 32 | _ => Err(anyhow!("Unknown command: {value}")), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/rust/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let src_dir = std::path::Path::new("src"); 3 | 4 | let mut c_config = cc::Build::new(); 5 | c_config.include(src_dir); 6 | c_config 7 | .flag_if_supported("-Wno-unused-parameter") 8 | .flag_if_supported("-Wno-unused-but-set-variable") 9 | .flag_if_supported("-Wno-trigraphs"); 10 | 11 | #[cfg(target_env = "msvc")] 12 | c_config.flag("-utf-8"); 13 | 14 | let parser_path = src_dir.join("parser.c"); 15 | println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); 16 | c_config.file(&parser_path); 17 | 18 | let scanner_path = src_dir.join("scanner.c"); 19 | println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); 20 | c_config.file(&scanner_path); 21 | 22 | c_config.compile("parser"); 23 | } 24 | -------------------------------------------------------------------------------- /src/rule/missing_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Reports recipe dependencies that reference recipes which don't exist in the 5 | /// current document. 6 | MissingDependencyRule { 7 | id: "missing-dependencies", 8 | message: "missing dependency", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | let recipe_names = context.recipe_names(); 13 | 14 | for recipe in context.recipes() { 15 | for dependency in &recipe.dependencies { 16 | if !recipe_names.contains(&dependency.name) { 17 | diagnostics.push(Diagnostic::error( 18 | format!("Recipe `{}` not found", dependency.name), 19 | dependency.range, 20 | )); 21 | } 22 | } 23 | } 24 | 25 | diagnostics 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /www/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | import * as React from 'react'; 6 | 7 | function Separator({ 8 | className, 9 | orientation = 'horizontal', 10 | decorative = true, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 24 | ); 25 | } 26 | 27 | export { Separator }; 28 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tree-sitter-just" 7 | description = "Just grammar for tree-sitter" 8 | version = "0.0.1" 9 | keywords = ["incremental", "parsing", "tree-sitter", "just"] 10 | classifiers = [ 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Topic :: Software Development :: Compilers", 14 | "Topic :: Text Processing :: Linguistic", 15 | "Typing :: Typed" 16 | ] 17 | requires-python = ">=3.8" 18 | license.text = "MIT" 19 | readme = "README.md" 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/tree-sitter/tree-sitter-just" 23 | 24 | [project.optional-dependencies] 25 | core = ["tree-sitter~=0.21"] 26 | 27 | [tool.cibuildwheel] 28 | build = "cp38-*" 29 | build-frontend = "build" 30 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/python/tree_sitter_just/binding.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct TSLanguage TSLanguage; 4 | 5 | TSLanguage *tree_sitter_just(void); 6 | 7 | static PyObject *_binding_language(PyObject *self, PyObject *args) { 8 | return PyLong_FromVoidPtr(tree_sitter_just()); 9 | } 10 | 11 | static PyMethodDef methods[] = { 12 | {"language", _binding_language, METH_NOARGS, 13 | "Get the tree-sitter language for this grammar."}, 14 | {NULL, NULL, 0, NULL}}; 15 | 16 | static struct PyModuleDef module = {.m_base = PyModuleDef_HEAD_INIT, 17 | .m_name = "_binding", 18 | .m_doc = NULL, 19 | .m_size = -1, 20 | .m_methods = methods}; 21 | 22 | PyMODINIT_FUNC PyInit__binding(void) { return PyModule_Create(&module); } 23 | -------------------------------------------------------------------------------- /src/rule/attribute_invalid_target.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Ensures attributes only appear on syntax nodes that actually accept 5 | /// attributes. 6 | AttributeInvalidTargetRule { 7 | id: "attribute-invalid-target", 8 | message: "invalid attribute target", 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 | if context.builtin_attributes(attribute_name).is_empty() { 16 | continue; 17 | } 18 | 19 | if attribute.target.is_none() { 20 | diagnostics.push(Diagnostic::error( 21 | format!("Attribute `{attribute_name}` applied to invalid target",), 22 | attribute.range, 23 | )); 24 | } 25 | } 26 | 27 | diagnostics 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/rule/script_shebang_conflict.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Reports recipes that combine a shebang line with the `[script]` attribute. 5 | ScriptShebangConflictRule { 6 | id: "script-shebang-conflict", 7 | message: "shebang conflict", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | for recipe in context.recipes() { 12 | let Some(script_attribute) = recipe.find_attribute("script") else { 13 | continue; 14 | }; 15 | 16 | if recipe.shebang.is_none() { 17 | continue; 18 | } 19 | 20 | diagnostics.push(Diagnostic::error( 21 | format!( 22 | "Recipe `{}` has both shebang line and `[script]` attribute", 23 | recipe.name.value 24 | ), 25 | script_attribute.range, 26 | )); 27 | } 28 | 29 | diagnostics 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import google from "eslint-config-google"; 3 | import prettier from "eslint-config-prettier"; 4 | 5 | export default [ 6 | { 7 | ignores: ["repositories/", "target/"], 8 | }, 9 | prettier, 10 | { 11 | ...google, 12 | languageOptions: { 13 | globals: { 14 | ...globals.commonjs, 15 | ...globals.es2021, 16 | }, 17 | 18 | ecmaVersion: "latest", 19 | sourceType: "module", 20 | }, 21 | 22 | rules: { 23 | indent: [ 24 | "error", 25 | 2, 26 | { 27 | SwitchCase: 1, 28 | }, 29 | ], 30 | 31 | "max-len": [ 32 | "error", 33 | { 34 | code: 120, 35 | ignoreComments: true, 36 | ignoreUrls: true, 37 | ignoreStrings: true, 38 | }, 39 | ], 40 | }, 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /src/rule/invalid_setting_kind.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Ensures each `set` statement uses the correct value type (boolean, string, 5 | /// or array) for the targeted builtin setting. 6 | InvalidSettingKindRule { 7 | id: "invalid-setting-kind", 8 | message: "invalid setting kind", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | for setting in context.settings() { 13 | let Some(Builtin::Setting { kind, .. }) = 14 | context.builtin_setting(&setting.name) 15 | else { 16 | continue; 17 | }; 18 | 19 | if setting.kind == *kind { 20 | continue; 21 | } 22 | 23 | diagnostics.push(Diagnostic::error( 24 | format!("Setting `{}` expects a {kind} value", setting.name,), 25 | setting.range, 26 | )); 27 | } 28 | 29 | diagnostics 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { nixpkgs, flake-utils, ... }: 9 | flake-utils.lib.eachDefaultSystem (system: 10 | let 11 | pkgs = import nixpkgs { inherit system; }; 12 | in 13 | { 14 | devShells.default = with pkgs; 15 | let 16 | darwinInclude = lib.optionalString stdenv.isDarwin ''- "-I${darwin.Libsystem}/include/"''; 17 | in 18 | mkShell { 19 | packages = [ tree-sitter nodejs-slim_22 graphviz ]; 20 | shellHook = '' 21 | cat < .clangd 22 | CompileFlags: 23 | Add: 24 | - "-I${clang}/resource-root/include/" 25 | ${darwinInclude} 26 | EOF 27 | ''; 28 | }; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/attribute_target.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | pub(crate) enum AttributeTarget { 5 | Alias, 6 | Assignment, 7 | Module, 8 | Recipe, 9 | } 10 | 11 | impl Display for AttributeTarget { 12 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 13 | write!( 14 | f, 15 | "{}", 16 | match self { 17 | AttributeTarget::Alias => "alias", 18 | AttributeTarget::Assignment => "assignment", 19 | AttributeTarget::Module => "module", 20 | AttributeTarget::Recipe => "recipe", 21 | } 22 | ) 23 | } 24 | } 25 | 26 | impl AttributeTarget { 27 | #[must_use] 28 | pub(crate) fn try_from_kind(kind: &str) -> Option { 29 | match kind { 30 | "alias" => Some(Self::Alias), 31 | "assignment" | "export" => Some(Self::Assignment), 32 | "module" => Some(Self::Module), 33 | "recipe" => Some(Self::Recipe), 34 | _ => None, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rule/duplicate_variables.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Emits diagnostics when variable assignments reuse the same name without 5 | /// explicitly opting into overriding via `allow-duplicate-variables`. 6 | DuplicateVariableRule { 7 | id: "duplicate-variable", 8 | message: "duplicate variable", 9 | run(context) { 10 | let allow_duplicates = context.setting_enabled("allow-duplicate-variables"); 11 | 12 | if allow_duplicates { 13 | return Vec::new(); 14 | } 15 | 16 | let mut diagnostics = Vec::new(); 17 | let mut seen = HashSet::new(); 18 | 19 | for variable in context.variables() { 20 | if !seen.insert(variable.name.value.clone()) { 21 | diagnostics.push(Diagnostic::error( 22 | format!("Duplicate variable `{}`", variable.name.value), 23 | variable.range, 24 | )); 25 | } 26 | } 27 | 28 | diagnostics 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /queries/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition.var) 14 | 15 | (assignment 16 | left: (identifier) @local.definition.var) 17 | 18 | (module 19 | name: (identifier) @local.definition.namespace) 20 | 21 | (parameter 22 | name: (identifier) @local.definition.var) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition.function) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/helix/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/lapce/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-flavored/zed/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries-src/locals.scm: -------------------------------------------------------------------------------- 1 | ; This file tells us about the scope of variables so e.g. local 2 | ; variables override global functions with the same name 3 | 4 | ; Scope 5 | 6 | (recipe) @local.scope 7 | 8 | ; Definitions 9 | 10 | (alias 11 | left: (identifier) @local.definition.variable) 12 | 13 | (assignment 14 | left: (identifier) @local.definition.variable) 15 | 16 | (module 17 | name: (identifier) @local.definition.namespace) 18 | 19 | (parameter 20 | name: (identifier) @local.definition.variable) 21 | 22 | (recipe_header 23 | name: (identifier) @local.definition.function) 24 | 25 | ; References 26 | 27 | (alias 28 | right: (identifier) @local.reference.variable) 29 | 30 | (function_call 31 | name: (identifier) @local.reference.function) 32 | 33 | (dependency 34 | name: (identifier) @local.reference.function) 35 | 36 | (dependency_expression 37 | name: (identifier) @local.reference.function) 38 | 39 | (value 40 | (identifier) @local.reference.variable) 41 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/queries/just/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition.var) 14 | 15 | (assignment 16 | left: (identifier) @local.definition.var) 17 | 18 | (module 19 | name: (identifier) @local.definition.namespace) 20 | 21 | (parameter 22 | name: (identifier) @local.definition.var) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition.function) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /src/rule/unused_variables.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Finds non-exported global variables that are never referenced anywhere in 5 | /// the document. 6 | UnusedVariableRule { 7 | id: "unused-variables", 8 | message: "unused variable", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | if context.tree().is_none() { 13 | return diagnostics; 14 | } 15 | 16 | for (variable_name, is_used) in context.variable_usage() { 17 | if *is_used { 18 | continue; 19 | } 20 | 21 | let Some(variable) = context.document().find_variable(variable_name) 22 | else { 23 | continue; 24 | }; 25 | 26 | if variable.export { 27 | continue; 28 | } 29 | 30 | diagnostics.push(Diagnostic::warning( 31 | format!("Variable `{variable_name}` appears unused"), 32 | variable.name.range, 33 | )); 34 | } 35 | 36 | diagnostics 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/rule/working_directory_conflict.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Detects conflicts between working-directory and no-cd directives. 5 | WorkingDirectoryConflictRule { 6 | id: "working-directory-conflict", 7 | message: "conflicting directory attributes", 8 | run(context) { 9 | let mut diagnostics = Vec::new(); 10 | 11 | for recipe in context.recipes() { 12 | let working_directory_attribute = 13 | recipe.find_attribute("working-directory"); 14 | 15 | let no_cd_attribute = recipe.find_attribute("no-cd"); 16 | 17 | if let (Some(attribute), Some(_)) = 18 | (working_directory_attribute, no_cd_attribute) 19 | { 20 | diagnostics.push(Diagnostic::error( 21 | format!( 22 | "Recipe `{}` can't combine `[working-directory]` with `[no-cd]`", 23 | recipe.name.value 24 | ), 25 | attribute.range, 26 | )); 27 | } 28 | } 29 | 30 | diagnostics 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/rule/parallel_dependencies.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Warn when `[parallel]` is applied to a recipe that lacks enough 5 | /// dependencies for the attribute to have any effect. 6 | ParallelDependenciesRule { 7 | id: "parallel-dependencies", 8 | message: "unnecessary parallel attribute", 9 | run(context) { 10 | context 11 | .recipes() 12 | .iter() 13 | .filter_map(|recipe| { 14 | let attribute = recipe.find_attribute("parallel")?; 15 | 16 | let message = match recipe.dependencies.len() { 17 | 0 => format!( 18 | "Recipe `{}` has no dependencies, so `[parallel]` has no effect", 19 | recipe.name.value 20 | ), 21 | 1 => format!( 22 | "Recipe `{}` has only one dependency, so `[parallel]` has no effect", 23 | recipe.name.value 24 | ), 25 | _ => return None, 26 | }; 27 | 28 | Some(Diagnostic::warning(message, attribute.range)) 29 | }) 30 | .collect() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /www/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as SwitchPrimitive from '@radix-ui/react-switch'; 3 | import * as React from 'react'; 4 | 5 | function Switch({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) { 9 | return ( 10 | 18 | 24 | 25 | ); 26 | } 27 | 28 | export { Switch }; 29 | -------------------------------------------------------------------------------- /crates/just-lsp-changelog/src/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | core::str, 3 | regex::{Captures, Regex}, 4 | std::{fs, process::Command}, 5 | }; 6 | 7 | const REPO: &str = "terror/just-lsp"; 8 | 9 | fn author(pr: u64) -> String { 10 | eprintln!("#{pr}"); 11 | 12 | let output = Command::new("sh") 13 | .args([ 14 | "-c", 15 | &format!("gh pr view {pr} --json author | jq -r .author.login"), 16 | ]) 17 | .output() 18 | .unwrap(); 19 | 20 | assert!( 21 | output.status.success(), 22 | "{}", 23 | String::from_utf8_lossy(&output.stderr) 24 | ); 25 | 26 | str::from_utf8(&output.stdout).unwrap().trim().to_owned() 27 | } 28 | 29 | fn main() { 30 | fs::write( 31 | "CHANGELOG.md", 32 | &*Regex::new(r"\(#(\d+)( by @[a-z]+)?\)") 33 | .unwrap() 34 | .replace_all( 35 | &fs::read_to_string("CHANGELOG.md").unwrap(), 36 | |captures: &Captures| { 37 | let pr = captures[1].parse().unwrap(); 38 | 39 | let contributor = author(pr); 40 | 41 | format!("([#{pr}](https://github.com/{REPO}/pull/{pr}) by [{contributor}](https://github.com/{contributor}))") 42 | }, 43 | ), 44 | ) 45 | .unwrap(); 46 | } 47 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/src/tree_sitter/alloc.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_ALLOC_H_ 2 | #define TREE_SITTER_ALLOC_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // Allow clients to override allocation functions 13 | #ifdef TREE_SITTER_REUSE_ALLOCATOR 14 | 15 | extern void *(*ts_current_malloc)(size_t size); 16 | extern void *(*ts_current_calloc)(size_t count, size_t size); 17 | extern void *(*ts_current_realloc)(void *ptr, size_t size); 18 | extern void (*ts_current_free)(void *ptr); 19 | 20 | #ifndef ts_malloc 21 | #define ts_malloc ts_current_malloc 22 | #endif 23 | #ifndef ts_calloc 24 | #define ts_calloc ts_current_calloc 25 | #endif 26 | #ifndef ts_realloc 27 | #define ts_realloc ts_current_realloc 28 | #endif 29 | #ifndef ts_free 30 | #define ts_free ts_current_free 31 | #endif 32 | 33 | #else 34 | 35 | #ifndef ts_malloc 36 | #define ts_malloc malloc 37 | #endif 38 | #ifndef ts_calloc 39 | #define ts_calloc calloc 40 | #endif 41 | #ifndef ts_realloc 42 | #define ts_realloc realloc 43 | #endif 44 | #ifndef ts_free 45 | #define ts_free free 46 | #endif 47 | 48 | #endif 49 | 50 | #ifdef __cplusplus 51 | } 52 | #endif 53 | 54 | #endif // TREE_SITTER_ALLOC_H_ 55 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/fuzzer/build-corpus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Turn our tests into a corpus for the fuzzer (one test per file)""" 3 | 4 | from pathlib import Path 5 | import re 6 | 7 | RE = r"===+\n(?P.*?)\n===+\n(?P.*?)\n---+" 8 | REPO = Path(__file__).parent.parent 9 | OUT_DIR = REPO / "fuzzer" / "corpus" 10 | 11 | 12 | def main(): 13 | ts_corpus_files = (REPO / "test" / "corpus").glob("*.txt") 14 | 15 | corpus = {} 16 | 17 | for fname in ts_corpus_files: 18 | text = fname.read_text() 19 | prefix = fname.name.rstrip(".txt") 20 | 21 | for match in re.finditer(RE, text, re.MULTILINE | re.DOTALL): 22 | name = match.group("name").replace(" ", "_") 23 | name = f"{prefix}_{name}.just" 24 | source = match.group("source") 25 | corpus[name] = source 26 | 27 | OUT_DIR.mkdir(exist_ok=True) 28 | 29 | # Clear the corpus of all files we created 30 | for existing in OUT_DIR.iterdir(): 31 | if existing.name.endswith(".just"): 32 | existing.unlink() 33 | 34 | for name, source in corpus.items(): 35 | out_file: Path = OUT_DIR / name 36 | out_file.write_text(source) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/tree-sitter.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammars": [ 3 | { 4 | "name": "just", 5 | "camelcase": "Just", 6 | "scope": "source.just", 7 | "path": ".", 8 | "file-types": [ 9 | "just", 10 | ".just", 11 | "justfile", 12 | "JUSTFILE", 13 | "Justfile", 14 | ".justfile", 15 | ".JUSTFILE", 16 | ".Justfile" 17 | ], 18 | "highlights": "queries-src/highlights.scm", 19 | "injections": "queries-src/injections.scm", 20 | "locals": "queries-src/locals.scm", 21 | "injection-regex": "^(?i)just(file)?$", 22 | "first-line-regex": "#!\\S*bin\\S*[/ ]just" 23 | } 24 | ], 25 | "metadata": { 26 | "version": "0.1.0", 27 | "license": "MIT", 28 | "description": "Justfiles grammar for tree-sitter", 29 | "authors": [ 30 | { 31 | "name": "Anshuman Medhi", 32 | "email": "amedhi@connect.ust.uk" 33 | }, 34 | { 35 | "name": "Trevor Gross", 36 | "email": "tmgross@umich.edu" 37 | } 38 | ], 39 | "links": { 40 | "repository": "https://github.com/IndianBoy42/tree-sitter-just" 41 | } 42 | }, 43 | "bindings": { 44 | "c": true, 45 | "go": true, 46 | "node": true, 47 | "python": true, 48 | "rust": true, 49 | "swift": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/lua/tree-sitter-just/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local is_windows 4 | if jit ~= nil then 5 | is_windows = jit.os == "Windows" 6 | else 7 | is_windows = package.config:sub(1, 1) == "\\" 8 | end 9 | local get_separator = function() 10 | if is_windows then 11 | return "\\" 12 | end 13 | return "/" 14 | end 15 | local join_paths = function(...) 16 | local separator = get_separator() 17 | return table.concat({ ... }, separator) 18 | end 19 | 20 | function M.setup(arg) 21 | local parser_config = require("nvim-treesitter.parsers").get_parser_configs() 22 | parser_config.just = { 23 | install_info = { 24 | url = arg["local"] and join_paths( 25 | vim.fn.stdpath("data"), 26 | "site", 27 | "pack", 28 | "packer", 29 | "start", 30 | "tree-sitter-just" 31 | ) or "https://github.com/IndianBoy42/tree-sitter-just", -- local path or git repo 32 | files = { "src/parser.c", "src/scanner.c" }, 33 | branch = "main", 34 | }, 35 | maintainers = { "@IndianBoy42" }, 36 | } 37 | local ok, ft = pcall(require, "filetype") 38 | if ok then 39 | ft.setup({ 40 | overrides = { 41 | extensions = { 42 | just = "just", 43 | }, 44 | literals = { 45 | Justfile = "just", 46 | justfile = "just", 47 | [".Justfile"] = "just", 48 | [".justfile"] = "just", 49 | }, 50 | }, 51 | }) 52 | end 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /.github/workflows/web.yaml: -------------------------------------------------------------------------------- 1 | name: Web 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: 'pages' 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Bun 26 | uses: oven-sh/setup-bun@v1 27 | with: 28 | bun-version: latest 29 | 30 | - name: Setup Pages 31 | uses: actions/configure-pages@v4 32 | 33 | - name: Install dependencies 34 | working-directory: ./www 35 | run: bun install 36 | 37 | - name: Test 38 | working-directory: ./www 39 | run: bun test 40 | 41 | - name: Build 42 | working-directory: ./www 43 | run: bun run build 44 | 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: './www/dist' 49 | 50 | deploy: 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | 55 | runs-on: ubuntu-latest 56 | 57 | needs: build 58 | 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /bin/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | VERSION=${REF#"refs/tags/"} 6 | DIST=$(pwd)/dist 7 | 8 | echo "Packaging just-lsp $VERSION for $TARGET..." 9 | 10 | test -f Cargo.lock || cargo generate-lockfile 11 | 12 | echo "Installing rust toolchain for $TARGET..." 13 | rustup target add "$TARGET" 14 | 15 | if [[ $TARGET == aarch64-unknown-linux-musl ]]; then 16 | export CC=aarch64-linux-gnu-gcc 17 | fi 18 | 19 | echo "Building just-lsp..." 20 | 21 | RUSTFLAGS="--deny warnings --codegen target-feature=+crt-static $TARGET_RUSTFLAGS" \ 22 | cargo build --bin just-lsp --target "$TARGET" --release 23 | 24 | EXECUTABLE=target/$TARGET/release/just-lsp 25 | 26 | if [[ $OS == windows-latest ]]; then 27 | EXECUTABLE=$EXECUTABLE.exe 28 | fi 29 | 30 | echo "Copying release files..." 31 | 32 | mkdir dist 33 | 34 | cp -r \ 35 | "$EXECUTABLE" \ 36 | Cargo.lock \ 37 | Cargo.toml \ 38 | LICENSE \ 39 | README.md \ 40 | "$DIST" 41 | 42 | cd "$DIST" 43 | 44 | echo "Creating release archive..." 45 | 46 | case $OS in 47 | ubuntu-latest | macos-latest) 48 | ARCHIVE=just-lsp-$VERSION-$TARGET.tar.gz 49 | tar czf "$ARCHIVE" ./* 50 | echo "archive=$DIST/$ARCHIVE" >> "$GITHUB_OUTPUT" 51 | ;; 52 | windows-latest) 53 | ARCHIVE=just-lsp-$VERSION-$TARGET.zip 54 | 7z a "$ARCHIVE" ./* 55 | echo "archive=$(pwd -W)/$ARCHIVE" >> "$GITHUB_OUTPUT" 56 | ;; 57 | esac 58 | -------------------------------------------------------------------------------- /src/rule/attribute_target_support.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Validates that each attribute is attached to a supported target kind 5 | /// (recipe, module, alias, etc.) according to the builtin metadata. 6 | AttributeTargetSupportRule { 7 | id: "attribute-target-support", 8 | message: "unsupported attribute target", 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 Some(target_type) = attribute.target else { 22 | continue; 23 | }; 24 | 25 | let is_valid_target = matching.iter().copied().any(|attr| { 26 | if let Builtin::Attribute { targets, .. } = attr { 27 | targets.contains(&target_type) 28 | } else { 29 | false 30 | } 31 | }); 32 | 33 | if !is_valid_target { 34 | diagnostics.push(Diagnostic::error( 35 | format!( 36 | "Attribute `{attribute_name}` cannot be applied to {target_type} target", 37 | ), 38 | attribute.range, 39 | )); 40 | } 41 | } 42 | 43 | diagnostics 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/highlight/injections.just: -------------------------------------------------------------------------------- 1 | # Injection highlighting shows up as none during tests 2 | 3 | localhost := `if [ -f "foo" ]; then; echo hi; fi` 4 | # <- variable 5 | # ^^ operator 6 | # ^ punctuation.special 7 | # ^ punctuation.special 8 | 9 | # interpolation within injection 10 | foo := `[ {{ dir(localhost) }} = "hi" ] && "bye"` 11 | # <- variable 12 | # ^^ operator 13 | # ^ punctuation.special 14 | # ^^ punctuation.bracket 15 | # ^^^ function.call 16 | # ^ punctuation.bracket 17 | # ^^^^^^^^^ variable 18 | # ^ punctuation.bracket 19 | # ^^ punctuation.bracket 20 | # ^ punctuation.special 21 | 22 | stuff := ``` 23 | echo foo 24 | echo bar 25 | ``` 26 | # <- punctuation.special 27 | 28 | # These have to be verified manually, no automated tests are available 29 | 30 | foo: 31 | echo "hello {{ foo }}" 32 | if [ -f "foo" ]; then; \ 33 | echo hi; \ 34 | fi 35 | 36 | py_shebang: 37 | #!/usr/bin/env python 38 | if foo is not None: 39 | print("bar") 40 | 41 | perl_shebang: 42 | #!/usr/bin/env perl 43 | my $iter = $dir->iterator; 44 | while (my $file = $iter->()) { 45 | 46 | # See if it is a directory and skip 47 | next if $file->is_dir(); 48 | 49 | # Print out the file name and path 50 | print "$file\n"; 51 | } 52 | -------------------------------------------------------------------------------- /www/src/lib/cm-highlight-extension.ts: -------------------------------------------------------------------------------- 1 | import { StateEffect } from '@codemirror/state'; 2 | import { 3 | Decoration, 4 | DecorationSet, 5 | EditorView, 6 | ViewPlugin, 7 | ViewUpdate, 8 | } from '@codemirror/view'; 9 | 10 | const highlightMark = Decoration.mark({ class: 'cm-highlighted-node' }); 11 | 12 | export const addHighlightEffect = StateEffect.define<{ 13 | from: number; 14 | to: number; 15 | }>(); 16 | 17 | export const removeHighlightEffect = StateEffect.define(); 18 | 19 | export const highlightExtension = ViewPlugin.fromClass( 20 | class { 21 | decorations: DecorationSet; 22 | 23 | constructor() { 24 | this.decorations = Decoration.none; 25 | } 26 | 27 | update(update: ViewUpdate) { 28 | const effects = update.transactions 29 | .flatMap((tr) => tr.effects) 30 | .filter((e) => e.is(addHighlightEffect) || e.is(removeHighlightEffect)); 31 | 32 | if (!effects.length) return; 33 | 34 | for (const effect of effects) { 35 | if (effect.is(addHighlightEffect)) { 36 | const { from, to } = effect.value; 37 | this.decorations = Decoration.set([highlightMark.range(from, to)]); 38 | } else if (effect.is(removeHighlightEffect)) { 39 | this.decorations = Decoration.none; 40 | } 41 | } 42 | } 43 | }, 44 | { 45 | provide: (plugin) => 46 | EditorView.outerDecorations.of( 47 | (view) => view.plugin(plugin)?.decorations ?? Decoration.none 48 | ), 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /www/src/hooks/use-persisted-state.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | 3 | export function usePersistedState( 4 | key: string, 5 | initialValue: T, 6 | options?: { 7 | serialize?: (value: T) => string; 8 | deserialize?: (value: string) => T; 9 | } 10 | ): [T, (action: Partial | ((prevState: T) => Partial)) => void] { 11 | const [state, setFullState] = useState(() => { 12 | const savedValue = localStorage.getItem(key); 13 | 14 | if (savedValue !== null) { 15 | try { 16 | return options?.deserialize 17 | ? options.deserialize(savedValue) 18 | : JSON.parse(savedValue); 19 | } catch (error) { 20 | console.warn(`Error reading ${key} from localStorage:`, error); 21 | return initialValue; 22 | } 23 | } 24 | 25 | return initialValue; 26 | }); 27 | 28 | useEffect(() => { 29 | try { 30 | localStorage.setItem( 31 | key, 32 | options?.serialize ? options.serialize(state) : JSON.stringify(state) 33 | ); 34 | } catch (error) { 35 | console.warn(`Error saving ${key} to localStorage:`, error); 36 | } 37 | }, [key, state, options]); 38 | 39 | const setState = useCallback( 40 | (action: Partial | ((prevState: T) => Partial)) => { 41 | setFullState((prevState) => ({ 42 | ...prevState, 43 | ...(typeof action === 'function' ? action(prevState) : action), 44 | })); 45 | }, 46 | [] 47 | ); 48 | 49 | return [state, setState]; 50 | } 51 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/corpus/injections.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | global shebang 3 | ================================================================================ 4 | #!/usr/bin/env just 5 | 6 | -------------------------------------------------------------------------------- 7 | 8 | (source_file 9 | (shebang 10 | (language))) 11 | 12 | ================================================================================ 13 | set shell basic 14 | ================================================================================ 15 | 16 | set shell := ["fooshell"] 17 | 18 | -------------------------------------------------------------------------------- 19 | 20 | (source_file 21 | (setting 22 | (string))) 23 | 24 | ================================================================================ 25 | set shell with args 26 | ================================================================================ 27 | 28 | set shell := ['nu', '-m', 'light', '-c'] 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | (source_file 33 | (setting 34 | (string) 35 | (string) 36 | (string) 37 | (string))) 38 | 39 | ================================================================================ 40 | set shell still parse empty 41 | ================================================================================ 42 | 43 | set shell := [] 44 | 45 | -------------------------------------------------------------------------------- 46 | 47 | (source_file 48 | (setting)) 49 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/.github/workflows/fuzz.yaml: -------------------------------------------------------------------------------- 1 | name: Fuzz Parser 2 | 3 | on: 4 | push: 5 | paths: 6 | - src/scanner.c 7 | - .github/workflows/fuzz.yaml 8 | - fuzzer/ 9 | pull_request: 10 | paths: 11 | - src/scanner.c 12 | - .github/workflows/fuzz.yaml 13 | - fuzzer/ 14 | workflow_dispatch: 15 | 16 | env: 17 | RUST_BACKTRACE: 1 18 | CI: 1 19 | 20 | jobs: 21 | test: 22 | name: Parser fuzzing 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 25 25 | # Run in a container because we had some issues reproducing failures 26 | container: 27 | image: node:lts-slim 28 | env: 29 | # Just 10 minutes as a sanity check, should run longer locally. 30 | # For whatever reason, this time is always wayyyy exceeded 31 | # (hence the longer workflow timeout) 32 | FUZZ_TOTAL_TIME: 600 33 | steps: 34 | - uses: actions/checkout@v4 35 | - run: | 36 | if [ -f /.dockerenv ]; then 37 | echo "Running in docker" 38 | else 39 | echo "Not in a docker container!" 40 | exit 1 41 | fi 42 | 43 | apt-get update 44 | apt-get install -y clang curl make g++ git 45 | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | 46 | bash -s -- --to /usr/bin 47 | - run: just setup 48 | - run: just fuzz 49 | - name: Print failures 50 | if: always() 51 | run: ls fuzzer/failures/* | 52 | xargs -IFNAME sh -c 'echo "\nContents of FNAME:" && base64 -i FNAME' 53 | -------------------------------------------------------------------------------- /src/rule/function_arguments.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Verifies builtin function calls use a valid argument count and respect 5 | /// variadic constraints. 6 | FunctionArgumentsRule { 7 | id: "function-arguments", 8 | message: "invalid function arguments", 9 | run(context) { 10 | let mut diagnostics = Vec::new(); 11 | 12 | for function_call in context.function_calls() { 13 | let function_name = &function_call.name.value; 14 | 15 | if let Some(Builtin::Function { 16 | required_args, 17 | accepts_variadic, 18 | .. 19 | }) = context.builtin_function(function_name.as_str()) 20 | { 21 | let arg_count = function_call.arguments.len(); 22 | 23 | if arg_count < *required_args { 24 | diagnostics.push(Diagnostic::error( 25 | format!( 26 | "Function `{function_name}` requires at least {required_args} {}, but {arg_count} provided", 27 | Count("argument", *required_args) 28 | ), 29 | function_call.range, 30 | )); 31 | } else if !accepts_variadic && arg_count > *required_args { 32 | diagnostics.push(Diagnostic::error( 33 | format!( 34 | "Function `{function_name}` accepts {required_args} {}, but {arg_count} provided", 35 | Count("argument", *required_args) 36 | ), 37 | function_call.range, 38 | )); 39 | } 40 | } 41 | } 42 | 43 | diagnostics 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rule/duplicate_recipes.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | define_rule! { 4 | /// Detects recipes that have the same name and overlapping OS constraints, 5 | /// which would shadow each other at runtime unless overrides are enabled. 6 | DuplicateRecipeRule { 7 | id: "duplicate-recipes", 8 | message: "duplicate recipes", 9 | run(context) { 10 | let allow_duplicates = context.setting_enabled("allow-duplicate-recipes"); 11 | 12 | if allow_duplicates { 13 | return Vec::new(); 14 | } 15 | 16 | let mut diagnostics = Vec::new(); 17 | 18 | let mut recipe_groups: HashMap)>> = 19 | HashMap::new(); 20 | 21 | for recipe in context.recipes() { 22 | recipe_groups 23 | .entry(recipe.name.value.clone()) 24 | .or_default() 25 | .push((recipe.range, recipe.groups())); 26 | } 27 | 28 | for (recipe_name, group) in &recipe_groups { 29 | if group.len() <= 1 { 30 | continue; 31 | } 32 | 33 | for (i, (range, a)) in group.iter().enumerate() { 34 | for (_, (_, b)) in group.iter().enumerate().take(i) { 35 | let has_conflict = 36 | a.iter().any(|a| b.iter().any(|b| a.conflicts_with(*b))); 37 | 38 | if has_conflict { 39 | diagnostics.push(Diagnostic::error( 40 | format!("Duplicate recipe name `{recipe_name}`"), 41 | *range, 42 | )); 43 | 44 | break; 45 | } 46 | } 47 | } 48 | } 49 | 50 | diagnostics 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/diagnostic.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct Diagnostic { 5 | /// A short header summarizing the diagnostic. 6 | pub(crate) display: String, 7 | /// A unique identifier for the diagnostic. 8 | pub(crate) id: String, 9 | /// A detailed message describing the diagnostic. 10 | pub(crate) message: String, 11 | /// The range in the source code where the diagnostic applies. 12 | pub(crate) range: lsp::Range, 13 | /// The severity level of the diagnostic. 14 | pub(crate) severity: lsp::DiagnosticSeverity, 15 | } 16 | 17 | impl From for lsp::Diagnostic { 18 | fn from(value: Diagnostic) -> lsp::Diagnostic { 19 | lsp::Diagnostic { 20 | code: Some(lsp::NumberOrString::String(value.id)), 21 | message: value.message, 22 | range: value.range, 23 | severity: Some(value.severity), 24 | source: Some("just-lsp".to_string()), 25 | ..Default::default() 26 | } 27 | } 28 | } 29 | 30 | impl Diagnostic { 31 | pub(crate) fn error(message: impl Into, range: lsp::Range) -> Self { 32 | Self::new(message, range, lsp::DiagnosticSeverity::ERROR) 33 | } 34 | 35 | pub(crate) fn new( 36 | message: impl Into, 37 | range: lsp::Range, 38 | severity: lsp::DiagnosticSeverity, 39 | ) -> Self { 40 | Self { 41 | display: String::new(), 42 | id: String::new(), 43 | message: message.into(), 44 | range, 45 | severity, 46 | } 47 | } 48 | 49 | pub(crate) fn warning(message: impl Into, range: lsp::Range) -> Self { 50 | Self::new(message, range, lsp::DiagnosticSeverity::WARNING) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-sitter-just", 3 | "author": "Anshuman Medhi ", 4 | "contributors": [ 5 | "Trevor Gross ", 6 | "Amaan Qureshi " 7 | ], 8 | "license": "MIT", 9 | "version": "0.1.0", 10 | "description": "Justfiles grammar for tree-sitter", 11 | "main": "bindings/node", 12 | "types": "bindings/node", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/IndianBoy42/tree-sitter-just" 16 | }, 17 | "keywords": [ 18 | "tree-sitter", 19 | "justfiles", 20 | "parser", 21 | "lexer" 22 | ], 23 | "files": [ 24 | "grammar.js", 25 | "binding.gyp", 26 | "prebuilds/**", 27 | "bindings/node/*", 28 | "queries/*", 29 | "src/**" 30 | ], 31 | "dependencies": { 32 | "tree-sitter": "^0.22.1", 33 | "prettier": "^3.3.3", 34 | "node-addon-api": "^8.2.2", 35 | "node-gyp-build": "^4.8.3" 36 | }, 37 | "peerDependencies": { 38 | "tree-sitter": "^0.22.1" 39 | }, 40 | "peerDependenciesMeta": { 41 | "tree_sitter": { 42 | "optional": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "eslint": ">=9.15.0", 47 | "eslint-config-google": "^0.14.0", 48 | "eslint-config-prettier": "^9.1.0", 49 | "tree-sitter-cli": "^0.24.4", 50 | "prebuildify": "^6.0.1" 51 | }, 52 | "scripts": { 53 | "format:check": "prettier --check .", 54 | "format:write": "prettier --write .", 55 | "lint:check": "eslint .", 56 | "lint:fix": "eslint --fix .", 57 | "install": "node-gyp-build", 58 | "prebuildify": "prebuildify --napi --strip" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "TreeSitterJust", 6 | products: [ 7 | .library(name: "TreeSitterJust", targets: ["TreeSitterJust"]), 8 | ], 9 | dependencies: [], 10 | targets: [ 11 | .target(name: "TreeSitterJust", 12 | path: ".", 13 | exclude: [ 14 | "Cargo.toml", 15 | "Makefile", 16 | "binding.gyp", 17 | "bindings/c", 18 | "bindings/go", 19 | "bindings/node", 20 | "bindings/python", 21 | "bindings/rust", 22 | "prebuilds", 23 | "grammar.js", 24 | "package.json", 25 | "package-lock.json", 26 | "pyproject.toml", 27 | "setup.py", 28 | "test", 29 | "examples", 30 | ".editorconfig", 31 | ".github", 32 | ".gitignore", 33 | ".gitattributes", 34 | ".gitmodules", 35 | ], 36 | sources: [ 37 | "src/parser.c", 38 | // NOTE: if your language has an external scanner, add it here. 39 | ], 40 | resources: [ 41 | .copy("queries") 42 | ], 43 | publicHeadersPath: "bindings/swift", 44 | cSettings: [.headerSearchPath("src")]) 45 | ], 46 | cLanguageStandard: .c11 47 | ) 48 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/test/issue69-segfault.just: -------------------------------------------------------------------------------- 1 | alias b := build 2 | alias c := check 3 | alias dr := dry-run 4 | alias sw := switch 5 | alias t := test 6 | alias u := update 7 | alias ui := update-input 8 | 9 | rebuildArgs := "--verbose" 10 | rebuild := if os() == "macos" { "darwin-rebuild" } else { "nixos-rebuild" } 11 | 12 | default: 13 | @just --choose 14 | 15 | [private] 16 | rebuild subcmd: 17 | {{ rebuild }} {{ subcmd }} {{ rebuildArgs }} --flake . 18 | 19 | build: (rebuild "build") 20 | 21 | dry-run: (rebuild "dry-run") 22 | 23 | switch: (rebuild "switch") 24 | 25 | test: (rebuild "test") 26 | 27 | ci: 28 | nix run \ 29 | --inputs-from . \ 30 | --override-input nixpkgs nixpkgs \ 31 | github:Mic92/nix-fast-build -- \ 32 | --no-nom \ 33 | --skip-cached \ 34 | --option accept-flake-config true \ 35 | --option allow-import-from-derivation false \ 36 | --flake '.#hydraJobs' 37 | 38 | check: 39 | nix flake check \ 40 | --print-build-logs \ 41 | --show-trace \ 42 | --accept-flake-config 43 | 44 | update: 45 | nix flake update 46 | 47 | update-input input: 48 | nix flake lock \ 49 | --update-input {{ input }} \ 50 | --commit-lock-file \ 51 | --commit-lockfile-summary "flake: update {{ input }}" 52 | 53 | deploy system: 54 | nix run \ 55 | --inputs-from . \ 56 | 'nixpkgs#deploy-rs' -- \ 57 | -s '.#{{ system }}' 58 | 59 | deploy-all: 60 | nix run \ 61 | --inputs-from . \ 62 | 'nixpkgs#deploy-rs' -- -s 63 | 64 | a: 65 | 66 | clean: 67 | rm -rf \ 68 | result* \ 69 | repl-result-out* \ 70 | config.tf.json \ 71 | .terraform* 72 | -------------------------------------------------------------------------------- /src/position_ext.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub(crate) trait PositionExt { 4 | fn point(&self, document: &Document) -> Point; 5 | } 6 | 7 | impl PositionExt for lsp::Position { 8 | /// LSP positions use a zero-based line index for `line` and a UTF-16 9 | /// code-unit offset within that line for `character`. 10 | /// 11 | /// Ropey and Tree-sitter, however, operate on UTF-8 byte offsets. To bridge 12 | /// this mismatch, we take the line number directly as the Tree-sitter `row`, 13 | /// then look up the corresponding line in the Rope and convert the UTF-16 14 | /// `character` offset into a char index and, from there, into a UTF-8 byte 15 | /// offset for the `column`. 16 | /// 17 | /// The resulting `(row, column)` byte position is then used to locate the 18 | /// node in the syntax tree. 19 | fn point(&self, document: &Document) -> Point { 20 | let row = 21 | usize::try_from(self.line).expect("line index exceeds usize::MAX"); 22 | 23 | let line = document.content.line(row); 24 | 25 | let utf16_cu = usize::try_from(self.character) 26 | .expect("character index exceeds usize::MAX"); 27 | 28 | Point { 29 | row, 30 | column: line.char_to_byte(line.utf16_cu_to_char(utf16_cu)), 31 | } 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use {super::*, pretty_assertions::assert_eq}; 38 | 39 | #[test] 40 | fn converts_utf16_offsets_to_utf8_columns() { 41 | let document = Document::from("a🧪b"); 42 | 43 | let position = lsp::Position { 44 | line: 0, 45 | character: 3, 46 | }; 47 | 48 | let point = position.point(&document); 49 | 50 | assert_eq!(point, Point { row: 0, column: 5 }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /www/src/providers/editor-settings-provider.tsx: -------------------------------------------------------------------------------- 1 | import { usePersistedState } from '@/hooks/use-persisted-state'; 2 | import { ReactNode, createContext, useContext } from 'react'; 3 | 4 | export interface EditorSettings { 5 | fontSize: number; 6 | keybindings: 'default' | 'vim'; 7 | lineNumbers: boolean; 8 | lineWrapping: boolean; 9 | tabSize: number; 10 | } 11 | 12 | const defaultSettings: EditorSettings = { 13 | fontSize: 14, 14 | keybindings: 'default', 15 | lineNumbers: true, 16 | lineWrapping: true, 17 | tabSize: 2, 18 | }; 19 | 20 | type EditorSettingsContextType = { 21 | settings: EditorSettings; 22 | updateSettings: (settings: Partial) => void; 23 | }; 24 | 25 | const EditorSettingsContext = createContext< 26 | EditorSettingsContextType | undefined 27 | >(undefined); 28 | 29 | export const useEditorSettings = () => { 30 | const context = useContext(EditorSettingsContext); 31 | 32 | if (context === undefined) { 33 | throw new Error( 34 | 'useEditorSettings must be used within an EditorSettingsProvider' 35 | ); 36 | } 37 | 38 | return context; 39 | }; 40 | 41 | export const EditorSettingsProvider = ({ 42 | children, 43 | }: { 44 | children: ReactNode; 45 | }) => { 46 | const [settings, setSettings] = usePersistedState( 47 | 'editor-settings', 48 | defaultSettings 49 | ); 50 | 51 | const updateSettings = (newSettings: Partial) => { 52 | setSettings((prevSettings) => ({ ...prevSettings, ...newSettings })); 53 | }; 54 | 55 | return ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1719468428, 24 | "narHash": "sha256-vN5xJAZ4UGREEglh3lfbbkIj+MPEYMuqewMn4atZFaQ=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "1e3deb3d8a86a870d925760db1a5adecc64d329d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /www/src/components/ui/badge.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 badgeVariants = cva( 7 | 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', 13 | secondary: 14 | 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', 15 | destructive: 16 | 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', 17 | outline: 18 | 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: 'default', 23 | }, 24 | } 25 | ); 26 | 27 | function Badge({ 28 | className, 29 | variant, 30 | asChild = false, 31 | ...props 32 | }: React.ComponentProps<'span'> & 33 | VariantProps & { asChild?: boolean }) { 34 | const Comp = asChild ? Slot : 'span'; 35 | 36 | return ( 37 | 42 | ); 43 | } 44 | 45 | export { Badge, badgeVariants }; 46 | -------------------------------------------------------------------------------- /vendor/tree-sitter-just/bindings/debug.c: -------------------------------------------------------------------------------- 1 | // Demo that parses the first argument. Useful for attaching a debugger. 2 | 3 | #include "tree_sitter/api.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define DEFAULT_ALLOC_SIZE 1024; 10 | 11 | TSLanguage *tree_sitter_just(void); 12 | 13 | int main(int argc, char **argv) { 14 | TSParser *parser = ts_parser_new(); 15 | ts_parser_set_language(parser, tree_sitter_just()); 16 | 17 | assert(argc == 2 && "must provide a file name"); 18 | 19 | if (argc < 2) { 20 | printf("must provide one or more file names\n"); 21 | return 1; 22 | } 23 | 24 | size_t alloc_size = DEFAULT_ALLOC_SIZE; 25 | char *data = malloc(alloc_size); 26 | assert(data); 27 | 28 | for (int i = 1; i < argc; ++i) { 29 | memset(data, 0, alloc_size); 30 | 31 | FILE *fp = fopen(argv[i], "r"); 32 | if (!fp) { 33 | printf("failed to open file %s\n", argv[i]); 34 | exit(1); 35 | } 36 | 37 | fseek(fp, 0L, SEEK_END); 38 | size_t file_size = ftell(fp); 39 | rewind(fp); 40 | 41 | if (file_size > alloc_size) { 42 | data = realloc(data, file_size); 43 | assert(data); 44 | alloc_size = file_size; 45 | } 46 | 47 | size_t readlen = fread(data, sizeof(char), alloc_size, fp); 48 | assert(readlen == file_size); 49 | assert(!ferror(fp)); 50 | printf("read %zu bytes\n", readlen); 51 | 52 | // Build a syntax tree based on source code stored in a string. 53 | TSTree *tree = ts_parser_parse_string(parser, NULL, data, file_size); 54 | // TSNode root_node = ts_tree_root_node(tree); 55 | // assert(ts_node_child_count(root_node) > 0); 56 | 57 | // Free all of the heap-allocated memory. 58 | ts_tree_delete(tree); 59 | ts_parser_delete(parser); 60 | } 61 | 62 | free(data); 63 | 64 | return 0; 65 | } 66 | -------------------------------------------------------------------------------- /www/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as React from 'react'; 3 | 4 | function Card({ className, ...props }: React.ComponentProps<'div'>) { 5 | return ( 6 |
14 | ); 15 | } 16 | 17 | function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { 18 | return ( 19 |
24 | ); 25 | } 26 | 27 | function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { 28 | return ( 29 |
34 | ); 35 | } 36 | 37 | function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { 38 | return ( 39 |
44 | ); 45 | } 46 | 47 | function CardContent({ className, ...props }: React.ComponentProps<'div'>) { 48 | return ( 49 |
54 | ); 55 | } 56 | 57 | function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { 58 | return ( 59 |
64 | ); 65 | } 66 | 67 | export { 68 | Card, 69 | CardHeader, 70 | CardFooter, 71 | CardTitle, 72 | CardDescription, 73 | CardContent, 74 | }; 75 | -------------------------------------------------------------------------------- /www/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 3 | import * as React from 'react'; 4 | 5 | function ScrollArea({ 6 | className, 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 16 | 20 | {children} 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | function ScrollBar({ 29 | className, 30 | orientation = 'vertical', 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 47 | 51 | 52 | ); 53 | } 54 | 55 | export { ScrollArea, ScrollBar }; 56 | -------------------------------------------------------------------------------- /www/src/components/tree-node.tsx: -------------------------------------------------------------------------------- 1 | import type { SyntaxNode, TreeNode as TreeNodeType } from '@/lib/types'; 2 | import { ChevronDown, ChevronRight } from 'lucide-react'; 3 | 4 | interface TreeNodeProps { 5 | expandNode: (node: SyntaxNode) => void; 6 | expandedNodes: Set; 7 | hoveredNode?: SyntaxNode; 8 | item: TreeNodeType; 9 | setHoveredNode: (node?: SyntaxNode) => void; 10 | } 11 | 12 | export const TreeNode: React.FC = ({ 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 | --------------------------------------------------------------------------------