fzf-gnat 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fzf_gnat-0.1.1/MANIFEST.in +3 -0
- fzf_gnat-0.1.1/PKG-INFO +160 -0
- fzf_gnat-0.1.1/README.md +145 -0
- fzf_gnat-0.1.1/packaging/build_deb.py +70 -0
- fzf_gnat-0.1.1/pyproject.toml +22 -0
- fzf_gnat-0.1.1/setup.cfg +4 -0
- fzf_gnat-0.1.1/src/fzf_gnat/__init__.py +1 -0
- fzf_gnat-0.1.1/src/fzf_gnat/cli.py +220 -0
- fzf_gnat-0.1.1/src/fzf_gnat/index.py +237 -0
- fzf_gnat-0.1.1/src/fzf_gnat/names.py +238 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/PKG-INFO +160 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/SOURCES.txt +19 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/dependency_links.txt +1 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/entry_points.txt +2 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/requires.txt +9 -0
- fzf_gnat-0.1.1/src/fzf_gnat.egg-info/top_level.txt +1 -0
- fzf_gnat-0.1.1/tests/conftest.py +13 -0
- fzf_gnat-0.1.1/tests/test_cli.py +317 -0
- fzf_gnat-0.1.1/tests/test_index.py +290 -0
- fzf_gnat-0.1.1/tests/test_names.py +85 -0
- fzf_gnat-0.1.1/tests/test_symbols.py +129 -0
fzf_gnat-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fzf-gnat
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fuzzy search Ada sources by unit name and krunched file name
|
|
5
|
+
Author-email: Zakaria Fadli <zakaria1193@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: tree-sitter>=0.23
|
|
9
|
+
Requires-Dist: tree-sitter-ada>=0.1
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
12
|
+
Provides-Extra: release
|
|
13
|
+
Requires-Dist: build>=1; extra == "release"
|
|
14
|
+
Requires-Dist: twine>=6; extra == "release"
|
|
15
|
+
|
|
16
|
+
# fzf-gnat
|
|
17
|
+
|
|
18
|
+
Search Ada source trees by unit name, file name, or declaration with fzf.
|
|
19
|
+
Find `Ada.Strings.Unbounded` even when GNAT names its source file `a-strunb.ads`.
|
|
20
|
+
Preview the source, then open your selection in `$EDITOR`.
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
Run these commands from an Ada source tree, or append the directory you want to search.
|
|
25
|
+
|
|
26
|
+
### Find a unit by its Ada name
|
|
27
|
+
|
|
28
|
+
You know the unit is `Ada.Strings.Unbounded`, but its file is named `a-strunb.ads`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
fzf-gnat -q Ada.Strings.Unbounded
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The list shows the unit name beside the file name, so either spelling works:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
fzf-gnat -q strunb
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Find a declaration without knowing its file
|
|
41
|
+
|
|
42
|
+
Search for `Append` in package specs:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
fzf-gnat -ss -q Ada.Strings.Unbounded.Append
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Preview each declaration, then press Enter to open it in your editor.
|
|
49
|
+
The editor starts at the declaration when it supports line arguments.
|
|
50
|
+
Use `-s` instead of `-ss` to include declarations in package bodies.
|
|
51
|
+
|
|
52
|
+
### Find local declarations
|
|
53
|
+
|
|
54
|
+
Include local variables and nested subprograms when searching for a name such as `Count`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
fzf-gnat -sa -q Count
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Search another source tree
|
|
61
|
+
|
|
62
|
+
Pass a directory to search without changing your working directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
fzf-gnat -ss -q Ada.Strings.Unbounded.Append ~/work/toolchain/gnat
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
Requires Python 3.10 or later and `fzf` on your `PATH`.
|
|
71
|
+
On Debian or Ubuntu, install fzf with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
sudo apt install fzf
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After the first PyPI release, install with pip:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install fzf-gnat
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or use pipx:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pipx install fzf-gnat
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For a local checkout, use `pip install .` or `pipx install .`.
|
|
90
|
+
Install `bat` for syntax highlighting in the preview. Without it, the preview uses `cat`.
|
|
91
|
+
|
|
92
|
+
## Search files
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
fzf-gnat ~/work/toolchain/gnat
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Each row shows the file name, its kind (spec, body, or subunit), the unit name, and the path.
|
|
99
|
+
Search any field, for example `strunb` or `Strings.Unb`.
|
|
100
|
+
Press Enter to open a file in `$EDITOR`. The default editor is `vi`.
|
|
101
|
+
Press Tab to select several files.
|
|
102
|
+
|
|
103
|
+
The first run parses the source tree with tree-sitter-ada.
|
|
104
|
+
Later runs parse only files that changed.
|
|
105
|
+
Use `--rebuild` to parse everything again.
|
|
106
|
+
Use `--print` to print the index without fzf.
|
|
107
|
+
Use `-v` to show warnings.
|
|
108
|
+
A file that contains only `pragma No_Body;` has no unit and shows as `?`.
|
|
109
|
+
|
|
110
|
+
## Search declarations
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
fzf-gnat -s ~/work/toolchain/gnat
|
|
114
|
+
fzf-gnat -ss ~/work/toolchain/gnat
|
|
115
|
+
fzf-gnat -sa ~/work/toolchain/gnat
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
* `-s` searches package-level declarations in specs and bodies.
|
|
119
|
+
* `-ss` searches package-level declarations in specs only.
|
|
120
|
+
* `-sa` searches every declaration, including local declarations.
|
|
121
|
+
|
|
122
|
+
Each row shows the qualified name, kind, unit, and file.
|
|
123
|
+
Press Enter to open the editor at the declaration when the editor supports line arguments.
|
|
124
|
+
Use `-q TEXT` to start fzf with an initial query.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python3 -m venv .venv
|
|
130
|
+
.venv/bin/python -m pip install -e '.[dev,release]'
|
|
131
|
+
.venv/bin/python -m pytest
|
|
132
|
+
.venv/bin/python -m build
|
|
133
|
+
.venv/bin/python -m twine check --strict dist/*.whl dist/*.tar.gz
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The build produces a source archive and a wheel in `dist/`.
|
|
137
|
+
|
|
138
|
+
## Publish to PyPI
|
|
139
|
+
|
|
140
|
+
The GitHub Actions workflow `.github/workflows/publish.yml` tests pushes and pull requests on Python 3.10, 3.12, and 3.14.
|
|
141
|
+
It builds and checks both distributions after the tests pass.
|
|
142
|
+
It tests the installed wheel and checks installation with pipx before publication.
|
|
143
|
+
A manual workflow run builds artifacts without publishing.
|
|
144
|
+
A published GitHub release publishes to PyPI unless GitHub marks it as a prerelease.
|
|
145
|
+
|
|
146
|
+
Before the first release:
|
|
147
|
+
|
|
148
|
+
1. Host this repository on GitHub.
|
|
149
|
+
2. Sign in to the PyPI account associated with `zakaria1193@gmail.com`.
|
|
150
|
+
3. Register a pending trusted publisher for `fzf-gnat` on PyPI.
|
|
151
|
+
4. Enter the GitHub owner, repository name, workflow filename `publish.yml`, and environment name `pypi`.
|
|
152
|
+
5. Create the `pypi` environment in the GitHub repository.
|
|
153
|
+
|
|
154
|
+
See the [PyPI trusted publisher instructions](https://docs.pypi.org/trusted-publishers/adding-a-publisher/).
|
|
155
|
+
This method does not require a stored PyPI API token.
|
|
156
|
+
The author email in the package metadata does not authenticate uploads.
|
|
157
|
+
|
|
158
|
+
For each release, update `version` in `pyproject.toml` and publish a matching GitHub release.
|
|
159
|
+
For version `0.1.0`, use the tag `v0.1.0`.
|
|
160
|
+
PyPI does not permit replacement of an existing release file.
|
fzf_gnat-0.1.1/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# fzf-gnat
|
|
2
|
+
|
|
3
|
+
Search Ada source trees by unit name, file name, or declaration with fzf.
|
|
4
|
+
Find `Ada.Strings.Unbounded` even when GNAT names its source file `a-strunb.ads`.
|
|
5
|
+
Preview the source, then open your selection in `$EDITOR`.
|
|
6
|
+
|
|
7
|
+
## Examples
|
|
8
|
+
|
|
9
|
+
Run these commands from an Ada source tree, or append the directory you want to search.
|
|
10
|
+
|
|
11
|
+
### Find a unit by its Ada name
|
|
12
|
+
|
|
13
|
+
You know the unit is `Ada.Strings.Unbounded`, but its file is named `a-strunb.ads`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
fzf-gnat -q Ada.Strings.Unbounded
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The list shows the unit name beside the file name, so either spelling works:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
fzf-gnat -q strunb
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Find a declaration without knowing its file
|
|
26
|
+
|
|
27
|
+
Search for `Append` in package specs:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
fzf-gnat -ss -q Ada.Strings.Unbounded.Append
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Preview each declaration, then press Enter to open it in your editor.
|
|
34
|
+
The editor starts at the declaration when it supports line arguments.
|
|
35
|
+
Use `-s` instead of `-ss` to include declarations in package bodies.
|
|
36
|
+
|
|
37
|
+
### Find local declarations
|
|
38
|
+
|
|
39
|
+
Include local variables and nested subprograms when searching for a name such as `Count`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
fzf-gnat -sa -q Count
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Search another source tree
|
|
46
|
+
|
|
47
|
+
Pass a directory to search without changing your working directory:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
fzf-gnat -ss -q Ada.Strings.Unbounded.Append ~/work/toolchain/gnat
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
Requires Python 3.10 or later and `fzf` on your `PATH`.
|
|
56
|
+
On Debian or Ubuntu, install fzf with:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sudo apt install fzf
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
After the first PyPI release, install with pip:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install fzf-gnat
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Or use pipx:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pipx install fzf-gnat
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For a local checkout, use `pip install .` or `pipx install .`.
|
|
75
|
+
Install `bat` for syntax highlighting in the preview. Without it, the preview uses `cat`.
|
|
76
|
+
|
|
77
|
+
## Search files
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
fzf-gnat ~/work/toolchain/gnat
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Each row shows the file name, its kind (spec, body, or subunit), the unit name, and the path.
|
|
84
|
+
Search any field, for example `strunb` or `Strings.Unb`.
|
|
85
|
+
Press Enter to open a file in `$EDITOR`. The default editor is `vi`.
|
|
86
|
+
Press Tab to select several files.
|
|
87
|
+
|
|
88
|
+
The first run parses the source tree with tree-sitter-ada.
|
|
89
|
+
Later runs parse only files that changed.
|
|
90
|
+
Use `--rebuild` to parse everything again.
|
|
91
|
+
Use `--print` to print the index without fzf.
|
|
92
|
+
Use `-v` to show warnings.
|
|
93
|
+
A file that contains only `pragma No_Body;` has no unit and shows as `?`.
|
|
94
|
+
|
|
95
|
+
## Search declarations
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
fzf-gnat -s ~/work/toolchain/gnat
|
|
99
|
+
fzf-gnat -ss ~/work/toolchain/gnat
|
|
100
|
+
fzf-gnat -sa ~/work/toolchain/gnat
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
* `-s` searches package-level declarations in specs and bodies.
|
|
104
|
+
* `-ss` searches package-level declarations in specs only.
|
|
105
|
+
* `-sa` searches every declaration, including local declarations.
|
|
106
|
+
|
|
107
|
+
Each row shows the qualified name, kind, unit, and file.
|
|
108
|
+
Press Enter to open the editor at the declaration when the editor supports line arguments.
|
|
109
|
+
Use `-q TEXT` to start fzf with an initial query.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python3 -m venv .venv
|
|
115
|
+
.venv/bin/python -m pip install -e '.[dev,release]'
|
|
116
|
+
.venv/bin/python -m pytest
|
|
117
|
+
.venv/bin/python -m build
|
|
118
|
+
.venv/bin/python -m twine check --strict dist/*.whl dist/*.tar.gz
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The build produces a source archive and a wheel in `dist/`.
|
|
122
|
+
|
|
123
|
+
## Publish to PyPI
|
|
124
|
+
|
|
125
|
+
The GitHub Actions workflow `.github/workflows/publish.yml` tests pushes and pull requests on Python 3.10, 3.12, and 3.14.
|
|
126
|
+
It builds and checks both distributions after the tests pass.
|
|
127
|
+
It tests the installed wheel and checks installation with pipx before publication.
|
|
128
|
+
A manual workflow run builds artifacts without publishing.
|
|
129
|
+
A published GitHub release publishes to PyPI unless GitHub marks it as a prerelease.
|
|
130
|
+
|
|
131
|
+
Before the first release:
|
|
132
|
+
|
|
133
|
+
1. Host this repository on GitHub.
|
|
134
|
+
2. Sign in to the PyPI account associated with `zakaria1193@gmail.com`.
|
|
135
|
+
3. Register a pending trusted publisher for `fzf-gnat` on PyPI.
|
|
136
|
+
4. Enter the GitHub owner, repository name, workflow filename `publish.yml`, and environment name `pypi`.
|
|
137
|
+
5. Create the `pypi` environment in the GitHub repository.
|
|
138
|
+
|
|
139
|
+
See the [PyPI trusted publisher instructions](https://docs.pypi.org/trusted-publishers/adding-a-publisher/).
|
|
140
|
+
This method does not require a stored PyPI API token.
|
|
141
|
+
The author email in the package metadata does not authenticate uploads.
|
|
142
|
+
|
|
143
|
+
For each release, update `version` in `pyproject.toml` and publish a matching GitHub release.
|
|
144
|
+
For version `0.1.0`, use the tag `v0.1.0`.
|
|
145
|
+
PyPI does not permit replacement of an existing release file.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Build a Debian archive with private Python dependencies for the current interpreter."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from email.parser import BytesParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
NAME = "fzf-gnat"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
18
|
+
parser.add_argument("--output", type=Path, default=ROOT / "dist")
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
args.output.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
architecture = subprocess.check_output(["dpkg", "--print-architecture"], text=True).strip()
|
|
22
|
+
python = f"python{sys.version_info.major}.{sys.version_info.minor}"
|
|
23
|
+
with tempfile.TemporaryDirectory(prefix=f"{NAME}-") as temp:
|
|
24
|
+
stage = Path(temp) / "package"
|
|
25
|
+
library = stage / "usr/lib" / NAME
|
|
26
|
+
subprocess.run(
|
|
27
|
+
[sys.executable, "-m", "pip", "install", "--no-compile", "--target", str(library), str(ROOT)],
|
|
28
|
+
check=True,
|
|
29
|
+
)
|
|
30
|
+
shutil.rmtree(library / "bin", ignore_errors=True)
|
|
31
|
+
metadata = BytesParser().parsebytes(
|
|
32
|
+
next(library.glob("fzf_gnat-*.dist-info/METADATA")).read_bytes()
|
|
33
|
+
)
|
|
34
|
+
version = metadata["Version"]
|
|
35
|
+
binary = stage / "usr/bin" / NAME
|
|
36
|
+
binary.parent.mkdir(parents=True)
|
|
37
|
+
binary.write_text(
|
|
38
|
+
f"#!/usr/bin/{python} -I\n"
|
|
39
|
+
"import sys\n"
|
|
40
|
+
"from pathlib import Path\n"
|
|
41
|
+
f"sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'lib/{NAME}'))\n"
|
|
42
|
+
"from fzf_gnat.cli import main\n"
|
|
43
|
+
"sys.exit(main())\n"
|
|
44
|
+
)
|
|
45
|
+
binary.chmod(0o755)
|
|
46
|
+
documentation = stage / "usr/share/doc" / NAME
|
|
47
|
+
documentation.mkdir(parents=True)
|
|
48
|
+
shutil.copyfile(ROOT / "README.md", documentation / "README.md")
|
|
49
|
+
control = stage / "DEBIAN/control"
|
|
50
|
+
control.parent.mkdir()
|
|
51
|
+
control.write_text(
|
|
52
|
+
f"Package: {NAME}\n"
|
|
53
|
+
f"Version: {version}\n"
|
|
54
|
+
"Section: devel\n"
|
|
55
|
+
"Priority: optional\n"
|
|
56
|
+
f"Architecture: {architecture}\n"
|
|
57
|
+
"Maintainer: Zakaria Fadli <zakaria1193@gmail.com>\n"
|
|
58
|
+
f"Depends: {python}, libc6, fzf\n"
|
|
59
|
+
"Recommends: bat\n"
|
|
60
|
+
"Description: Fuzzy search Ada sources by unit name and declaration\n"
|
|
61
|
+
" Search GNAT source trees by unit name, shortened file name, or declaration.\n"
|
|
62
|
+
" Python parser dependencies are included in a private directory.\n"
|
|
63
|
+
)
|
|
64
|
+
archive = args.output.resolve() / f"{NAME}_{version}_{architecture}.deb"
|
|
65
|
+
subprocess.run(["dpkg-deb", "--root-owner-group", "--build", str(stage), str(archive)], check=True)
|
|
66
|
+
print(archive)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fzf-gnat"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Fuzzy search Ada sources by unit name and krunched file name"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Zakaria Fadli", email = "zakaria1193@gmail.com"}]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["tree-sitter>=0.23", "tree-sitter-ada>=0.1"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = ["pytest>=7"]
|
|
16
|
+
release = ["build>=1", "twine>=6"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
fzf-gnat = "fzf_gnat.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
fzf_gnat-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Fuzzy search an Ada tree by unit name and krunched file name; start at ``cli.main``."""
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""The ``fzf-gnat`` command."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import signal
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from fzf_gnat.index import Entry, SymbolEntry, build_index, build_symbol_index
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProgressBar:
|
|
17
|
+
"""Draw a one-line progress bar on a terminal, and erase it when done."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, out, width: int = 40):
|
|
20
|
+
self.out = out
|
|
21
|
+
self.width = width
|
|
22
|
+
|
|
23
|
+
def __call__(self, done: int, total: int) -> None:
|
|
24
|
+
filled = self.width * done // total
|
|
25
|
+
bar = "#" * filled + " " * (self.width - filled)
|
|
26
|
+
self.out.write(f"\r[{bar}] {done:4}/{total} ")
|
|
27
|
+
if done == total:
|
|
28
|
+
self.out.write("\r\x1b[K")
|
|
29
|
+
self.out.flush()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT_COLUMNS = 120
|
|
33
|
+
|
|
34
|
+
# fzf's pointer and marker take two columns on the left, its scrollbar one on
|
|
35
|
+
# the right.
|
|
36
|
+
FZF_GUTTER = 3
|
|
37
|
+
|
|
38
|
+
MIN_PATH_WIDTH = 20
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def shorten(path: str, width: int) -> str:
|
|
42
|
+
"""Return ``path``, cut in the middle with ``...`` if it is longer than ``width``."""
|
|
43
|
+
if len(path) <= width:
|
|
44
|
+
return path
|
|
45
|
+
# Keep more of the end, which holds the directory and file names.
|
|
46
|
+
keep = width - 3
|
|
47
|
+
head = keep // 3
|
|
48
|
+
return f"{path[:head]}...{path[len(path) - (keep - head):]}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _columns(rows: list[tuple[str, str, str, Path, int]], columns: int) -> list[str]:
|
|
52
|
+
# The first three columns take their longest value and the shown path
|
|
53
|
+
# gets what is left of the line. The full path and the line follow,
|
|
54
|
+
# hidden, for the preview and the editor. fzf runs with --tabstop 1, so
|
|
55
|
+
# each tab shows as one space.
|
|
56
|
+
widths = [max((len(row[i]) for row in rows), default=0) for i in range(3)]
|
|
57
|
+
path_width = max(MIN_PATH_WIDTH, columns - FZF_GUTTER - sum(widths) - 3)
|
|
58
|
+
return [
|
|
59
|
+
f"{a:<{widths[0]}}\t{b:<{widths[1]}}\t{c:<{widths[2]}}\t"
|
|
60
|
+
f"{shorten(str(path), path_width):<{path_width}}\t{path}\t{line}"
|
|
61
|
+
for a, b, c, path, line in rows
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def format_lines(entries: list[Entry], root: Path, columns: int = DEFAULT_COLUMNS) -> list[str]:
|
|
66
|
+
"""Return one fzf line per entry, ``columns`` wide: file name, kind, unit and shortened path, then the hidden full path and unit line."""
|
|
67
|
+
return _columns(
|
|
68
|
+
[(Path(e.relpath).name, e.kind, e.unit, root / e.relpath, e.line) for e in entries],
|
|
69
|
+
columns,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_symbol_lines(
|
|
74
|
+
entries: list[SymbolEntry], root: Path, columns: int = DEFAULT_COLUMNS
|
|
75
|
+
) -> list[str]:
|
|
76
|
+
"""Return one fzf line per symbol, ``columns`` wide: name, kind, unit and shortened path, then the hidden full path and line."""
|
|
77
|
+
return _columns(
|
|
78
|
+
[(e.name, e.kind, e.unit, root / e.relpath, e.line) for e in entries],
|
|
79
|
+
columns,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def preview_command() -> str:
|
|
84
|
+
"""Return the fzf preview command, using bat when it is installed."""
|
|
85
|
+
# Debian and Ubuntu install bat as batcat.
|
|
86
|
+
for bat in ("bat", "batcat"):
|
|
87
|
+
if shutil.which(bat):
|
|
88
|
+
return f"{bat} --color=always --style=numbers {{5}} --highlight-line {{6}}"
|
|
89
|
+
return "cat {5}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def fzf_argv(preview: str, query: str = "") -> list[str]:
|
|
93
|
+
return [
|
|
94
|
+
"fzf",
|
|
95
|
+
"--multi",
|
|
96
|
+
"--delimiter", "\t",
|
|
97
|
+
"--tabstop", "1",
|
|
98
|
+
"--preview", preview,
|
|
99
|
+
# Fields 5 and 6, the full path and the line, stay hidden; the line
|
|
100
|
+
# centres the preview.
|
|
101
|
+
"--with-nth", "1..4",
|
|
102
|
+
"--preview-window", "up,60%,+{6}/2",
|
|
103
|
+
"--query", query,
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Editors that take +LINE before a file. With several files, it applies to
|
|
108
|
+
# the first one only.
|
|
109
|
+
_PLUS_LINE_EDITORS = {"vi", "vim", "nvim", "gvim", "view", "nano", "emacs", "emacsclient", "micro"}
|
|
110
|
+
_GOTO_EDITORS = {"code", "codium"}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def editor_argv(files: list[tuple[str, int]]) -> list[str]:
|
|
114
|
+
"""Return the $EDITOR command that opens ``files``, at the given line of the first one when the editor supports it."""
|
|
115
|
+
editor = shlex.split(os.environ.get("EDITOR") or "vi")
|
|
116
|
+
name = os.path.basename(editor[0])
|
|
117
|
+
(first, line), rest = files[0], [path for path, _ in files[1:]]
|
|
118
|
+
if name in _PLUS_LINE_EDITORS:
|
|
119
|
+
return [*editor, f"+{line}", first, *rest]
|
|
120
|
+
if name in _GOTO_EDITORS:
|
|
121
|
+
return [*editor, "-g", f"{first}:{line}", *rest]
|
|
122
|
+
return [*editor, first, *rest]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv: list[str] | None = None) -> int:
|
|
126
|
+
# Python keeps SIGINT ignored when the parent ignored it, which leaves
|
|
127
|
+
# Ctrl-C dead during a long parse.
|
|
128
|
+
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
129
|
+
try:
|
|
130
|
+
return _main(argv)
|
|
131
|
+
except KeyboardInterrupt:
|
|
132
|
+
if sys.stderr.isatty():
|
|
133
|
+
sys.stderr.write("\r\x1b[K")
|
|
134
|
+
return 130
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _main(argv: list[str] | None) -> int:
|
|
138
|
+
parser = argparse.ArgumentParser(
|
|
139
|
+
prog="fzf-gnat",
|
|
140
|
+
description="Fuzzy search Ada sources by unit name and krunched file name, or by declaration.",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument("root", nargs="?", default=".", help="tree to search (default: .)")
|
|
143
|
+
parser.add_argument("--rebuild", action="store_true", help="ignore the cache")
|
|
144
|
+
parser.add_argument("--print", action="store_true", help="print the index, do not run fzf")
|
|
145
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="show warnings")
|
|
146
|
+
parser.add_argument("-q", "--query", default="", help="start fzf with this query")
|
|
147
|
+
symbols = parser.add_mutually_exclusive_group()
|
|
148
|
+
symbols.add_argument(
|
|
149
|
+
"-s", dest="symbols", action="store_const", const="package",
|
|
150
|
+
help="search declarations at package level instead of files",
|
|
151
|
+
)
|
|
152
|
+
symbols.add_argument(
|
|
153
|
+
"-ss", dest="symbols", action="store_const", const="spec",
|
|
154
|
+
help="search declarations at package level in specs only",
|
|
155
|
+
)
|
|
156
|
+
symbols.add_argument(
|
|
157
|
+
"-sa", dest="symbols", action="store_const", const="all",
|
|
158
|
+
help="search all declarations, including locals",
|
|
159
|
+
)
|
|
160
|
+
args = parser.parse_args(argv)
|
|
161
|
+
tty = sys.stderr.isatty()
|
|
162
|
+
# On a terminal, a warning first erases the progress bar it would
|
|
163
|
+
# otherwise be printed over.
|
|
164
|
+
erase = "\r\x1b[K" if tty else ""
|
|
165
|
+
logging.basicConfig(
|
|
166
|
+
level=logging.WARNING if args.verbose else logging.ERROR,
|
|
167
|
+
format=f"{erase}fzf-gnat: %(message)s",
|
|
168
|
+
force=True,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
root = Path(args.root).resolve()
|
|
172
|
+
if not root.is_dir():
|
|
173
|
+
print(f"fzf-gnat: {args.root}: not a directory", file=sys.stderr)
|
|
174
|
+
return 2
|
|
175
|
+
if not args.print and not shutil.which("fzf"):
|
|
176
|
+
print("fzf-gnat: fzf not found on PATH", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
|
|
179
|
+
progress = ProgressBar(sys.stderr) if tty else None
|
|
180
|
+
columns = os.get_terminal_size(sys.stderr.fileno()).columns if tty else DEFAULT_COLUMNS
|
|
181
|
+
if args.symbols:
|
|
182
|
+
found = build_symbol_index(
|
|
183
|
+
root, local=args.symbols == "all", rebuild=args.rebuild, progress=progress
|
|
184
|
+
)
|
|
185
|
+
if args.symbols == "spec":
|
|
186
|
+
found = [e for e in found if e.relpath.endswith(".ads")]
|
|
187
|
+
formatted = format_symbol_lines(found, root, columns)
|
|
188
|
+
else:
|
|
189
|
+
entries = build_index(root, rebuild=args.rebuild, progress=progress)
|
|
190
|
+
formatted = format_lines(entries, root, columns)
|
|
191
|
+
lines = "".join(f"{line}\n" for line in formatted)
|
|
192
|
+
if args.print:
|
|
193
|
+
try:
|
|
194
|
+
sys.stdout.write(lines)
|
|
195
|
+
sys.stdout.flush()
|
|
196
|
+
except BrokenPipeError:
|
|
197
|
+
# The reader quit early, as `head` does. Point stdout at devnull
|
|
198
|
+
# so that the flush at interpreter exit does not fail again.
|
|
199
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
fzf_argv(preview_command(), args.query), input=lines, stdout=subprocess.PIPE, text=True, cwd=root
|
|
204
|
+
)
|
|
205
|
+
if result.returncode != 0:
|
|
206
|
+
return 0
|
|
207
|
+
selected = [
|
|
208
|
+
(fields[4], int(fields[5]))
|
|
209
|
+
for fields in (line.split("\t") for line in result.stdout.splitlines())
|
|
210
|
+
if len(fields) == 6
|
|
211
|
+
]
|
|
212
|
+
if not selected:
|
|
213
|
+
return 0
|
|
214
|
+
editor = editor_argv(selected)
|
|
215
|
+
try:
|
|
216
|
+
subprocess.run(editor)
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
print(f"fzf-gnat: editor not found: {editor[0]}", file=sys.stderr)
|
|
219
|
+
return 2
|
|
220
|
+
return 0
|