From 25608bb69428c1cc284dd0ea88ff97b34226d943 Mon Sep 17 00:00:00 2001
From: kyu08 <49891479+kyu08@users.noreply.github.com>
Date: Wed, 25 Dec 2024 22:37:41 +0900
Subject: [PATCH] feat(runner): support just (#397)
* add test data
* wip add just
* wip
* wip
* find_justfile
* wip
* wip add tree-sitter and tree-sitter-just
* get recipe names and its position
* update test_data
* add test
* update `CREDITS`
* add comment
* continue outer loop
* fix error handling
* update doc
---
CREDITS | 262 +++++++++++++++++++++++++++++++
Cargo.lock | 80 ++++++++--
Cargo.toml | 4 +
README.md | 14 +-
justfile | 26 ++++
src/model/just/just_main.rs | 299 ++++++++++++++++++++++++++++++++++++
src/model/just/mod.rs | 1 +
src/model/mod.rs | 1 +
src/model/runner.rs | 14 +-
src/model/runner_type.rs | 5 +
src/usecase/tui/app.rs | 26 ++--
test_data/just/justfile | 26 ++++
12 files changed, 727 insertions(+), 31 deletions(-)
create mode 100644 justfile
create mode 100644 src/model/just/just_main.rs
create mode 100644 src/model/just/mod.rs
create mode 100644 test_data/just/justfile
diff --git a/CREDITS b/CREDITS
index d4b73be4..1bf86f39 100644
--- a/CREDITS
+++ b/CREDITS
@@ -1462,3 +1462,265 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
+
+Stebalien/tempfile
+https://github.com/Stebalien/tempfile
+----------------------------------------------------------------
+Copyright (c) 2015 Steven Allen
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+================================================================
+
+tree-sitter/tree-sitter
+https://github.com/tree-sitter/tree-sitter
+----------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2018-2024 Max Brunsfeld
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+================================================================
+
+IndianBoy42/tree-sitter-just
+https://github.com/IndianBoy42/tree-sitter-just
+----------------------------------------------------------------
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+================================================================
diff --git a/Cargo.lock b/Cargo.lock
index f26c6e16..130547ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -145,9 +145,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.4"
+version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
+checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"shlex",
]
@@ -322,14 +322,20 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
-version = "0.3.9"
+version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
[[package]]
name = "filedescriptor"
version = "0.8.2"
@@ -484,8 +490,11 @@ dependencies = [
"simple-home-dir",
"syntect",
"syntect-tui",
+ "tempfile",
"tokio",
"toml",
+ "tree-sitter",
+ "tree-sitter-just",
"tui-textarea",
"update-informer",
"uuid",
@@ -725,9 +734,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.155"
+version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libredox"
@@ -862,9 +871,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.17.1"
+version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "onig"
@@ -1157,15 +1166,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
-version = "0.38.34"
+version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -1467,6 +1476,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+[[package]]
+name = "streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
+
[[package]]
name = "strum"
version = "0.26.3"
@@ -1577,6 +1592,19 @@ dependencies = [
"libc",
]
+[[package]]
+name = "tempfile"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -1780,6 +1808,34 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "tree-sitter"
+version = "0.24.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4"
+dependencies = [
+ "cc",
+ "regex",
+ "regex-syntax",
+ "streaming-iterator",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-just"
+version = "0.0.1"
+source = "git+https://github.com/IndianBoy42/tree-sitter-just.git?rev=f6d2930#f6d29300f9fee15dcd8c2b25ab762001d38da731"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
+[[package]]
+name = "tree-sitter-language"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c199356c799a8945965bb5f2c55b2ad9d9aa7c4b4f6e587fe9dea0bc715e5f9c"
+
[[package]]
name = "try-lock"
version = "0.2.5"
diff --git a/Cargo.toml b/Cargo.toml
index f20c022a..e31829b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,3 +31,7 @@ update-informer = { version = "1.1", default-features = true, features = ["githu
futures = "0.3"
syntect-tui = "3.0.5"
syntect = "5.2.0"
+tempfile = "3.14.0"
+tree-sitter = "=0.24.4"
+# The latest tag is 0.1.0 and it is old now. So, I am using the latest commit hash.
+tree-sitter-just = { git = "https://github.com/IndianBoy42/tree-sitter-just.git", rev = "f6d2930" }
diff --git a/README.md b/README.md
index 488454bb..ba55dfef 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-`fzf-make` is a command line tool that executes commands using fuzzy finder with preview window. Currently supporting **make**, **pnpm**, **yarn**.
+`fzf-make` is a command line tool that executes commands using fuzzy finder with preview window. Currently supporting [**make**](https://www.gnu.org/software/make/), [**pnpm**](https://github.com/pnpm/pnpm), [**yarn**](https://github.com/yarnpkg/berry), [**just**](https://github.com/casey/just).
![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square)
[![Latest Release](https://img.shields.io/github/v/release/kyu08/fzf-make?style=flat-square)](https://github.com/kyu08/fzf-make/releases/latest)
@@ -14,13 +14,14 @@
# 🛠️ Features
-- Select and execute a make target or (pnpm | yarn) scripts using fuzzy-finder with a preview window by running `fzf-make`!
+- Select and execute a make target or (pnpm | yarn) scripts or just recipe using fuzzy-finder with a preview window by running `fzf-make`!
- Execute the last executed command(By running `fzf-make --repeat`.)
- Command history
-- Support make, pnpm, yarn. **Scheduled to be developed: npm.**
+- Support [**make**](https://www.gnu.org/software/make/), [**pnpm**](https://github.com/pnpm/pnpm), [**yarn**](https://github.com/yarnpkg/berry), [**just**](https://github.com/casey/just). **Scheduled to be developed: npm.**
- [make] Support `include` directive
- [pnpm] Support workspace(collect scripts all of `package.json` in the directory where fzf-make is launched.)
- [yarn] Support workspace(collect all scripts which is defined in `workspaces` field in root `package.json`.)
+- [just] Support execution inside of directory of justfile.
- **(Scheduled to be developed)** Support config file
# 📦 Installation
@@ -67,13 +68,13 @@ cargo install --locked fzf-make
# 💡 Usage
## Run target using fuzzy finder
-1. Execute `fzf-make` in the directory you want to run make target, or (pnpm | yarn) scripts.
+1. Execute `fzf-make` in the directory you want to run make target, or (pnpm | yarn) scripts or just recipe.
1. Select command you want to execute. If you type some characters, the list will be filtered.
## Run target from history
-1. Execute `fzf-make` in the directory you want to run make target, or (pnpm | yarn) scripts.
+1. Execute `fzf-make` in the directory you want to run make target, or (pnpm | yarn) scripts or just recipe.(For just, we support execution inside of directory of justfile.)
1. Press `Tab` to move to the history pane.
1. Select command you want to execute.
@@ -88,6 +89,9 @@ Whether `package.json` and `pnpm-lock.yaml` are in the current directory.
### yarn
Whether `package.json` and `yarn.lock` are in the current directory.
+### just
+Whether `justfile` or `.justfile` are in the current directory or ancestor directories. If the lower cased file name matches `justfile` or `.justfile`, it is treat as a justfile. (e.g. `justFile` or `.JustFile` are also valid.)
+
## Commands Supported
| Command | Description |
| -------- | -------- |
diff --git a/justfile b/justfile
new file mode 100644
index 00000000..0e342d52
--- /dev/null
+++ b/justfile
@@ -0,0 +1,26 @@
+#!/usr/bin/env -S just --justfile
+
+test:
+ cargo test --all
+
+[group: 'misc']
+run:
+ echo run
+
+[group: 'misc']
+build:
+ echo build
+
+[group: 'misc']
+fmt : # https://example.com
+ echo fmt
+
+[group: 'misc']
+[private ]
+fmt-private:
+ echo fmt
+
+# everyone's favorite animate paper clip
+[group: 'check']
+clippy:
+ echo clippy
diff --git a/src/model/just/just_main.rs b/src/model/just/just_main.rs
new file mode 100644
index 00000000..a830b3b6
--- /dev/null
+++ b/src/model/just/just_main.rs
@@ -0,0 +1,299 @@
+use crate::model::{
+ command::{self, Command},
+ runner_type::RunnerType,
+};
+use anyhow::{anyhow, bail, Result};
+use std::{
+ fs::{self},
+ path::PathBuf,
+ process,
+};
+use tree_sitter::Parser;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Just {
+ path: PathBuf,
+ commands: Vec,
+}
+
+impl Just {
+ pub fn new(current_dir: PathBuf) -> Result {
+ let justfile_path = match Just::find_justfile(current_dir.clone()) {
+ Some(path) => path,
+ None => bail!("justfile not found"),
+ };
+ let source_code = fs::read_to_string(&justfile_path)?;
+
+ let commands = match Just::parse_justfile(justfile_path.clone(), source_code) {
+ Some(c) => c,
+ None => return Err(anyhow!("failed to parse justfile")),
+ };
+
+ Ok(Just {
+ path: justfile_path,
+ commands,
+ })
+ }
+
+ pub fn to_commands(&self) -> Vec {
+ self.commands.clone()
+ }
+
+ pub fn path(&self) -> PathBuf {
+ self.path.clone()
+ }
+
+ pub fn command_to_run(&self, command: &command::Command) -> Result {
+ let command = match self.get_command(command.clone()) {
+ Some(c) => c,
+ None => return Err(anyhow!("command not found")),
+ };
+
+ Ok(format!("just {}", command.args))
+ }
+
+ pub fn execute(&self, command: &command::Command) -> Result<(), anyhow::Error> {
+ let command = match self.get_command(command.clone()) {
+ Some(c) => c,
+ None => return Err(anyhow!("command not found")),
+ };
+
+ let child = process::Command::new("just")
+ .stdin(process::Stdio::inherit())
+ .arg(&command.args)
+ .spawn();
+
+ match child {
+ Ok(mut child) => match child.wait() {
+ Ok(_) => Ok(()),
+ Err(e) => Err(anyhow!("failed to run: {}", e)),
+ },
+ Err(e) => Err(anyhow!("failed to spawn: {}", e)),
+ }
+ }
+
+ fn get_command(&self, command: command::Command) -> Option {
+ self.to_commands()
+ .iter()
+ .find(|c| **c == command)
+ .map(|_| command)
+ }
+
+ fn find_justfile(current_dir: PathBuf) -> Option {
+ for path in current_dir.ancestors() {
+ for entry in PathBuf::from(path).read_dir().unwrap() {
+ let entry = entry.unwrap();
+ let file_name = entry.file_name().to_string_lossy().to_lowercase();
+ if file_name == "justfile" || file_name == ".justfile" {
+ return Some(entry.path());
+ }
+ }
+ }
+ None
+ }
+
+ fn parse_justfile(justfile_path: PathBuf, source_code: String) -> Option> {
+ let mut parser = Parser::new();
+ parser.set_language(&tree_sitter_just::language()).unwrap();
+ let tree = parser.parse(&source_code, None).unwrap();
+ // source_file
+ // ├── shebang
+ // │ └── language
+ // └── recipe (multiple))
+ // ├── recipe_header
+ // │ └── name: identifier
+ // ├── recipe_body
+ // │ └── recipe_line
+ // │ └── text
+ // └── attribute (multiple, optional)
+ // ├── identifier
+ // └── argument: string
+ let mut commands = vec![];
+
+ // At first, it seemed that it is more readable if we can use `Node#children_by_field_name` instead of `Node#children`.
+ // But the elements wanted to be extracted here do not have names.
+ // So we had no choice but to use `Node#children`.
+ 'recipe: for recipes_and_its_siblings in tree.root_node().named_children(&mut tree.walk()) {
+ if recipes_and_its_siblings.kind() == "recipe" {
+ let mut should_skip = false;
+ recipes_and_its_siblings
+ .children(&mut tree.walk())
+ .for_each(|attr| {
+ let attr_name = &source_code[attr.byte_range()];
+ if attr_name.contains("private") {
+ should_skip = true;
+ }
+ });
+ if should_skip {
+ continue;
+ }
+
+ for recipe_child in recipes_and_its_siblings.named_children(&mut tree.walk()) {
+ if recipe_child.kind() == "recipe_header" {
+ // `recipe_name` has format like: `fmt:`
+ let recipe_name = &source_code[recipe_child.byte_range()];
+ let trimmed = recipe_name.split(":").collect::>();
+ if let Some(r) = trimmed.first() {
+ commands.push(Command::new(
+ RunnerType::Just,
+ r.trim().to_string(),
+ justfile_path.clone(),
+ recipe_child.start_position().row as u32 + 1,
+ ))
+ };
+ continue 'recipe;
+ };
+ }
+ }
+ }
+
+ if commands.is_empty() {
+ None
+ } else {
+ Some(commands)
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use pretty_assertions::assert_eq;
+ use uuid::Uuid;
+
+ #[test]
+ fn test_find_justfile() {
+ // cleanup before test
+ let test_root_dir = std::env::temp_dir().join("fzf_make_test");
+ // error will be returned if the directory does not exist.
+ let _ = std::fs::remove_dir_all(&test_root_dir);
+ std::fs::create_dir(&test_root_dir).unwrap();
+
+ // justfile exists in temp_dir
+ {
+ let test_target_dir = test_root_dir.join(Uuid::new_v4().to_string());
+ std::fs::create_dir(&test_target_dir).unwrap();
+
+ let justfile_path = test_target_dir.join("justfile");
+ std::fs::File::create(&justfile_path).unwrap();
+ assert_eq!(Just::find_justfile(test_target_dir), Some(justfile_path));
+ }
+
+ // .justfile exists in temp_dir
+ {
+ let test_target_dir = test_root_dir.join(Uuid::new_v4().to_string());
+ std::fs::create_dir(&test_target_dir).unwrap();
+
+ let justfile_path = test_target_dir.join(".justfile");
+ std::fs::File::create(&justfile_path).unwrap();
+ assert_eq!(Just::find_justfile(test_target_dir), Some(justfile_path));
+ }
+
+ // justfile exists in the one of ancestors of temp_dir
+ {
+ let parent = test_root_dir.join(Uuid::new_v4().to_string());
+ let test_target_dir = parent.join("child_dir");
+ std::fs::create_dir_all(&test_target_dir).unwrap();
+
+ let justfile_path = parent.join("justfile");
+ std::fs::File::create(&justfile_path).unwrap();
+ assert_eq!(Just::find_justfile(test_target_dir), Some(justfile_path));
+ }
+
+ // no justfile exists
+ {
+ let parent = test_root_dir.join(Uuid::new_v4().to_string());
+ let test_target_dir = parent.join("child_dir");
+ std::fs::create_dir_all(&test_target_dir).unwrap();
+
+ assert_eq!(Just::find_justfile(test_target_dir), None);
+ }
+
+ let _ = std::fs::remove_dir_all(&test_root_dir);
+ }
+
+ #[test]
+ fn test_parse_justfile() {
+ struct Case {
+ name: &'static str,
+ source_code: &'static str,
+ expected: Option>,
+ }
+ let cases = vec![
+ Case {
+ name: "empty justfile",
+ source_code: "",
+ expected: None,
+ },
+ Case {
+ name: "justfile with one recipe",
+ source_code: r#"
+#!/usr/bin/env -S just --justfile
+
+test:
+ cargo test --all
+
+[group: 'misc']
+run:
+ echo run
+
+[group: 'misc']
+build:
+ echo build
+
+[group: 'misc']
+fmt : # https://example.com
+ echo fmt
+
+[group: 'misc']
+[private ]
+fmt-private:
+ echo fmt
+
+# everyone's favorite animate paper clip
+[group: 'check']
+clippy:
+ echo clippy
+ "#,
+ expected: Some(vec![
+ Command {
+ runner_type: RunnerType::Just,
+ args: "test".to_string(),
+ file_name: PathBuf::from("justfile"),
+ line_number: 4,
+ },
+ Command {
+ runner_type: RunnerType::Just,
+ args: "run".to_string(),
+ file_name: PathBuf::from("justfile"),
+ line_number: 8,
+ },
+ Command {
+ runner_type: RunnerType::Just,
+ args: "build".to_string(),
+ file_name: PathBuf::from("justfile"),
+ line_number: 12,
+ },
+ Command {
+ runner_type: RunnerType::Just,
+ args: "fmt".to_string(),
+ file_name: PathBuf::from("justfile"),
+ line_number: 16,
+ },
+ Command {
+ runner_type: RunnerType::Just,
+ args: "clippy".to_string(),
+ file_name: PathBuf::from("justfile"),
+ line_number: 26,
+ },
+ ]),
+ },
+ ];
+
+ for case in cases {
+ let commands =
+ Just::parse_justfile(PathBuf::from("justfile"), case.source_code.to_string());
+ assert_eq!(commands, case.expected, "{}", case.name);
+ }
+ }
+}
diff --git a/src/model/just/mod.rs b/src/model/just/mod.rs
new file mode 100644
index 00000000..52746859
--- /dev/null
+++ b/src/model/just/mod.rs
@@ -0,0 +1 @@
+pub(crate) mod just_main;
diff --git a/src/model/mod.rs b/src/model/mod.rs
index c844432f..53944663 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -2,6 +2,7 @@ pub(super) mod command;
mod file_util;
pub(crate) mod histories;
pub(super) mod js_package_manager;
+pub(super) mod just;
pub(super) mod make;
pub(super) mod runner;
pub(super) mod runner_type;
diff --git a/src/model/runner.rs b/src/model/runner.rs
index b5c250da..d2a625c6 100644
--- a/src/model/runner.rs
+++ b/src/model/runner.rs
@@ -1,12 +1,16 @@
-use super::{command, js_package_manager::js_package_manager_main as js, make::make_main};
+use super::{
+ command, js_package_manager::js_package_manager_main::JsPackageManager, just::just_main::Just,
+ make::make_main::Make,
+};
use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub enum Runner {
- MakeCommand(make_main::Make),
- JsPackageManager(js::JsPackageManager),
+ MakeCommand(Make),
+ JsPackageManager(JsPackageManager),
+ Just(Just),
}
impl Runner {
@@ -14,6 +18,7 @@ impl Runner {
match self {
Runner::MakeCommand(make) => make.to_commands(),
Runner::JsPackageManager(js) => js.to_commands(),
+ Runner::Just(just) => just.to_commands(),
}
}
@@ -21,6 +26,7 @@ impl Runner {
match self {
Runner::MakeCommand(make) => make.path.clone(),
Runner::JsPackageManager(js) => js.path(),
+ Runner::Just(just) => just.path(),
}
}
@@ -28,6 +34,7 @@ impl Runner {
let command_or_error_message = match self {
Runner::MakeCommand(make) => make.command_to_run(command),
Runner::JsPackageManager(js) => js.command_to_run(command),
+ Runner::Just(just) => just.command_to_run(command),
};
println!(
@@ -42,6 +49,7 @@ impl Runner {
match self {
Runner::MakeCommand(make) => make.execute(command),
Runner::JsPackageManager(js) => js.execute(command),
+ Runner::Just(just) => just.execute(command),
}
}
}
diff --git a/src/model/runner_type.rs b/src/model/runner_type.rs
index 3e7f64f3..bf290d0c 100644
--- a/src/model/runner_type.rs
+++ b/src/model/runner_type.rs
@@ -7,6 +7,7 @@ use std::fmt;
pub enum RunnerType {
Make,
JsPackageManager(JsPackageManager),
+ Just,
}
#[derive(Hash, PartialEq, Debug, Clone, Serialize, Deserialize, Eq)]
@@ -37,6 +38,7 @@ impl RunnerType {
RunnerType::JsPackageManager(JsPackageManager::Yarn)
}
},
+ runner::Runner::Just(_) => RunnerType::Just,
}
}
}
@@ -49,6 +51,7 @@ impl fmt::Display for RunnerType {
JsPackageManager::Pnpm => "pnpm",
JsPackageManager::Yarn => "yarn",
},
+ RunnerType::Just => "just",
};
write!(f, "{}", name)
}
@@ -71,6 +74,7 @@ impl<'de> Deserialize<'de> for RunnerType {
"make" => Ok(RunnerType::Make),
"pnpm" => Ok(RunnerType::JsPackageManager(JsPackageManager::Pnpm)),
"yarn" => Ok(RunnerType::JsPackageManager(JsPackageManager::Yarn)),
+ "just" => Ok(RunnerType::Just),
_ => Err(de::Error::custom(format!("Unknown runner type: {}", s))),
}
}
@@ -90,6 +94,7 @@ impl Serialize for RunnerType {
RunnerType::JsPackageManager(JsPackageManager::Yarn) => {
serializer.serialize_str("yarn")
}
+ RunnerType::Just => serializer.serialize_str("just"),
}
}
}
diff --git a/src/usecase/tui/app.rs b/src/usecase/tui/app.rs
index 6be49583..fbf4ce3e 100644
--- a/src/usecase/tui/app.rs
+++ b/src/usecase/tui/app.rs
@@ -3,11 +3,13 @@ use crate::{
err::any_to_string,
file::toml,
model::{
- command,
+ command::{self, Command},
histories::{self},
js_package_manager::js_package_manager_main as js,
- make::make_main,
- runner, runner_type,
+ just::just_main::Just,
+ make::make_main::Make,
+ runner::{self, Runner},
+ runner_type,
},
};
use anyhow::{anyhow, bail, Result};
@@ -90,7 +92,7 @@ impl Model<'_> {
fn get_histories(
current_working_directory: PathBuf,
runners: Vec,
- ) -> Vec {
+ ) -> Vec {
let histories = toml::Histories::into(toml::Histories::get_history());
for history in histories.histories {
@@ -377,14 +379,16 @@ impl SelectCommandState<'_> {
let runners = {
let mut runners = vec![];
- if let Ok(f) = make_main::Make::new(current_dir.clone()) {
- runners.push(runner::Runner::MakeCommand(f));
+ if let Ok(f) = Make::new(current_dir.clone()) {
+ runners.push(Runner::MakeCommand(f));
};
if let Some(js_package_manager) = js::get_js_package_manager_runner(current_dir.clone())
{
- runners.push(runner::Runner::JsPackageManager(js_package_manager));
+ runners.push(Runner::JsPackageManager(js_package_manager));
+ };
+ if let Ok(just) = Just::new(current_dir.clone()) {
+ runners.push(Runner::Just(just));
};
-
runners
};
@@ -643,7 +647,7 @@ impl SelectCommandState<'_> {
SelectCommandState {
current_dir: env::current_dir().unwrap(),
current_pane: CurrentPane::Main,
- runners: vec![runner::Runner::MakeCommand(make_main::Make::new_for_test())],
+ runners: vec![runner::Runner::MakeCommand(Make::new_for_test())],
search_text_area: TextArea_(TextArea::default()),
commands_list_state: ListState::with_selected(ListState::default(), Some(0)),
history: vec![
@@ -911,7 +915,7 @@ mod test {
message: Some(Message::ExecuteCommand),
expect_model: Model {
app_state: AppState::ExecuteCommand(ExecuteCommandState::new(
- runner::Runner::MakeCommand(make_main::Make::new_for_test()),
+ runner::Runner::MakeCommand(Make::new_for_test()),
command::Command::new(
runner_type::RunnerType::Make,
"target0".to_string(),
@@ -933,7 +937,7 @@ mod test {
message: Some(Message::ExecuteCommand),
expect_model: Model {
app_state: AppState::ExecuteCommand(ExecuteCommandState::new(
- runner::Runner::MakeCommand(make_main::Make::new_for_test()),
+ runner::Runner::MakeCommand(Make::new_for_test()),
command::Command::new(
runner_type::RunnerType::Make,
"history1".to_string(),
diff --git a/test_data/just/justfile b/test_data/just/justfile
new file mode 100644
index 00000000..0e342d52
--- /dev/null
+++ b/test_data/just/justfile
@@ -0,0 +1,26 @@
+#!/usr/bin/env -S just --justfile
+
+test:
+ cargo test --all
+
+[group: 'misc']
+run:
+ echo run
+
+[group: 'misc']
+build:
+ echo build
+
+[group: 'misc']
+fmt : # https://example.com
+ echo fmt
+
+[group: 'misc']
+[private ]
+fmt-private:
+ echo fmt
+
+# everyone's favorite animate paper clip
+[group: 'check']
+clippy:
+ echo clippy