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. demo demo ## 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. demo @@ -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