Merge branch 'main' into fix/plugin-directory-extensions
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# Fix Issues Command
|
||||
|
||||
Diagnose, reproduce, and (when reproducible) fix open GitHub issues in parallel — each in its own clean worktree, with build artifacts symlinked so nothing recompiles.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$ARGUMENTS` — optional. Either:
|
||||
- a space- or comma-separated list of issue numbers / URLs, OR
|
||||
- GitHub-search qualifiers (`is:open`, `label:bug`, `author:foo`, ...) and/or a relative time window like `3d`, `2w`, `12h`.
|
||||
|
||||
If no issues and no flags are passed, default to **all open issues opened in the last 3 days**.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Resolve the issue set
|
||||
|
||||
Parse `$ARGUMENTS`.
|
||||
|
||||
- If explicit issue numbers/URLs given, use them verbatim.
|
||||
- Otherwise call the `github` tool with `op: search_issues`. Default (no args):
|
||||
|
||||
```
|
||||
github { op: "search_issues", query: "is:open", since: "3d", limit: 50 }
|
||||
```
|
||||
|
||||
Pass any user-supplied qualifiers verbatim through `query` (combine with `is:open` if not already present). Use `since` for the time window (`3d`, `2w`, `12h`, ISO date — see the `github` tool docs); set `dateField: "updated"` instead of the `created` default only when the user explicitly asks for recently-touched issues.
|
||||
|
||||
Print the resolved set before fanning out so the user can confirm scope.
|
||||
|
||||
### 2. Fan out one subagent per issue
|
||||
|
||||
Use **`task` with parallel subagents** — one task per issue. Pass the issue number, title, body summary, and the workflow below as the assignment. Subagents work in isolation; coordinate via `irc` only when two issues clearly touch the same file.
|
||||
|
||||
Each subagent **MUST** follow this exact workflow:
|
||||
|
||||
#### a. Read everything
|
||||
|
||||
1. `github issue_view` (with comments) — comments often carry the real repro and fix hints.
|
||||
2. `gh search prs` for the issue number to see if a fix is already in flight.
|
||||
- If a PR exists and looks reasonable → switch tracks: review that PR per `.omp/commands/review-prs.md` instead, and report back as `existing-pr`. Do **not** open a competing fix.
|
||||
|
||||
#### b. Diagnose & try to reproduce — **in the current cwd, on `main`**
|
||||
|
||||
Reproduce **here first**, before touching any worktree. The point is to confirm the bug is real on current main before investing in a fix branch.
|
||||
|
||||
1. Read the relevant source paths in this checkout. Form a concrete hypothesis (one or two sentences) about the failure.
|
||||
2. Write a focused test file under the package the bug lives in. Naming: `repro-issue-<N>-<slug>.test.ts` (or `.rs`, etc.) — unique, greppable, deletable.
|
||||
3. Run **only that test file**, not the suite. Confirm it fails for the reason in the issue.
|
||||
|
||||
Outcomes:
|
||||
- **Reproduced** → continue to (c).
|
||||
- **Not reproduced** → stop. Delete the test file. Report `unreproduced` with: hypothesis tried, evidence it doesn't fail, and what info would unblock (versions, OS, config, repro snippet from author). Do **not** create a worktree or commit.
|
||||
- **Out of scope / not a bug** (e.g. user config error, intended behavior, dup) → stop. Report `not-a-bug` with the explanation suitable for posting to the issue.
|
||||
|
||||
#### c. Create a worktree off main
|
||||
|
||||
Only after a confirmed local repro:
|
||||
|
||||
```bash
|
||||
MAIN="$(git rev-parse --show-toplevel)"
|
||||
ENC="$(printf '%s' "$MAIN" | sed 's|[/\\:]|-|g')"
|
||||
WT="$HOME/.omp/wt/${ENC}/fix-issue-<N>"
|
||||
|
||||
git -C "$MAIN" fetch origin main
|
||||
git -C "$MAIN" worktree add -B "fix/issue-<N>" "$WT" origin/main
|
||||
```
|
||||
|
||||
Branch naming: `fix/issue-<N>` (or `fix/issue-<N>-<slug>` if you'll open multiple). Path under `~/.omp/wt/<encoded-main-path>/...` matches the convention `pr_checkout` uses.
|
||||
|
||||
#### d. Symlink build artifacts
|
||||
|
||||
From the new worktree, link build outputs from `$MAIN` so `bun check` / `cargo build` / native loaders skip rebuilds:
|
||||
|
||||
```bash
|
||||
cd "$WT"
|
||||
ln -snf "$MAIN/target" "$WT/target"
|
||||
ln -snf "$MAIN/node_modules" "$WT/node_modules"
|
||||
|
||||
# Only the .node binaries are expensive to rebuild. The rest of
|
||||
# packages/natives/native/ is tracked by git, so folder-level symlinks would
|
||||
# shadow real source files and break the fix.
|
||||
for f in "$MAIN"/packages/natives/native/*.node; do
|
||||
[ -e "$f" ] && ln -snf "$f" "$WT/packages/natives/native/"
|
||||
done
|
||||
```
|
||||
|
||||
Use absolute paths — the worktree lives outside the main checkout.
|
||||
|
||||
#### e. Move the repro test in & fix
|
||||
|
||||
1. Move (don't copy) the failing test file from the main checkout into the same path inside the worktree. Delete it from main so the original cwd is left clean.
|
||||
2. Confirm it still fails inside the worktree on the current branch.
|
||||
3. Implement the fix in source. Match existing patterns (see `AGENTS.md`); fix at the source, not at the symptom; no stubs, no mocks added to product code.
|
||||
4. Re-run the repro test until it passes.
|
||||
5. Add or adjust adjacent unit/contract tests where the fix changes a real contract — not just plumbing. Run **only** the affected test files; no full-suite runs from subagents.
|
||||
6. Run `bun fmt` over the union of files edited.
|
||||
|
||||
#### f. Commit
|
||||
|
||||
Conventional commit, one logical change per commit, with `Fixes #<N>`:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix(<scope>): <one-line summary>
|
||||
|
||||
<short body explaining root cause and the fix>
|
||||
|
||||
Fixes #<N>."
|
||||
```
|
||||
|
||||
Do **not** push. The human pushes / opens the PR.
|
||||
|
||||
#### g. Report back
|
||||
|
||||
Each subagent returns a short structured report:
|
||||
|
||||
```
|
||||
Issue #<N> <title>
|
||||
Status: fixed | unreproduced | not-a-bug | existing-pr (#<M>)
|
||||
Repro: <test path inside worktree> (if applicable)
|
||||
Worktree: ~/.omp/wt/.../fix-issue-<N> (if created)
|
||||
Branch: fix/issue-<N> (if created)
|
||||
Commits: <shas + one-liners> (if any)
|
||||
Notes: <root cause in one sentence; or what info is missing>
|
||||
```
|
||||
|
||||
### 3. Aggregate
|
||||
|
||||
After all subagents finish, print a single summary table:
|
||||
|
||||
```
|
||||
| # | Title | Status | Branch / Notes |
|
||||
|---|-------|--------|----------------|
|
||||
```
|
||||
|
||||
Group worktree paths by status (`fixed` first), so the user can `cd` and push the ready ones in one pass.
|
||||
|
||||
## Rules
|
||||
|
||||
- **MUST** reproduce on `main` in the current cwd **before** creating any worktree. No worktree until repro is confirmed.
|
||||
- **MUST** use parallel subagents — one per issue.
|
||||
- **MUST** check for an existing PR first; if one exists and is reasonable, divert to `review-prs` flow instead of duplicating work.
|
||||
- **MUST** symlink `target`, `node_modules`, and the native `*.node` binaries before any build/test runs in the worktree. **MUST NOT** symlink the whole `packages/natives/native/` directory that would shadow tracked source files.
|
||||
- **MUST** use conventional commits with `Fixes #<N>` in the body.
|
||||
- **MUST NOT** push, open PRs, or comment on issues. Human handles delivery.
|
||||
- **MUST NOT** ship stubs, mocks-as-product-code, or "TODO: implement" placeholders as a fix.
|
||||
- **MUST NOT** expand scope: fix the reported bug, not adjacent code smells.
|
||||
- If repro fails, delete the temporary test file from cwd before yielding — leave the original checkout clean.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Review PRs Command
|
||||
|
||||
Triage incoming pull requests in parallel: decide what's worth merging, prep clean rebased worktrees, fix any blockers, and hand them back ready for human merge.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$ARGUMENTS` — optional. Either:
|
||||
- a space- or comma-separated list of PR numbers / URLs, OR
|
||||
- GitHub-search qualifiers (`is:open`, `author:foo`, `label:bug`, `draft:false`, ...) and/or a relative time window like `3d`, `2w`, `12h`.
|
||||
|
||||
If no PRs and no flags are passed, default to **all open PRs opened in the last 3 days**.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Resolve the PR set
|
||||
|
||||
Parse `$ARGUMENTS`.
|
||||
|
||||
- If explicit PR numbers/URLs given, use them verbatim.
|
||||
- Otherwise call the `github` tool with `op: search_prs`. Default (no args):
|
||||
|
||||
```
|
||||
github { op: "search_prs", query: "is:open", since: "3d", limit: 50 }
|
||||
```
|
||||
|
||||
Pass any user-supplied qualifiers verbatim through `query` (combine with `is:open` if not already present). Use `since` for the time window (`3d`, `2w`, `12h`, ISO date — see the `github` tool docs); set `dateField: "updated"` instead of the `created` default only when the user explicitly asks for recently-touched PRs.
|
||||
|
||||
Print the resolved set before fanning out so the user can confirm scope.
|
||||
|
||||
### 2. Fan out one subagent per PR
|
||||
|
||||
Use **`task` with parallel subagents** — one task per PR. Pass the PR number, head ref, author, and the workflow below as the assignment. Each subagent works in isolation; they coordinate via `irc` only if a fix on PR A would obviously conflict with PR B.
|
||||
|
||||
Each subagent **MUST** follow this exact workflow:
|
||||
|
||||
#### a. Read & decide
|
||||
|
||||
1. `github pr_view` (with comments) and `github pr_diff` for the PR.
|
||||
2. Check `git log origin/main` and `gh search prs` for whether the same change already landed.
|
||||
3. Classify into one of:
|
||||
- **slop** — AI-generated noise, broken, off-spec, or net-negative. Drop, write a 1–2 line justification, do not check out.
|
||||
- **superseded** — already fixed/merged in main or by a newer PR. Drop with a pointer.
|
||||
- **worthy** — proceed.
|
||||
|
||||
Anything ambiguous defaults to `worthy` — let the human decide on a real branch.
|
||||
|
||||
#### b. Check out into a worktree
|
||||
|
||||
```bash
|
||||
gh_PR=<NUMBER>
|
||||
# pr_checkout creates ~/.omp/wt/<encoded-repo>/pr-<N>/ and configures push remote
|
||||
```
|
||||
|
||||
Use the `github pr_checkout` tool, **not** raw `gh pr checkout`. That gives a dedicated worktree wired up for `pr_push` later.
|
||||
|
||||
#### c. Symlink build artifacts (skip native rebuilds)
|
||||
|
||||
From inside the new worktree, link the heavy build outputs from the main checkout so `bun check` / `cargo build` / native loaders do not recompile:
|
||||
|
||||
```bash
|
||||
MAIN="<absolute path to main worktree, e.g. ~/Projects/pi>"
|
||||
WT="$(pwd)"
|
||||
|
||||
# Rust target dir + JS deps (root-level in this monorepo)
|
||||
ln -snf "$MAIN/target" "$WT/target"
|
||||
ln -snf "$MAIN/node_modules" "$WT/node_modules"
|
||||
|
||||
# Prebuilt native addon (avoids 30s+ napi-rs rebuild). Link only the .node
|
||||
# binaries — the rest of packages/natives/native/ is tracked by git, so
|
||||
# folder-level symlinks would shadow PR-modified files and break review.
|
||||
for f in "$MAIN"/packages/natives/native/*.node; do
|
||||
[ -e "$f" ] && ln -snf "$f" "$WT/packages/natives/native/"
|
||||
done
|
||||
```
|
||||
|
||||
Resolve `$MAIN` from the original cwd before `pr_checkout` (`git rev-parse --show-toplevel`). Use absolute paths in symlinks; the worktree lives outside the main repo so relative paths break.
|
||||
|
||||
#### d. Rebase onto main
|
||||
|
||||
```bash
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
If the rebase conflicts:
|
||||
- Resolve trivially mechanical conflicts (formatting, import order, adjacent-line edits) and continue.
|
||||
- Anything semantic → abort the rebase, leave a note in the final report, do not commit.
|
||||
|
||||
#### e. Review & fix critical issues
|
||||
|
||||
Inside the worktree, review the diff with the lens of: correctness, security, regressions, breaking-change impact, test coverage of the new path.
|
||||
|
||||
Only fix things that **block merge**: build/test breakage, obvious bugs introduced by the PR, missing edge-case handling the PR's own goal demands. Do **not** rewrite for taste, refactor unrelated code, or expand scope.
|
||||
|
||||
For every fix:
|
||||
- Read existing patterns first; match repo conventions (see `AGENTS.md`).
|
||||
- Add or update tests for the actual behavior change.
|
||||
- Run only the targeted test file(s) for the area touched. No project-wide test runs from subagents.
|
||||
|
||||
Format/lint at the end with `bun fmt` over the union of files you edited.
|
||||
|
||||
#### f. Commit
|
||||
|
||||
One conventional commit per logical fix on top of the rebased PR branch:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix(<scope>): <what & why>
|
||||
|
||||
Addresses review feedback on #<PR>."
|
||||
```
|
||||
|
||||
Do **not** amend the PR author's commits. Do **not** push — the human merges.
|
||||
|
||||
#### g. Report back
|
||||
|
||||
Each subagent returns a short structured report:
|
||||
|
||||
```
|
||||
PR #<N> <title>
|
||||
Decision: worthy | slop | superseded
|
||||
Worktree: ~/.omp/wt/.../pr-<N> (or: not checked out)
|
||||
Rebase: clean | conflicts (resolved | aborted: <reason>)
|
||||
Fixes: <commit shas + one-liners> (or: none needed)
|
||||
Blockers: <anything the human must decide>
|
||||
```
|
||||
|
||||
### 3. Aggregate
|
||||
|
||||
After all subagents finish, print a single summary table:
|
||||
|
||||
```
|
||||
| PR | Title | Decision | Rebase | Fixes | Blockers |
|
||||
|----|-------|----------|--------|-------|----------|
|
||||
```
|
||||
|
||||
Followed by the worktree paths grouped by decision, so the user can `cd` and merge in one go.
|
||||
|
||||
## Rules
|
||||
|
||||
- **MUST** use parallel subagents — one per PR — not a serial loop.
|
||||
- **MUST** use `github pr_checkout` (carries push metadata) — not raw `gh pr checkout`.
|
||||
- **MUST** symlink `target`, `node_modules`, and the native `*.node` binaries before any build/test runs in the worktree. **MUST NOT** symlink the whole `packages/natives/native/` directory that would shadow tracked PR changes.
|
||||
- **MUST NOT** push or merge. Human reviews and merges.
|
||||
- **MUST NOT** expand scope: fixes are limited to merge blockers on this PR's diff.
|
||||
- **MUST NOT** force-push over the PR author's history.
|
||||
- If a PR is `slop`/`superseded`, skip checkout entirely — just record the decision.
|
||||
Generated
+65
-33
@@ -2370,47 +2370,15 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pi-natives"
|
||||
name = "pi-ast"
|
||||
version = "14.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"ast-grep-core",
|
||||
"brush-builtins",
|
||||
"brush-core",
|
||||
"brush-parser 0.3.0",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"globset",
|
||||
"grep-matcher",
|
||||
"grep-regex",
|
||||
"grep-searcher",
|
||||
"html-to-markdown-rs",
|
||||
"icy_sixel",
|
||||
"ignore",
|
||||
"image",
|
||||
"inferno",
|
||||
"libc",
|
||||
"memmap2",
|
||||
"mimalloc",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"os_pipe",
|
||||
"parking_lot",
|
||||
"phf 0.13.1",
|
||||
"portable-pty",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar 3.1.0",
|
||||
"smallvec",
|
||||
"syntect",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"tree-sitter-astro-next",
|
||||
"tree-sitter-bash",
|
||||
@@ -2467,6 +2435,48 @@ dependencies = [
|
||||
"tree-sitter-xml",
|
||||
"tree-sitter-yaml",
|
||||
"tree-sitter-zig",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pi-natives"
|
||||
version = "14.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"ast-grep-core",
|
||||
"clap",
|
||||
"dashmap",
|
||||
"globset",
|
||||
"grep-matcher",
|
||||
"grep-regex",
|
||||
"grep-searcher",
|
||||
"html-to-markdown-rs",
|
||||
"icy_sixel",
|
||||
"ignore",
|
||||
"image",
|
||||
"inferno",
|
||||
"libc",
|
||||
"memmap2",
|
||||
"mimalloc",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"parking_lot",
|
||||
"phf 0.13.1",
|
||||
"pi-ast",
|
||||
"pi-shell",
|
||||
"portable-pty",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar 3.1.0",
|
||||
"smallvec",
|
||||
"syntect",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"webp",
|
||||
@@ -2475,6 +2485,28 @@ dependencies = [
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pi-shell"
|
||||
version = "14.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brush-builtins",
|
||||
"brush-core",
|
||||
"brush-parser 0.3.0",
|
||||
"clap",
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"windows-sys 0.61.2",
|
||||
"winreg 0.56.0",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
|
||||
+11
-3
@@ -37,9 +37,17 @@ incremental = true
|
||||
strip = false
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
opt-level = 0
|
||||
lto = false
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
debug = "line-tables-only"
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
# Deps compile optimized once and cache; your own crates stay fast.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
debug = false
|
||||
|
||||
[workspace.lints.clippy]
|
||||
# Base Lint Levels
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
telemetry = false
|
||||
|
||||
[install]
|
||||
minimumReleaseAge = 259200 # 3 days in seconds
|
||||
linker = "hoisted"
|
||||
exact = true
|
||||
saveTextLockfile = true
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "pi-ast"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
ast-grep-core = { version = "0.39", default-features = false, features = ["tree-sitter"] }
|
||||
globset = "0.4"
|
||||
ignore = "0.4"
|
||||
phf = { version = "0.13", features = ["macros"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tree-sitter = "0.25"
|
||||
tree-sitter-astro = { version = "0.1.1", package = "tree-sitter-astro-next" }
|
||||
tree-sitter-bash = "0.25"
|
||||
tree-sitter-c = "0.24"
|
||||
tree-sitter-clojure = "0.1"
|
||||
tree-sitter-cmake = "0.7.1"
|
||||
tree-sitter-c-sharp = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-dart = "0.2"
|
||||
tree-sitter-css = "0.25"
|
||||
tree-sitter-diff = "0.1"
|
||||
tree-sitter-dockerfile = { version = "0.2.0", package = "tree-sitter-dockerfile-updated" }
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-erlang = "0.16.0"
|
||||
tree-sitter-go = "0.25"
|
||||
tree-sitter-graphql = "0.1.0"
|
||||
tree-sitter-haskell = "0.23"
|
||||
tree-sitter-hcl = "1.1"
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-ini = "1.4.0"
|
||||
tree-sitter-java = "0.23"
|
||||
tree-sitter-javascript = "0.25"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-just = "0.2.0"
|
||||
tree-sitter-julia = "0.23"
|
||||
tree-sitter-kotlin = { version = "0.4", package = "tree-sitter-kotlin-sg" }
|
||||
tree-sitter-lua = "0.5"
|
||||
tree-sitter-make = "1.1"
|
||||
tree-sitter-md = "0.5"
|
||||
tree-sitter-nix = "0.3"
|
||||
tree-sitter-objc = "3.0"
|
||||
tree-sitter-ocaml = "0.24.2"
|
||||
tree-sitter-odin = "1.3"
|
||||
tree-sitter-perl = { version = "0.1.0", package = "tree-sitter-perl-next" }
|
||||
tree-sitter-php = "0.24"
|
||||
tree-sitter-powershell = "0.26.4"
|
||||
tree-sitter-proto = "0.4.0"
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-r = "1.2.0"
|
||||
tree-sitter-regex = "0.25"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-scala = "0.26"
|
||||
tree-sitter-solidity = "1.2"
|
||||
tree-sitter-sql = { version = "0.3.11", package = "tree-sitter-sequel" }
|
||||
tree-sitter-starlark = "1.3"
|
||||
tree-sitter-svelte = { version = "0.1.1", package = "tree-sitter-svelte-next" }
|
||||
tree-sitter-swift = "0.7"
|
||||
tree-sitter-toml-ng = "0.7"
|
||||
tree-sitter-tlaplus = "1.5"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-verilog = "1.0"
|
||||
tree-sitter-vue = { version = "0.1.0", package = "tree-sitter-vue-next" }
|
||||
tree-sitter-xml = "0.7"
|
||||
tree-sitter-yaml = "0.7"
|
||||
tree-sitter-zig = "1.1"
|
||||
@@ -411,6 +411,10 @@ impl SupportLang {
|
||||
LANG_ALIASES.get(lowered.as_str()).copied()
|
||||
}
|
||||
|
||||
pub fn from_path(path: &Path) -> Option<Self> {
|
||||
from_extension(path)
|
||||
}
|
||||
|
||||
pub fn sorted_aliases() -> &'static [&'static str] {
|
||||
&SORTED_ALIASES
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod language;
|
||||
pub mod ops;
|
||||
pub mod summary;
|
||||
|
||||
pub use language::SupportLang;
|
||||
@@ -0,0 +1,305 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use ast_grep_core::{
|
||||
MatchStrictness, Position,
|
||||
matcher::{Pattern, PatternError},
|
||||
source::Edit,
|
||||
tree_sitter::{LanguageExt, StrDoc},
|
||||
};
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use ignore::WalkBuilder;
|
||||
|
||||
use crate::language::SupportLang;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AstMatchStrictness {
|
||||
Cst,
|
||||
Smart,
|
||||
Ast,
|
||||
Relaxed,
|
||||
Signature,
|
||||
Template,
|
||||
}
|
||||
|
||||
impl From<AstMatchStrictness> for MatchStrictness {
|
||||
fn from(value: AstMatchStrictness) -> Self {
|
||||
match value {
|
||||
AstMatchStrictness::Cst => Self::Cst,
|
||||
AstMatchStrictness::Smart => Self::Smart,
|
||||
AstMatchStrictness::Ast => Self::Ast,
|
||||
AstMatchStrictness::Relaxed => Self::Relaxed,
|
||||
AstMatchStrictness::Signature => Self::Signature,
|
||||
AstMatchStrictness::Template => Self::Template,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AstMatch {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
pub end_line: usize,
|
||||
pub end_column: usize,
|
||||
pub byte_start: usize,
|
||||
pub byte_end: usize,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MatchedFile {
|
||||
pub absolute_path: PathBuf,
|
||||
pub relative_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledRewrite {
|
||||
pub out: String,
|
||||
pub patterns: Vec<Pattern>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resolve_strictness(value: Option<AstMatchStrictness>) -> MatchStrictness {
|
||||
value.map_or(MatchStrictness::Smart, Into::into)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn supported_lang_list() -> String {
|
||||
SupportLang::sorted_aliases().join(", ")
|
||||
}
|
||||
|
||||
pub fn resolve_supported_lang(value: &str) -> Result<SupportLang> {
|
||||
SupportLang::from_alias(value).ok_or_else(|| {
|
||||
anyhow!("Unsupported language '{value}'. Supported: {}", supported_lang_list())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_language(lang: Option<&str>, file_path: &Path) -> Result<SupportLang> {
|
||||
if let Some(lang) = lang.map(str::trim).filter(|lang| !lang.is_empty()) {
|
||||
return resolve_supported_lang(lang);
|
||||
}
|
||||
SupportLang::from_path(file_path).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Unable to infer language from file extension: {}. Specify `lang` explicitly.",
|
||||
file_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_supported_file(file_path: &Path, explicit_lang: Option<&str>) -> bool {
|
||||
if explicit_lang.is_some() {
|
||||
return true;
|
||||
}
|
||||
resolve_language(None, file_path).is_ok()
|
||||
}
|
||||
|
||||
pub fn compile_pattern(
|
||||
pattern: &str,
|
||||
selector: Option<&str>,
|
||||
strictness: &MatchStrictness,
|
||||
lang: SupportLang,
|
||||
) -> Result<Pattern> {
|
||||
let mut compiled = if let Some(selector) = selector.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Pattern::contextual(pattern, selector, lang)
|
||||
} else {
|
||||
Pattern::try_new(pattern, lang)
|
||||
}
|
||||
.map_err(|err| anyhow!("Invalid pattern: {err}"))?;
|
||||
compiled.strictness = strictness.clone();
|
||||
Ok(compiled)
|
||||
}
|
||||
|
||||
pub fn compile_search_patterns(
|
||||
pattern: &str,
|
||||
language: SupportLang,
|
||||
) -> Result<Vec<Pattern>, PatternError> {
|
||||
let mut compiled = vec![Pattern::try_new(pattern, language)?];
|
||||
if language == SupportLang::Rust {
|
||||
let trimmed = pattern.trim_end();
|
||||
if let Some(contextual) = compile_rust_contextual_pattern(trimmed) {
|
||||
compiled.push(contextual);
|
||||
}
|
||||
}
|
||||
Ok(compiled)
|
||||
}
|
||||
|
||||
pub fn compile_rewrite_rules(
|
||||
rules: &[(String, String)],
|
||||
language: SupportLang,
|
||||
) -> Result<Vec<CompiledRewrite>, (usize, PatternError)> {
|
||||
rules
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, (pattern, out))| {
|
||||
compile_search_patterns(pattern, language)
|
||||
.map(|patterns| CompiledRewrite { out: out.clone(), patterns })
|
||||
.map_err(|error| (index, error))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn collect_matches(source: &str, language: SupportLang, patterns: &[Pattern]) -> Vec<AstMatch> {
|
||||
let ast = language.ast_grep(source);
|
||||
let mut matches = Vec::new();
|
||||
for pattern in patterns {
|
||||
for matched in ast.root().find_all(pattern.clone()) {
|
||||
let start = matched.start_pos();
|
||||
let end = matched.end_pos();
|
||||
let range = matched.range();
|
||||
matches.push(AstMatch {
|
||||
line: start.line() + 1,
|
||||
column: char_column(start, matched.get_node()) + 1,
|
||||
end_line: end.line() + 1,
|
||||
end_column: char_column(end, matched.get_node()) + 1,
|
||||
byte_start: range.start,
|
||||
byte_end: range.end,
|
||||
text: matched.text().into_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
matches
|
||||
}
|
||||
|
||||
pub fn rewrite_source(
|
||||
source: &str,
|
||||
language: SupportLang,
|
||||
ops: &[CompiledRewrite],
|
||||
) -> Result<(String, u32), String> {
|
||||
let mut ast = language.ast_grep(source);
|
||||
let mut replacements = 0_u32;
|
||||
for op in ops {
|
||||
for pattern in &op.patterns {
|
||||
let edits = ast.root().replace_all(pattern.clone(), op.out.as_str());
|
||||
if edits.is_empty() {
|
||||
continue;
|
||||
}
|
||||
replacements = replacements.saturating_add(edits.len() as u32);
|
||||
let updated =
|
||||
apply_edits(ast.root().text().as_ref(), &edits).map_err(|error| error.to_string())?;
|
||||
ast = language.ast_grep(updated);
|
||||
}
|
||||
}
|
||||
Ok((ast.root().text().into_owned(), replacements))
|
||||
}
|
||||
|
||||
pub fn apply_edits(content: &str, edits: &[Edit<String>]) -> Result<String> {
|
||||
let mut sorted: Vec<&Edit<String>> = edits.iter().collect();
|
||||
sorted.sort_by_key(|edit| edit.position);
|
||||
let mut prev_end = 0usize;
|
||||
for edit in &sorted {
|
||||
if edit.position < prev_end {
|
||||
return Err(anyhow!(
|
||||
"Overlapping replacements detected; refine pattern to avoid ambiguous edits"
|
||||
));
|
||||
}
|
||||
prev_end = edit.position.saturating_add(edit.deleted_length);
|
||||
}
|
||||
|
||||
let mut output = content.to_string();
|
||||
for edit in sorted.into_iter().rev() {
|
||||
let start = edit.position;
|
||||
let end = edit.position.saturating_add(edit.deleted_length);
|
||||
if end > output.len() || start > end {
|
||||
return Err(anyhow!("Computed edit range is out of bounds"));
|
||||
}
|
||||
let replacement = String::from_utf8(edit.inserted_text.clone())
|
||||
.map_err(|err| anyhow!("Replacement text is not valid UTF-8: {err}"))?;
|
||||
output.replace_range(start..end, &replacement);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn collect_matched_files(
|
||||
cwd: &Path,
|
||||
patterns: &[String],
|
||||
) -> Result<Vec<MatchedFile>, std::io::Error> {
|
||||
let globset = build_globset(patterns)?;
|
||||
let mut builder = WalkBuilder::new(cwd);
|
||||
builder
|
||||
.hidden(false)
|
||||
.git_ignore(true)
|
||||
.git_global(true)
|
||||
.git_exclude(true);
|
||||
let mut files = Vec::new();
|
||||
for entry in builder.build() {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(error) => return Err(std::io::Error::other(error)),
|
||||
};
|
||||
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
|
||||
continue;
|
||||
}
|
||||
let absolute_path = entry.into_path();
|
||||
let relative_path = absolute_path
|
||||
.strip_prefix(cwd)
|
||||
.unwrap_or(&absolute_path)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if globset.is_match(&relative_path)
|
||||
|| patterns.iter().any(|pattern| pattern == &relative_path)
|
||||
{
|
||||
files.push(MatchedFile { absolute_path, relative_path });
|
||||
}
|
||||
}
|
||||
files.sort_unstable_by(|left, right| left.relative_path.cmp(&right.relative_path));
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn build_globset(patterns: &[String]) -> Result<GlobSet, std::io::Error> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
for pattern in patterns {
|
||||
if has_glob_syntax(pattern) {
|
||||
let glob = Glob::new(pattern).map_err(|error| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("invalid glob `{pattern}`: {error}"),
|
||||
)
|
||||
})?;
|
||||
builder.add(glob);
|
||||
}
|
||||
}
|
||||
builder.build().map_err(std::io::Error::other)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_glob_syntax(pattern: &str) -> bool {
|
||||
pattern.contains('*') || pattern.contains('?') || pattern.contains('[')
|
||||
}
|
||||
|
||||
fn char_column(position: Position, node: &ast_grep_core::Node<'_, StrDoc<SupportLang>>) -> usize {
|
||||
position.column(node)
|
||||
}
|
||||
|
||||
fn compile_rust_contextual_pattern(pattern: &str) -> Option<Pattern> {
|
||||
let language = SupportLang::Rust;
|
||||
let context = format!("fn __rwp_wrapper() {{ {pattern}; }}");
|
||||
let ast = language.ast_grep(&context);
|
||||
let selector = ast.root().find("expression_statement")?;
|
||||
Pattern::contextual(pattern, selector.kind().as_ref(), language).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ast_grep_core::source::Edit;
|
||||
|
||||
use super::{SupportLang, apply_edits, compile_search_patterns};
|
||||
|
||||
#[test]
|
||||
fn compile_search_patterns_compiles_rust_patterns() {
|
||||
let patterns = compile_search_patterns("foo($$$ARGS)", SupportLang::Rust)
|
||||
.expect("rust pattern should compile");
|
||||
assert!(!patterns.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_edits_rejects_overlaps() {
|
||||
let source = "abcdef";
|
||||
let edits = vec![
|
||||
Edit::<String> { position: 1, deleted_length: 3, inserted_text: b"x".to_vec() },
|
||||
Edit::<String> { position: 2, deleted_length: 1, inserted_text: b"y".to_vec() },
|
||||
];
|
||||
assert!(apply_edits(source, &edits).is_err());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,13 +18,10 @@ tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
napi = { version = "3", features = ["napi10", "tokio_rt", "tokio_time"] }
|
||||
napi-derive = "3"
|
||||
brush-core = { version = "0.5.0", path = "../brush-core-vendored" }
|
||||
brush-builtins = { version = "0.2.0", path = "../brush-builtins-vendored" }
|
||||
brush-parser = "0.3"
|
||||
pi-shell = { path = "../pi-shell" }
|
||||
parking_lot = "0.12.5"
|
||||
dashmap = "6.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
os_pipe = "1"
|
||||
portable-pty = "0.9"
|
||||
grep-regex = "0.1"
|
||||
grep-searcher = "0.1"
|
||||
@@ -35,62 +32,7 @@ rayon = "1.12"
|
||||
ast-grep-core = { version = "0.39", default-features = false, features = [
|
||||
"tree-sitter",
|
||||
] }
|
||||
tree-sitter = "0.25"
|
||||
tree-sitter-astro = { version = "0.1.1", package = "tree-sitter-astro-next" }
|
||||
tree-sitter-bash = "0.25"
|
||||
tree-sitter-c = "0.24"
|
||||
tree-sitter-clojure = "0.1"
|
||||
tree-sitter-cmake = "0.7.1"
|
||||
tree-sitter-c-sharp = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-dart = "0.2"
|
||||
tree-sitter-css = "0.25"
|
||||
tree-sitter-diff = "0.1"
|
||||
tree-sitter-dockerfile = { version = "0.2.0", package = "tree-sitter-dockerfile-updated" }
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-erlang = "0.16.0"
|
||||
tree-sitter-go = "0.25"
|
||||
tree-sitter-graphql = "0.1.0"
|
||||
tree-sitter-haskell = "0.23"
|
||||
tree-sitter-hcl = "1.1"
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-ini = "1.4.0"
|
||||
tree-sitter-java = "0.23"
|
||||
tree-sitter-javascript = "0.25"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-just = "0.2.0"
|
||||
tree-sitter-julia = "0.23"
|
||||
tree-sitter-kotlin = { version = "0.4", package = "tree-sitter-kotlin-sg" }
|
||||
tree-sitter-lua = "0.5"
|
||||
tree-sitter-make = "1.1"
|
||||
tree-sitter-md = "0.5"
|
||||
tree-sitter-nix = "0.3"
|
||||
tree-sitter-objc = "3.0"
|
||||
tree-sitter-ocaml = "0.24.2"
|
||||
tree-sitter-odin = "1.3"
|
||||
tree-sitter-perl = { version = "0.1.0", package = "tree-sitter-perl-next" }
|
||||
tree-sitter-php = "0.24"
|
||||
tree-sitter-powershell = "0.26.4"
|
||||
tree-sitter-proto = "0.4.0"
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-r = "1.2.0"
|
||||
tree-sitter-regex = "0.25"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-scala = "0.26"
|
||||
tree-sitter-solidity = "1.2"
|
||||
tree-sitter-sql = { version = "0.3.11", package = "tree-sitter-sequel" }
|
||||
tree-sitter-starlark = "1.3"
|
||||
tree-sitter-svelte = { version = "0.1.1", package = "tree-sitter-svelte-next" }
|
||||
tree-sitter-swift = "0.7"
|
||||
tree-sitter-toml-ng = "0.7"
|
||||
tree-sitter-tlaplus = "1.5"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-verilog = "1.0"
|
||||
tree-sitter-vue = { version = "0.1.0", package = "tree-sitter-vue-next" }
|
||||
tree-sitter-xml = "0.7"
|
||||
tree-sitter-yaml = "0.7"
|
||||
tree-sitter-zig = "1.1"
|
||||
pi-ast = { path = "../pi-ast" }
|
||||
inferno = { version = "0.12", default-features = false }
|
||||
image = { version = "0.25", default-features = false, features = [
|
||||
"png",
|
||||
|
||||
@@ -5,13 +5,15 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use ast_grep_core::{
|
||||
Language, MatchStrictness, matcher::Pattern, source::Edit, tree_sitter::LanguageExt,
|
||||
};
|
||||
use ast_grep_core::{MatchStrictness, matcher::Pattern, source::Edit, tree_sitter::LanguageExt};
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
use pi_ast::{
|
||||
SupportLang,
|
||||
ops::{self as shared_ops},
|
||||
};
|
||||
|
||||
use crate::{fs_cache, glob_util, language::SupportLang, task};
|
||||
use crate::{fs_cache, glob_util, task};
|
||||
|
||||
const DEFAULT_FIND_LIMIT: u32 = 50;
|
||||
|
||||
@@ -227,29 +229,12 @@ fn to_u32(value: usize) -> u32 {
|
||||
value.min(u32::MAX as usize) as u32
|
||||
}
|
||||
|
||||
fn supported_lang_list() -> String {
|
||||
SupportLang::sorted_aliases().join(", ")
|
||||
}
|
||||
|
||||
fn resolve_supported_lang(value: &str) -> Result<SupportLang> {
|
||||
SupportLang::from_alias(value).ok_or_else(|| {
|
||||
Error::from_reason(format!(
|
||||
"Unsupported language '{value}'. Supported: {}",
|
||||
supported_lang_list()
|
||||
))
|
||||
})
|
||||
shared_ops::resolve_supported_lang(value).map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
fn resolve_language(lang: Option<&str>, file_path: &Path) -> Result<SupportLang> {
|
||||
if let Some(lang) = lang.map(str::trim).filter(|lang| !lang.is_empty()) {
|
||||
return resolve_supported_lang(lang);
|
||||
}
|
||||
SupportLang::from_path(file_path).ok_or_else(|| {
|
||||
Error::from_reason(format!(
|
||||
"Unable to infer language from file extension: {}. Specify `lang` explicitly.",
|
||||
file_path.display()
|
||||
))
|
||||
})
|
||||
shared_ops::resolve_language(lang, file_path).map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
/// Returns true if the file's extension resolves to a supported language.
|
||||
@@ -257,10 +242,7 @@ fn resolve_language(lang: Option<&str>, file_path: &Path) -> Result<SupportLang>
|
||||
/// (the user chose to treat them as that language). When `lang` is None,
|
||||
/// only files with recognizable code extensions are included.
|
||||
fn is_supported_file(file_path: &Path, explicit_lang: Option<&str>) -> bool {
|
||||
if explicit_lang.is_some() {
|
||||
return true;
|
||||
}
|
||||
resolve_language(None, file_path).is_ok()
|
||||
shared_ops::is_supported_file(file_path, explicit_lang)
|
||||
}
|
||||
|
||||
fn infer_single_replace_lang(
|
||||
@@ -414,43 +396,12 @@ fn compile_pattern(
|
||||
strictness: &MatchStrictness,
|
||||
lang: SupportLang,
|
||||
) -> Result<Pattern> {
|
||||
let mut compiled = if let Some(selector) = selector.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Pattern::contextual(pattern, selector, lang)
|
||||
} else {
|
||||
Pattern::try_new(pattern, lang)
|
||||
}
|
||||
.map_err(|err| Error::from_reason(format!("Invalid pattern: {err}")))?;
|
||||
compiled.strictness = strictness.clone();
|
||||
Ok(compiled)
|
||||
shared_ops::compile_pattern(pattern, selector, strictness, lang)
|
||||
.map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
fn apply_edits(content: &str, edits: &[Edit<String>]) -> Result<String> {
|
||||
let mut sorted: Vec<&Edit<String>> = edits.iter().collect();
|
||||
sorted.sort_by_key(|edit| edit.position);
|
||||
let mut prev_end = 0usize;
|
||||
for edit in &sorted {
|
||||
if edit.position < prev_end {
|
||||
return Err(Error::from_reason(
|
||||
"Overlapping replacements detected; refine pattern to avoid ambiguous edits"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
prev_end = edit.position.saturating_add(edit.deleted_length);
|
||||
}
|
||||
|
||||
let mut output = content.to_string();
|
||||
for edit in sorted.into_iter().rev() {
|
||||
let start = edit.position;
|
||||
let end = edit.position.saturating_add(edit.deleted_length);
|
||||
if end > output.len() || start > end {
|
||||
return Err(Error::from_reason("Computed edit range is out of bounds".to_string()));
|
||||
}
|
||||
let replacement = String::from_utf8(edit.inserted_text.clone()).map_err(|err| {
|
||||
Error::from_reason(format!("Replacement text is not valid UTF-8: {err}"))
|
||||
})?;
|
||||
output.replace_range(start..end, &replacement);
|
||||
}
|
||||
Ok(output)
|
||||
shared_ops::apply_edits(content, edits).map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
fn normalize_pattern_list(patterns: Option<Vec<String>>) -> Result<Vec<String>> {
|
||||
|
||||
@@ -37,7 +37,7 @@ pub mod highlight;
|
||||
pub mod html;
|
||||
pub mod image;
|
||||
pub mod keys;
|
||||
pub mod language;
|
||||
pub use pi_ast::language;
|
||||
|
||||
pub mod power;
|
||||
|
||||
|
||||
+29
-1391
File diff suppressed because it is too large
Load Diff
+221
-1385
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+38
-128
@@ -27,17 +27,10 @@
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
sync::{
|
||||
Arc, Weak,
|
||||
atomic::{AtomicU8, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::future::Future;
|
||||
|
||||
use napi::{Env, Error, Result, Task, bindgen_prelude::*};
|
||||
use tokio::sync::Notify;
|
||||
use pi_shell::cancel as core_cancel;
|
||||
|
||||
use crate::prof::profile_region;
|
||||
|
||||
@@ -47,55 +40,31 @@ use crate::prof::profile_region;
|
||||
|
||||
/// Reason for task abortion.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum AbortReason {
|
||||
Unknown = 1,
|
||||
Timeout = 2,
|
||||
Signal = 3,
|
||||
User = 4,
|
||||
Unknown,
|
||||
Timeout,
|
||||
Signal,
|
||||
User,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for AbortReason {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u8) -> std::result::Result<Self, ()> {
|
||||
impl From<core_cancel::AbortReason> for AbortReason {
|
||||
fn from(value: core_cancel::AbortReason) -> Self {
|
||||
match value {
|
||||
0 => Err(()),
|
||||
2 => Ok(Self::Timeout),
|
||||
3 => Ok(Self::Signal),
|
||||
4 => Ok(Self::User),
|
||||
_ => Ok(Self::Unknown),
|
||||
core_cancel::AbortReason::Unknown => Self::Unknown,
|
||||
core_cancel::AbortReason::Timeout => Self::Timeout,
|
||||
core_cancel::AbortReason::Signal => Self::Signal,
|
||||
core_cancel::AbortReason::User => Self::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Flag {
|
||||
reason: AtomicU8,
|
||||
notifier: Notify,
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
fn cause(&self) -> Option<AbortReason> {
|
||||
self.reason.load(Ordering::Relaxed).try_into().ok()
|
||||
}
|
||||
|
||||
async fn wait(&self) -> AbortReason {
|
||||
if let Some(reason) = self.cause() {
|
||||
return reason;
|
||||
}
|
||||
let notifier = self.notifier.notified();
|
||||
if let Some(reason) = self.cause() {
|
||||
return reason;
|
||||
}
|
||||
notifier.await;
|
||||
self.cause().unwrap_or(AbortReason::Unknown)
|
||||
}
|
||||
|
||||
fn abort(&self, reason: AbortReason) {
|
||||
let old = self.reason.swap(reason as u8, Ordering::SeqCst);
|
||||
if old == 0 {
|
||||
self.notifier.notify_waiters();
|
||||
impl From<AbortReason> for core_cancel::AbortReason {
|
||||
fn from(value: AbortReason) -> Self {
|
||||
match value {
|
||||
AbortReason::Unknown => Self::Unknown,
|
||||
AbortReason::Timeout => Self::Timeout,
|
||||
AbortReason::Signal => Self::Signal,
|
||||
AbortReason::User => Self::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +75,7 @@ impl Flag {
|
||||
/// cancellation requests from timeouts or abort signals.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CancelToken {
|
||||
deadline: Option<Instant>,
|
||||
flag: Option<Arc<Flag>>,
|
||||
core: core_cancel::CancelToken,
|
||||
}
|
||||
|
||||
impl From<()> for CancelToken {
|
||||
@@ -119,21 +87,10 @@ impl From<()> for CancelToken {
|
||||
impl CancelToken {
|
||||
/// Create a new cancel token from optional timeout and abort signal.
|
||||
pub fn new(timeout_ms: Option<u32>, signal: Option<Unknown>) -> Self {
|
||||
let mut result = Self::default();
|
||||
if let Some(signal) = signal.and_then(|s| AbortSignal::from_unknown(s).ok()) {
|
||||
let flag = Arc::new(Flag::default());
|
||||
signal.on_abort({
|
||||
let weak = Arc::downgrade(&flag);
|
||||
move || {
|
||||
if let Some(flag) = weak.upgrade() {
|
||||
flag.abort(AbortReason::Signal);
|
||||
}
|
||||
}
|
||||
});
|
||||
result.flag = Some(flag);
|
||||
}
|
||||
if let Some(timeout_ms) = timeout_ms {
|
||||
result.deadline = Some(Instant::now() + Duration::from_millis(timeout_ms as u64));
|
||||
let mut result = Self { core: core_cancel::CancelToken::new(timeout_ms) };
|
||||
if let Some(signal) = signal.and_then(|value| AbortSignal::from_unknown(value).ok()) {
|
||||
let abort_token = result.emplace_abort_token();
|
||||
signal.on_abort(move || abort_token.abort(AbortReason::Signal));
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -143,92 +100,45 @@ impl CancelToken {
|
||||
/// Returns `Ok(())` if work should continue, or an error if cancelled.
|
||||
/// Call this periodically in long-running loops.
|
||||
pub fn heartbeat(&self) -> Result<()> {
|
||||
if let Some(flag) = &self.flag
|
||||
&& let Some(reason) = flag.cause()
|
||||
{
|
||||
return Err(Error::from_reason(format!("Aborted: {reason:?}")));
|
||||
}
|
||||
if let Some(deadline) = self.deadline
|
||||
&& deadline < Instant::now()
|
||||
{
|
||||
return Err(Error::from_reason("Aborted: Timeout"));
|
||||
}
|
||||
Ok(())
|
||||
self
|
||||
.core
|
||||
.heartbeat()
|
||||
.map_err(|err| Error::from_reason(err.to_string()))
|
||||
}
|
||||
|
||||
/// Wait for the cancel token to be aborted.
|
||||
pub async fn wait(&self) -> AbortReason {
|
||||
let flag = self.flag.as_ref();
|
||||
if let Some(flag) = flag.and_then(|f| f.cause()) {
|
||||
return flag;
|
||||
}
|
||||
let fflag = async {
|
||||
let Some(flag) = self.flag.as_ref() else {
|
||||
return std::future::pending().await;
|
||||
};
|
||||
flag.wait().await
|
||||
};
|
||||
|
||||
let fttl = async {
|
||||
let Some(ttl) = self.deadline else {
|
||||
return std::future::pending().await;
|
||||
};
|
||||
tokio::time::sleep_until(ttl.into()).await;
|
||||
AbortReason::Timeout
|
||||
};
|
||||
|
||||
let fuser = async {
|
||||
if tokio::signal::ctrl_c().await.is_err() {
|
||||
return std::future::pending().await;
|
||||
}
|
||||
AbortReason::User
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
reason = fflag => reason,
|
||||
reason = fttl => reason,
|
||||
reason = fuser => reason,
|
||||
}
|
||||
self.core.wait().await.into()
|
||||
}
|
||||
|
||||
/// Get an abort token for external cancellation.
|
||||
pub fn abort_token(&self) -> AbortToken {
|
||||
AbortToken(self.flag.as_ref().map(Arc::downgrade))
|
||||
AbortToken(self.core.abort_token())
|
||||
}
|
||||
|
||||
/// Emplaces a cancel token if there is none, returns the abort token.
|
||||
pub fn emplace_abort_token(&mut self) -> AbortToken {
|
||||
AbortToken(Some(Arc::downgrade(self.flag.get_or_insert_default())))
|
||||
AbortToken(self.core.emplace_abort_token())
|
||||
}
|
||||
|
||||
/// Check if already aborted (non-blocking).
|
||||
pub fn aborted(&self) -> bool {
|
||||
if let Some(flag) = &self.flag
|
||||
&& flag.cause().is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(deadline) = self.deadline
|
||||
&& deadline < Instant::now()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
self.core.aborted()
|
||||
}
|
||||
|
||||
pub fn into_core(self) -> core_cancel::CancelToken {
|
||||
self.core
|
||||
}
|
||||
}
|
||||
|
||||
/// Token for requesting cancellation from outside the task.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AbortToken(Option<Weak<Flag>>);
|
||||
pub struct AbortToken(core_cancel::AbortToken);
|
||||
|
||||
impl AbortToken {
|
||||
/// Request cancellation of the associated task.
|
||||
pub fn abort(&self, reason: AbortReason) {
|
||||
if let Some(flag) = &self.0
|
||||
&& let Some(flag) = flag.upgrade()
|
||||
{
|
||||
flag.abort(reason);
|
||||
}
|
||||
self.0.abort(reason.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "pi-shell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
brush-core = { version = "0.5.0", path = "../brush-core-vendored" }
|
||||
brush-builtins = { version = "0.2.0", path = "../brush-builtins-vendored" }
|
||||
brush-parser = "0.3"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
os_pipe = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
toml = "1.1"
|
||||
regex = "1"
|
||||
xxhash-rust = { version = "0.8", features = ["xxh64"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.56"
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Storage_ProjectedFileSystem",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_LibraryLoader",
|
||||
] }
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::{
|
||||
env,
|
||||
fmt::Write as _,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
generate_minimizer_builtin_filters();
|
||||
}
|
||||
|
||||
fn generate_minimizer_builtin_filters() {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR should be set");
|
||||
let defs_dir = Path::new(&manifest_dir)
|
||||
.join("src")
|
||||
.join("minimizer")
|
||||
.join("defs");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set"));
|
||||
let output_path = out_dir.join("builtin_filters.toml");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", defs_dir.display());
|
||||
|
||||
let mut concatenated =
|
||||
String::from("# Auto-generated by build.rs -- do not edit.\nschema_version = 1\n\n");
|
||||
|
||||
let mut entries: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(read_dir) = fs::read_dir(&defs_dir) {
|
||||
for entry in read_dir.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|extension| extension.to_str()) == Some("toml") {
|
||||
entries.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.sort();
|
||||
|
||||
for path in entries {
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(body) => {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("unknown");
|
||||
writeln!(concatenated, "# --- {filename} ---").expect("write to String");
|
||||
for line in body.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with("schema_version") {
|
||||
continue;
|
||||
}
|
||||
concatenated.push_str(line);
|
||||
concatenated.push('\n');
|
||||
}
|
||||
concatenated.push('\n');
|
||||
},
|
||||
Err(error) => panic!("failed to read filter definition {}: {error}", path.display()),
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(&output_path, concatenated)
|
||||
.unwrap_or_else(|error| panic!("failed to write {}: {error}", output_path.display()));
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::{
|
||||
sync::{
|
||||
Arc, Weak,
|
||||
atomic::{AtomicU8, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum AbortReason {
|
||||
Unknown = 1,
|
||||
Timeout = 2,
|
||||
Signal = 3,
|
||||
User = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for AbortReason {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: u8) -> std::result::Result<Self, ()> {
|
||||
match value {
|
||||
0 => Err(()),
|
||||
2 => Ok(Self::Timeout),
|
||||
3 => Ok(Self::Signal),
|
||||
4 => Ok(Self::User),
|
||||
_ => Ok(Self::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Flag {
|
||||
reason: AtomicU8,
|
||||
notifier: Notify,
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
fn cause(&self) -> Option<AbortReason> {
|
||||
self.reason.load(Ordering::Relaxed).try_into().ok()
|
||||
}
|
||||
|
||||
async fn wait(&self) -> AbortReason {
|
||||
if let Some(reason) = self.cause() {
|
||||
return reason;
|
||||
}
|
||||
let notifier = self.notifier.notified();
|
||||
if let Some(reason) = self.cause() {
|
||||
return reason;
|
||||
}
|
||||
notifier.await;
|
||||
self.cause().unwrap_or(AbortReason::Unknown)
|
||||
}
|
||||
|
||||
fn abort(&self, reason: AbortReason) {
|
||||
let old = self.reason.swap(reason as u8, Ordering::SeqCst);
|
||||
if old == 0 {
|
||||
self.notifier.notify_waiters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CancelToken {
|
||||
deadline: Option<Instant>,
|
||||
flag: Option<Arc<Flag>>,
|
||||
}
|
||||
|
||||
impl From<()> for CancelToken {
|
||||
fn from((): ()) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl CancelToken {
|
||||
pub fn new(timeout_ms: Option<u32>) -> Self {
|
||||
Self::with_timeout(timeout_ms.map(|ms| Duration::from_millis(u64::from(ms))))
|
||||
}
|
||||
|
||||
pub fn with_timeout(timeout: Option<Duration>) -> Self {
|
||||
Self { deadline: timeout.map(|duration| Instant::now() + duration), flag: None }
|
||||
}
|
||||
|
||||
pub fn heartbeat(&self) -> Result<()> {
|
||||
if let Some(flag) = &self.flag
|
||||
&& let Some(reason) = flag.cause()
|
||||
{
|
||||
return Err(Error::msg(format!("Aborted: {reason:?}")));
|
||||
}
|
||||
if let Some(deadline) = self.deadline
|
||||
&& deadline < Instant::now()
|
||||
{
|
||||
return Err(Error::msg("Aborted: Timeout"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait(&self) -> AbortReason {
|
||||
if let Some(flag) = self.flag.as_ref().and_then(|flag| flag.cause()) {
|
||||
return flag;
|
||||
}
|
||||
|
||||
let by_flag = async {
|
||||
let Some(flag) = self.flag.as_ref() else {
|
||||
return std::future::pending().await;
|
||||
};
|
||||
flag.wait().await
|
||||
};
|
||||
|
||||
let by_timeout = async {
|
||||
let Some(deadline) = self.deadline else {
|
||||
return std::future::pending().await;
|
||||
};
|
||||
tokio::time::sleep_until(deadline.into()).await;
|
||||
AbortReason::Timeout
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
reason = by_flag => reason,
|
||||
reason = by_timeout => reason,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn abort_token(&self) -> AbortToken {
|
||||
AbortToken(self.flag.as_ref().map(Arc::downgrade))
|
||||
}
|
||||
|
||||
pub fn emplace_abort_token(&mut self) -> AbortToken {
|
||||
AbortToken(Some(Arc::downgrade(self.flag.get_or_insert_default())))
|
||||
}
|
||||
|
||||
pub fn aborted(&self) -> bool {
|
||||
if let Some(flag) = &self.flag
|
||||
&& flag.cause().is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(deadline) = self.deadline
|
||||
&& deadline < Instant::now()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AbortToken(Option<Weak<Flag>>);
|
||||
|
||||
impl AbortToken {
|
||||
pub fn abort(&self, reason: AbortReason) {
|
||||
if let Some(flag) = &self.0
|
||||
&& let Some(flag) = flag.upgrade()
|
||||
{
|
||||
flag.abort(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod cancel;
|
||||
pub mod minimizer;
|
||||
pub mod process;
|
||||
pub mod shell;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
pub use brush_core::commands::{ChildSessionAction, child_session_action};
|
||||
pub use shell::{
|
||||
MinimizerResult, Shell, ShellExecuteOptions, ShellExecuteResult, ShellOptions, ShellRunOptions,
|
||||
ShellRunResult, execute_shell,
|
||||
};
|
||||
+1
-3
@@ -12,15 +12,13 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use napi_derive::napi;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::shell::minimizer::pipeline::{self, PipelineRegistry, SUPPORTED_SCHEMA_VERSION};
|
||||
use crate::minimizer::pipeline::{self, PipelineRegistry, SUPPORTED_SCHEMA_VERSION};
|
||||
|
||||
const DEFAULT_MAX_CAPTURE_BYTES: u32 = 4 * 1024 * 1024;
|
||||
|
||||
/// N-API opt-in handle for the minimizer.
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MinimizerOptions {
|
||||
/// Master switch. Absent / false = disabled.
|
||||
+2
-2
@@ -8,7 +8,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::shell::minimizer::{
|
||||
use crate::minimizer::{
|
||||
MinimizerConfig, MinimizerCtx, MinimizerOutput, detect, filters,
|
||||
pipeline::{self, CompiledPipeline, PipelineRegistry},
|
||||
plan,
|
||||
@@ -420,7 +420,7 @@ mod tests {
|
||||
#[cfg(test)]
|
||||
mod pipeline_integration_tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerOptions;
|
||||
use crate::minimizer::MinimizerOptions;
|
||||
|
||||
#[test]
|
||||
fn builtin_filters_parse_and_pass_inline_tests() {
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
//! Bun package-manager, test-runner, and tool output filters.
|
||||
|
||||
use super::{cpp, generic, js_tools, lint, node_tests, pkg};
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
const BUN_PACKAGE_SUBCOMMANDS: &[&str] = &[
|
||||
"install", "i", "add", "update", "up", "upgrade", "remove", "rm", "outdated", "pm", "audit",
|
||||
@@ -142,7 +142,7 @@ fn is_important(line: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
fn ctx<'a>(
|
||||
program: &'a str,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! Cargo build/test output filters.
|
||||
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
@@ -293,7 +293,7 @@ fn is_general_cargo_noise(line: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
#[test]
|
||||
fn strips_compiling_noise() {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! Cloud and data command output filters.
|
||||
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
const MAX_PSQL_ROWS: usize = 30;
|
||||
const MAX_LINE_CHARS: usize = 500;
|
||||
@@ -401,7 +401,7 @@ fn join_lines(lines: Vec<String>) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
fn ctx<'a>(program: &'a str, cfg: &'a MinimizerConfig) -> MinimizerCtx<'a> {
|
||||
MinimizerCtx { program, subcommand: None, command: program, config: cfg }
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum CppTool {
|
||||
@@ -249,7 +249,7 @@ fn push_line(out: &mut String, line: &str) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
fn ctx<'a>(
|
||||
program: &'a str,
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! Container and cloud command output filters.
|
||||
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
@@ -168,7 +168,7 @@ fn head_tail_dedup(input: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
#[test]
|
||||
fn dedups_repeated_log_lines_before_truncation() {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! .NET CLI output filters.
|
||||
|
||||
use crate::shell::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
program == "dotnet" && matches!(subcommand, Some("build" | "test" | "restore" | "format"))
|
||||
@@ -326,7 +326,7 @@ fn contains_diagnostic_signal(lower: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::minimizer::MinimizerConfig;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
#[test]
|
||||
fn keeps_dotnet_build_diagnostic_and_strips_restore_noise() {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user