summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/project-ctags403
-rw-r--r--config.el193
-rw-r--r--config.org193
3 files changed, 489 insertions, 300 deletions
diff --git a/bin/project-ctags b/bin/project-ctags
new file mode 100755
index 0000000..4fccd90
--- /dev/null
+++ b/bin/project-ctags
@@ -0,0 +1,403 @@
+#!/usr/bin/env python3
+import argparse
+import ast
+import json
+import os
+import shlex
+import shutil
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+IGNORED_DIRECTORIES = {
+ ".direnv",
+ ".git",
+ ".hg",
+ ".mypy_cache",
+ ".pixi",
+ ".pytest_cache",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ "__pycache__",
+ "build",
+ "dist",
+ "node_modules",
+ "target",
+ "test",
+ "tests",
+}
+PYTHON_SUFFIXES = {".py", ".pyi"}
+C_SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".m", ".mm"}
+C_HEADER_SUFFIXES = {".h", ".hh", ".hpp", ".hxx"}
+NIX_CTAGS_OPTIONS = [
+ "--langdef=nix",
+ "--map-nix=+.nix",
+ "--kinddef-nix=a,attribute,attributes",
+ "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*(\\.[A-Za-z_][A-Za-z0-9_'-]+)+)[[:space:]]*=[^=]/\\1/a/",
+ "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*)[[:space:]]*=[^=]/\\1/a/",
+]
+PYTHON_RESOLVER = """
+import importlib.util
+import json
+import pathlib
+import sysconfig
+import sys
+
+resolved = {}
+stdlib = pathlib.Path(sysconfig.get_path("stdlib"))
+for name in sys.argv[1:]:
+ try:
+ spec = importlib.util.find_spec(name)
+ except (ImportError, ModuleNotFoundError, ValueError):
+ spec = None
+ paths = []
+ if spec is not None and spec.submodule_search_locations:
+ paths.extend(spec.submodule_search_locations)
+ elif spec is not None and spec.origin not in (None, "built-in", "frozen"):
+ paths.append(spec.origin)
+ elif spec is not None and spec.origin == "frozen":
+ module_file = stdlib / (name + ".py")
+ package_file = stdlib / name / "__init__.py"
+ if module_file.is_file():
+ paths.append(str(module_file))
+ elif package_file.is_file():
+ paths.append(str(package_file.parent))
+ resolved[name] = paths if spec is not None else None
+print(json.dumps(resolved))
+"""
+
+
+def parse_arguments():
+ parser = argparse.ArgumentParser(
+ description="Generate one Citre-compatible .tags file for project code and used dependencies."
+ )
+ parser.add_argument("root", nargs="?", default=".")
+ parser.add_argument("--output", default=".tags")
+ parser.add_argument("--ctags")
+ parser.add_argument("--python")
+ parser.add_argument("--python-command")
+ parser.add_argument("--source", action="append", default=[])
+ parser.add_argument("--no-python-deps", action="store_true")
+ parser.add_argument("--no-c-deps", action="store_true")
+ return parser.parse_args()
+
+
+def resolved_path(value, base):
+ expanded = os.path.expandvars(os.path.expanduser(value))
+ path = Path(expanded)
+ if not path.is_absolute():
+ path = base / path
+ return path.resolve()
+
+
+def walk_files(path, suffixes=None):
+ if path.is_file():
+ if suffixes is None or path.suffix.lower() in suffixes:
+ return [path.resolve()]
+ return []
+ if not path.is_dir():
+ return []
+ files = []
+ for directory, names, filenames in os.walk(path):
+ names[:] = [name for name in names if name not in IGNORED_DIRECTORIES]
+ parent = Path(directory)
+ for filename in filenames:
+ candidate = parent / filename
+ if suffixes is None or candidate.suffix.lower() in suffixes:
+ files.append(candidate.resolve())
+ return files
+
+
+def git_project_files(root):
+ git = shutil.which("git")
+ if git is None:
+ return None
+ result = subprocess.run(
+ [git, "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=False,
+ )
+ if result.returncode != 0:
+ return None
+ files = []
+ for name in result.stdout.split(b"\0"):
+ if not name:
+ continue
+ path = root / os.fsdecode(name)
+ if path.is_file():
+ files.append(path.resolve())
+ return files
+
+
+def project_files(root, output):
+ files = git_project_files(root)
+ if files is None:
+ files = walk_files(root)
+ return sorted(path for path in files if path != output)
+
+
+def imported_modules(files):
+ modules = set()
+ for path in files:
+ if path.suffix.lower() not in PYTHON_SUFFIXES:
+ continue
+ try:
+ tree = ast.parse(path.read_bytes(), filename=str(path))
+ except (OSError, SyntaxError, UnicodeError):
+ continue
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ modules.update(alias.name.partition(".")[0] for alias in node.names)
+ elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
+ modules.add(node.module.partition(".")[0])
+ return sorted(modules)
+
+
+def python_command(args, root):
+ if args.python_command:
+ return shlex.split(args.python_command)
+ environment_command = os.environ.get("PROJECT_CTAGS_PYTHON_COMMAND")
+ if environment_command:
+ return shlex.split(environment_command)
+ if args.python:
+ path = Path(os.path.expandvars(os.path.expanduser(args.python)))
+ if not path.is_absolute():
+ path = root / path
+ return [str(path.absolute())]
+ virtual_environment = os.environ.get("VIRTUAL_ENV")
+ candidates = []
+ if virtual_environment:
+ candidates.append(Path(virtual_environment) / "bin" / "python")
+ candidates.extend(
+ [
+ root / ".venv" / "bin" / "python",
+ root / ".devenv" / "state" / "venv" / "bin" / "python",
+ root / ".pixi" / "envs" / "default" / "bin" / "python",
+ ]
+ )
+ for candidate in candidates:
+ if candidate.is_file() and os.access(candidate, os.X_OK):
+ return [str(candidate.absolute())]
+ uv = shutil.which("uv")
+ if uv and (root / "uv.lock").exists():
+ return [uv, "run", "--no-sync", "python"]
+ python = shutil.which("python3") or shutil.which("python")
+ if python:
+ return [python]
+ return None
+
+
+def python_dependency_files(args, root, files):
+ modules = imported_modules(files)
+ if not modules:
+ return [], None, []
+ command = python_command(args, root)
+ if command is None:
+ return [], None, modules
+ result = subprocess.run(
+ command + ["-c", PYTHON_RESOLVER, *modules],
+ cwd=root,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if result.returncode != 0:
+ print(result.stderr.rstrip(), file=sys.stderr)
+ return [], shlex.join(command), modules
+ try:
+ resolved = json.loads(result.stdout)
+ except json.JSONDecodeError:
+ print(f"Could not read module paths from {shlex.join(command)}", file=sys.stderr)
+ return [], shlex.join(command), modules
+ dependency_files = set()
+ unresolved = []
+ for module, paths in resolved.items():
+ if paths is None:
+ unresolved.append(module)
+ continue
+ module_files = []
+ for value in paths:
+ path = Path(value).resolve()
+ module_files.extend(walk_files(path, PYTHON_SUFFIXES))
+ dependency_files.update(module_files)
+ return sorted(dependency_files), shlex.join(command), unresolved
+
+
+def compilation_database(root):
+ path = root / "compile_commands.json"
+ if not path.is_file():
+ return {}
+ try:
+ entries = json.loads(path.read_text())
+ except (OSError, json.JSONDecodeError):
+ return {}
+ database = {}
+ for entry in entries:
+ directory = resolved_path(entry.get("directory", root), root)
+ source = resolved_path(entry["file"], directory)
+ arguments = entry.get("arguments")
+ if arguments is None and entry.get("command"):
+ arguments = shlex.split(entry["command"])
+ if arguments:
+ database[source] = (directory, arguments)
+ return database
+
+
+def dependency_command(source, root, database):
+ entry = database.get(source)
+ if entry is None:
+ compiler = shlex.split(os.environ.get("CC", "cc"))
+ flags = shlex.split(os.environ.get("CPPFLAGS", ""))
+ flags.extend(shlex.split(os.environ.get("CFLAGS", "")))
+ return root, compiler + ["-M", "-MG", *flags, str(source)]
+ directory, arguments = entry
+ compiler = arguments[0]
+ flags = []
+ skip_next = False
+ options_with_values = {"-MF", "-MT", "-MQ", "-o"}
+ for argument in arguments[1:]:
+ if skip_next:
+ skip_next = False
+ continue
+ if argument in options_with_values:
+ skip_next = True
+ continue
+ if argument == "-c" or argument.startswith("-M"):
+ continue
+ candidate = resolved_path(argument, directory)
+ if candidate == source:
+ continue
+ flags.append(argument)
+ return directory, [compiler, "-M", "-MG", *flags, str(source)]
+
+
+def make_dependencies(output, directory):
+ flattened = output.replace("\\\n", " ")
+ if ":" not in flattened:
+ return []
+ dependencies = flattened.split(":", 1)[1]
+ paths = []
+ for value in shlex.split(dependencies):
+ path = resolved_path(value, directory)
+ if path.is_file() and path.suffix.lower() in C_HEADER_SUFFIXES:
+ paths.append(path)
+ return paths
+
+
+def c_dependency_files(root, files):
+ sources = [path for path in files if path.suffix.lower() in C_SOURCE_SUFFIXES]
+ if not sources:
+ return []
+ database = compilation_database(root)
+ headers = set()
+ for source in sources:
+ directory, command = dependency_command(source, root, database)
+ result = subprocess.run(
+ command,
+ cwd=directory,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if result.returncode == 0:
+ headers.update(make_dependencies(result.stdout, directory))
+ elif result.stderr:
+ print(result.stderr.rstrip(), file=sys.stderr)
+ return sorted(headers)
+
+
+def extra_source_files(args, root):
+ values = list(args.source)
+ environment_sources = os.environ.get("PROJECT_CTAGS_SOURCES")
+ if environment_sources:
+ values.extend(value for value in environment_sources.split(os.pathsep) if value)
+ files = set()
+ for value in values:
+ path = resolved_path(value, root)
+ if not path.exists():
+ print(f"Extra source does not exist: {path}", file=sys.stderr)
+ continue
+ files.update(walk_files(path))
+ return sorted(files)
+
+
+def generate_tags(ctags, output, files):
+ output.parent.mkdir(parents=True, exist_ok=True)
+ list_file = tempfile.NamedTemporaryFile(mode="w", prefix="project-ctags-", delete=False)
+ temporary_output = tempfile.NamedTemporaryFile(
+ prefix=f".{output.name}.", dir=output.parent, delete=False
+ )
+ temporary_output.close()
+ try:
+ for path in files:
+ list_file.write(f"{path}\n")
+ list_file.close()
+ command = [
+ ctags,
+ "--options=NONE",
+ "--output-format=e-ctags",
+ *NIX_CTAGS_OPTIONS,
+ "--languages=all",
+ "--kinds-all=*",
+ "--fields=*",
+ "--extras=*",
+ "-f",
+ temporary_output.name,
+ "-L",
+ list_file.name,
+ ]
+ result = subprocess.run(command, check=False)
+ if result.returncode != 0:
+ raise SystemExit(result.returncode)
+ os.replace(temporary_output.name, output)
+ finally:
+ list_file.close()
+ Path(list_file.name).unlink(missing_ok=True)
+ Path(temporary_output.name).unlink(missing_ok=True)
+
+
+def main():
+ args = parse_arguments()
+ root = resolved_path(args.root, Path.cwd())
+ if not root.is_dir():
+ raise SystemExit(f"Project root does not exist: {root}")
+ output = resolved_path(args.output, root)
+ ctags = args.ctags or shutil.which("ctags")
+ if ctags is None:
+ raise SystemExit("Universal Ctags is not available")
+
+ local_files = project_files(root, output)
+ python_files = []
+ python = None
+ unresolved = []
+ if not args.no_python_deps:
+ python_files, python, unresolved = python_dependency_files(args, root, local_files)
+ c_headers = []
+ if not args.no_c_deps:
+ c_headers = c_dependency_files(root, local_files)
+ extra_files = extra_source_files(args, root)
+ all_files = sorted(set(local_files + python_files + c_headers + extra_files))
+ if not all_files:
+ raise SystemExit("No source files found")
+
+ generate_tags(ctags, output, all_files)
+ print(f"project: {len(local_files)} files")
+ if python:
+ print(f"python: {python}; {len(python_files)} dependency files")
+ if unresolved:
+ print(f"python unresolved: {', '.join(unresolved)}")
+ if c_headers:
+ print(f"c: {len(c_headers)} dependency headers")
+ if extra_files:
+ print(f"extra: {len(extra_files)} files")
+ print(f"tags: {output}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/config.el b/config.el
index 76ca04b..1e69479 100644
--- a/config.el
+++ b/config.el
@@ -2318,171 +2318,64 @@ Timers that expired while Emacs was closed fire immediately."
(citre-edit-ctags-options-manually nil)
(citre-auto-enable-citre-mode-modes '(prog-mode))
:config
- (defconst my/project-ctags-nix-options
- '("--langdef=nix"
- "--map-nix=+.nix"
- "--kinddef-nix=a,attribute,attributes"
- "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*(\\.[A-Za-z_][A-Za-z0-9_'-]+)+)[[:space:]]*=[^=]/\\1/a/"
- "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*)[[:space:]]*=[^=]/\\1/a/"))
+ (defvar my/project-ctags-program
+ (expand-file-name "bin/project-ctags" user-emacs-directory))
+
+ (defvar-local my/project-ctags-arguments nil)
+
+ (put 'my/project-ctags-arguments 'safe-local-variable
+ (lambda (value)
+ (and (listp value) (seq-every-p #'stringp value))))
(defun my/project-ctags-root ()
(or (projectile-project-root)
(user-error "Not in a Projectile project")))
- (defun my/project-ctags-nixpkgs (root)
- (when (and (file-directory-p (expand-file-name "npins" root))
- (executable-find "npins"))
- (let ((default-directory root)
- (output (generate-new-buffer " *npins*")))
- (unwind-protect
- (when (zerop (process-file "npins" nil output nil
- "get-path" "nixpkgs"))
- (with-current-buffer output
- (string-trim (buffer-string))))
- (kill-buffer output)))))
-
- (defun my/project-ctags-nixos-options (nixpkgs output)
- (let ((expression
- (format
- "let
- nixpkgs = %S;
- evaluated = import (nixpkgs + \"/nixos/lib/eval-config.nix\") {
- system = builtins.currentSystem;
- modules = [ ];
- };
- collect = prefix: attrs:
- builtins.concatLists (builtins.map
- (name:
- let value = attrs.${name}; in
- if builtins.isAttrs value && (value._type or null) == \"option\"
- then
- if value.declarations == [ ] then [ ] else [
- [
- (builtins.concatStringsSep \".\" (prefix ++ [ name ]))
- (builtins.toString (builtins.head value.declarations))
- ]
- ]
- else if builtins.isAttrs value then collect (prefix ++ [ name ]) value
- else [ ])
- (builtins.attrNames attrs));
-in
- builtins.concatStringsSep \"\\n\" (builtins.map builtins.toJSON (collect [ ] evaluated.options))"
- nixpkgs)))
- (with-current-buffer output
- (erase-buffer))
- (if (zerop (process-file "nix-instantiate" nil output nil
- "--eval" "--raw" "--strict"
- "--expr" expression))
- (mapcar (lambda (line)
- (json-parse-string line :array-type 'list))
- (split-string (with-current-buffer output
- (buffer-string))
- "\n" t))
- (user-error "Could not evaluate NixOS options"))))
-
- (defun my/project-ctags-add-nixos-options (tags-file root output)
- (when-let* ((nixpkgs (my/project-ctags-nixpkgs root)))
- (condition-case err
- (let (headers tags)
- (dolist (line (with-temp-buffer
- (insert-file-contents tags-file)
- (split-string (buffer-string) "\n" t)))
- (if (string-prefix-p "!" line)
- (push line headers)
- (push line tags)))
- (dolist (option (my/project-ctags-nixos-options nixpkgs output))
- (pcase-let ((`(,name ,file) option))
- (push (format "%s\t%s\t1;\"\tkind:attribute\tlanguage:nix\troles:def"
- name file)
- tags)))
- (with-temp-file tags-file
- (dolist (line (nreverse headers))
- (insert line "\n"))
- (dolist (line (sort (delete-dups tags) #'string<))
- (insert line "\n"))))
- (error
- (message "NixOS option tags unavailable: %s"
- (error-message-string err))))))
-
- (defun my/project-ctags-files (root)
- (let ((default-directory root)
- files)
- (dolist (file (projectile-current-project-files))
- (unless (string= file ".tags")
- (when (file-regular-p (expand-file-name file root))
- (push (expand-file-name file root) files))))
- (nreverse files)))
-
-
(defun my/project-ctags-enable-citre (root)
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (and buffer-file-name
(file-in-directory-p buffer-file-name root))
(citre-mode 1)))))
- (defun my/project-ctags-start (root)
- (let* ((ctags (executable-find "ctags"))
- (tags-file (expand-file-name ".tags" root))
- (list-file (make-temp-file "citre-files-"))
- (temporary-tags-file (make-temp-file "citre-tags-"))
- (output (get-buffer-create "*citre-ctags*"))
- (files (my/project-ctags-files root)))
- (unless ctags
- (user-error "Universal Ctags is not available"))
- (unless files
- (user-error "No project files to tag"))
- (with-temp-file list-file
- (dolist (file files)
- (insert file "\n")))
+
+ (defun my/project-ctags-sentinel (process _event)
+ (when (memq (process-status process) '(exit signal))
+ (let ((root (process-get process 'project-root)))
+ (if (and (eq (process-status process) 'exit)
+ (zerop (process-exit-status process)))
+ (progn
+ (citre-clear-tags-file-cache)
+ (my/project-ctags-enable-citre root)
+ (message "Ctags ready for %s" root))
+ (display-buffer (process-buffer process))
+ (message "Ctags failed for %s; see %s"
+ root
+ (buffer-name (process-buffer process)))))))
+
+ (defun my/project-ctags-start (root arguments)
+ (unless (file-executable-p my/project-ctags-program)
+ (user-error "Project Ctags generator is not executable: %s"
+ my/project-ctags-program))
+ (let ((output (get-buffer-create "*project-ctags*")))
(with-current-buffer output
(erase-buffer))
- (make-process
- :name (format "ctags:%s" (file-name-nondirectory (directory-file-name root)))
- :buffer output
- :command (append
- (list ctags
- "--options=NONE"
- "--output-format=e-ctags")
- my/project-ctags-nix-options
- (list "--languages=all"
- "--kinds-all=*"
- "--fields=*"
- "--extras=*"
- "-f"
- temporary-tags-file
- "-L"
- list-file))
- :connection-type 'pipe
- :sentinel
- (lambda (process _event)
- (when (memq (process-status process) '(exit signal))
- (unwind-protect
- (if (and (eq (process-status process) 'exit)
- (zerop (process-exit-status process)))
- (progn
- (my/project-ctags-add-nixos-options
- temporary-tags-file root output)
- (rename-file temporary-tags-file tags-file t)
- (citre-clear-tags-file-cache)
- (my/project-ctags-enable-citre root)
- (message "Ctags ready for %s" root))
- (message "Ctags failed for %s; see %s"
- root
- (buffer-name output)))
- (when (file-exists-p list-file)
- (delete-file list-file))
- (when (file-exists-p temporary-tags-file)
- (delete-file temporary-tags-file))))))
- (message "Generating Ctags for %s..." root)))
+ (let ((process
+ (make-process
+ :name (format "ctags:%s"
+ (file-name-nondirectory
+ (directory-file-name root)))
+ :buffer output
+ :command (append (list my/project-ctags-program root)
+ arguments)
+ :connection-type 'pipe
+ :sentinel #'my/project-ctags-sentinel)))
+ (process-put process 'project-root root)))
+ (message "Generating Ctags for %s..." root))
(defun my/project-ctags-setup ()
(interactive)
- (let* ((root (my/project-ctags-root))
- (tags-file (expand-file-name ".tags" root)))
- (if (file-exists-p tags-file)
- (my/project-ctags-start root)
- (when (y-or-n-p (format "Set up Ctags for %s? " root))
- (my/project-ctags-start root)))))
+ (my/project-ctags-start (my/project-ctags-root)
+ my/project-ctags-arguments))
(with-eval-after-load 'evil
(evil-define-key '(normal motion) 'global
@@ -3592,7 +3485,7 @@ workspace (e.g. *scratch*)."
"pa" '(projectile-add-known-project :wk "add project")
"pi" '(projectile-invalidate-cache :wk "invalidate cache")
"pt" '(projectile-run-task :wk "tasks")
- "pc" '(my/project-ctags-setup :wk "setup/update ctags")
+ "pc" '(my/project-ctags-setup :wk "generate/update ctags")
"g" '(:ignore t :wk "git")
"gc" '(magit-clone :wk "clone")
diff --git a/config.org b/config.org
index cbd8727..c8082d1 100644
--- a/config.org
+++ b/config.org
@@ -2707,171 +2707,64 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
(citre-edit-ctags-options-manually nil)
(citre-auto-enable-citre-mode-modes '(prog-mode))
:config
- (defconst my/project-ctags-nix-options
- '("--langdef=nix"
- "--map-nix=+.nix"
- "--kinddef-nix=a,attribute,attributes"
- "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*(\\.[A-Za-z_][A-Za-z0-9_'-]+)+)[[:space:]]*=[^=]/\\1/a/"
- "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*)[[:space:]]*=[^=]/\\1/a/"))
+ (defvar my/project-ctags-program
+ (expand-file-name "bin/project-ctags" user-emacs-directory))
+
+ (defvar-local my/project-ctags-arguments nil)
+
+ (put 'my/project-ctags-arguments 'safe-local-variable
+ (lambda (value)
+ (and (listp value) (seq-every-p #'stringp value))))
(defun my/project-ctags-root ()
(or (projectile-project-root)
(user-error "Not in a Projectile project")))
- (defun my/project-ctags-nixpkgs (root)
- (when (and (file-directory-p (expand-file-name "npins" root))
- (executable-find "npins"))
- (let ((default-directory root)
- (output (generate-new-buffer " *npins*")))
- (unwind-protect
- (when (zerop (process-file "npins" nil output nil
- "get-path" "nixpkgs"))
- (with-current-buffer output
- (string-trim (buffer-string))))
- (kill-buffer output)))))
-
- (defun my/project-ctags-nixos-options (nixpkgs output)
- (let ((expression
- (format
- "let
- nixpkgs = %S;
- evaluated = import (nixpkgs + \"/nixos/lib/eval-config.nix\") {
- system = builtins.currentSystem;
- modules = [ ];
- };
- collect = prefix: attrs:
- builtins.concatLists (builtins.map
- (name:
- let value = attrs.${name}; in
- if builtins.isAttrs value && (value._type or null) == \"option\"
- then
- if value.declarations == [ ] then [ ] else [
- [
- (builtins.concatStringsSep \".\" (prefix ++ [ name ]))
- (builtins.toString (builtins.head value.declarations))
- ]
- ]
- else if builtins.isAttrs value then collect (prefix ++ [ name ]) value
- else [ ])
- (builtins.attrNames attrs));
-in
- builtins.concatStringsSep \"\\n\" (builtins.map builtins.toJSON (collect [ ] evaluated.options))"
- nixpkgs)))
- (with-current-buffer output
- (erase-buffer))
- (if (zerop (process-file "nix-instantiate" nil output nil
- "--eval" "--raw" "--strict"
- "--expr" expression))
- (mapcar (lambda (line)
- (json-parse-string line :array-type 'list))
- (split-string (with-current-buffer output
- (buffer-string))
- "\n" t))
- (user-error "Could not evaluate NixOS options"))))
-
- (defun my/project-ctags-add-nixos-options (tags-file root output)
- (when-let* ((nixpkgs (my/project-ctags-nixpkgs root)))
- (condition-case err
- (let (headers tags)
- (dolist (line (with-temp-buffer
- (insert-file-contents tags-file)
- (split-string (buffer-string) "\n" t)))
- (if (string-prefix-p "!" line)
- (push line headers)
- (push line tags)))
- (dolist (option (my/project-ctags-nixos-options nixpkgs output))
- (pcase-let ((`(,name ,file) option))
- (push (format "%s\t%s\t1;\"\tkind:attribute\tlanguage:nix\troles:def"
- name file)
- tags)))
- (with-temp-file tags-file
- (dolist (line (nreverse headers))
- (insert line "\n"))
- (dolist (line (sort (delete-dups tags) #'string<))
- (insert line "\n"))))
- (error
- (message "NixOS option tags unavailable: %s"
- (error-message-string err))))))
-
- (defun my/project-ctags-files (root)
- (let ((default-directory root)
- files)
- (dolist (file (projectile-current-project-files))
- (unless (string= file ".tags")
- (when (file-regular-p (expand-file-name file root))
- (push (expand-file-name file root) files))))
- (nreverse files)))
-
-
(defun my/project-ctags-enable-citre (root)
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (and buffer-file-name
(file-in-directory-p buffer-file-name root))
(citre-mode 1)))))
- (defun my/project-ctags-start (root)
- (let* ((ctags (executable-find "ctags"))
- (tags-file (expand-file-name ".tags" root))
- (list-file (make-temp-file "citre-files-"))
- (temporary-tags-file (make-temp-file "citre-tags-"))
- (output (get-buffer-create "*citre-ctags*"))
- (files (my/project-ctags-files root)))
- (unless ctags
- (user-error "Universal Ctags is not available"))
- (unless files
- (user-error "No project files to tag"))
- (with-temp-file list-file
- (dolist (file files)
- (insert file "\n")))
+
+ (defun my/project-ctags-sentinel (process _event)
+ (when (memq (process-status process) '(exit signal))
+ (let ((root (process-get process 'project-root)))
+ (if (and (eq (process-status process) 'exit)
+ (zerop (process-exit-status process)))
+ (progn
+ (citre-clear-tags-file-cache)
+ (my/project-ctags-enable-citre root)
+ (message "Ctags ready for %s" root))
+ (display-buffer (process-buffer process))
+ (message "Ctags failed for %s; see %s"
+ root
+ (buffer-name (process-buffer process)))))))
+
+ (defun my/project-ctags-start (root arguments)
+ (unless (file-executable-p my/project-ctags-program)
+ (user-error "Project Ctags generator is not executable: %s"
+ my/project-ctags-program))
+ (let ((output (get-buffer-create "*project-ctags*")))
(with-current-buffer output
(erase-buffer))
- (make-process
- :name (format "ctags:%s" (file-name-nondirectory (directory-file-name root)))
- :buffer output
- :command (append
- (list ctags
- "--options=NONE"
- "--output-format=e-ctags")
- my/project-ctags-nix-options
- (list "--languages=all"
- "--kinds-all=*"
- "--fields=*"
- "--extras=*"
- "-f"
- temporary-tags-file
- "-L"
- list-file))
- :connection-type 'pipe
- :sentinel
- (lambda (process _event)
- (when (memq (process-status process) '(exit signal))
- (unwind-protect
- (if (and (eq (process-status process) 'exit)
- (zerop (process-exit-status process)))
- (progn
- (my/project-ctags-add-nixos-options
- temporary-tags-file root output)
- (rename-file temporary-tags-file tags-file t)
- (citre-clear-tags-file-cache)
- (my/project-ctags-enable-citre root)
- (message "Ctags ready for %s" root))
- (message "Ctags failed for %s; see %s"
- root
- (buffer-name output)))
- (when (file-exists-p list-file)
- (delete-file list-file))
- (when (file-exists-p temporary-tags-file)
- (delete-file temporary-tags-file))))))
- (message "Generating Ctags for %s..." root)))
+ (let ((process
+ (make-process
+ :name (format "ctags:%s"
+ (file-name-nondirectory
+ (directory-file-name root)))
+ :buffer output
+ :command (append (list my/project-ctags-program root)
+ arguments)
+ :connection-type 'pipe
+ :sentinel #'my/project-ctags-sentinel)))
+ (process-put process 'project-root root)))
+ (message "Generating Ctags for %s..." root))
(defun my/project-ctags-setup ()
(interactive)
- (let* ((root (my/project-ctags-root))
- (tags-file (expand-file-name ".tags" root)))
- (if (file-exists-p tags-file)
- (my/project-ctags-start root)
- (when (y-or-n-p (format "Set up Ctags for %s? " root))
- (my/project-ctags-start root)))))
+ (my/project-ctags-start (my/project-ctags-root)
+ my/project-ctags-arguments))
(with-eval-after-load 'evil
(evil-define-key '(normal motion) 'global
@@ -4285,7 +4178,7 @@ workspace (e.g. *scratch*)."
"pa" '(projectile-add-known-project :wk "add project")
"pi" '(projectile-invalidate-cache :wk "invalidate cache")
"pt" '(projectile-run-task :wk "tasks")
- "pc" '(my/project-ctags-setup :wk "setup/update ctags")
+ "pc" '(my/project-ctags-setup :wk "generate/update ctags")
"g" '(:ignore t :wk "git")
"gc" '(magit-clone :wk "clone")