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.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include packaging/build_deb.py
3
+ recursive-include tests *.py
@@ -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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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