;;; company-ebuild.el --- Company backend for editing Ebuild files -*- lexical-binding: t -*- ;; Copyright 2022 Gentoo Authors ;; This file is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 2 of the License, or ;; (at your option) any later version. ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;; Authors: Maciej Barć ;; Created: 16 Aug 2022 ;; Version: 0.0.0 ;; Keywords: languages ;; Homepage: https://gitweb.gentoo.org/proj/company-ebuild.git ;; Package-Requires: ((emacs "25.1")) ;; SPDX-License-Identifier: GPL-2.0-or-later ;;; Commentary: ;; Company backend for editing Ebuild files. ;;; Code: (require 'cl-lib) (require 'company) (require 'ebuild-mode) (require 'company-ebuild-keywords) (defconst company-ebuild-version "0.0.0" "Company-Ebuild version.") (defun company-ebuild--annotation (candidate) "Return annotation for CANDIDATE." (cond ((member candidate company-ebuild--constant-keywords-architectures) " architecture") ((member candidate company-ebuild--constant-keywords-restrict) " restrict") ((member candidate company-ebuild--constant-keywords-phases) " phase") ((member candidate company-ebuild--constant-keywords-sandbox) " sandbox") ((member candidate company-ebuild--constant-keywords-doc) " doc") ((member candidate company-ebuild--constant-keywords-variables-predefined) " variable (predefined)") ((member candidate company-ebuild--constant-keywords-variables-ebuild-defined) " variable (ebuild-defined)") ((member candidate company-ebuild--constant-keywords-variables-dependencies) " variable (dependencies)") ((member candidate company-ebuild--constant-keywords-variables-user-environment) " variable (user-environment)") ((member candidate company-ebuild--dynamic-keywords-eclasses) " eclass") ((or (member candidate company-ebuild--constant-keywords-functions) (member candidate company-ebuild--dynamic-keywords-functions)) " function") ((member candidate company-ebuild--dynamic-keywords-variables) " variable (eclass)") ((member candidate company-ebuild--dynamic-keywords-use-flags) " USE flag") ((member candidate company-ebuild--dynamic-keywords-packages) " package") ((member candidate company-ebuild--dynamic-keywords-licenses) " license") ((executable-find candidate) " executable") (t ""))) (defun company-ebuild--packages () "Return a list of all available packages. Uses the \"qsearch\" tool to get the packages." (let ((qsearch (executable-find "qsearch")) (qsearch-formats '("%{CATEGORY}/%{PN}" "%{CATEGORY}/%{PN}-%{PV}" "%{CATEGORY}/%{PN}-%{PV}:%{SLOT}" "%{CATEGORY}/%{PN}-%{PV}:%{SLOT}::%{REPO}"))) (cond (qsearch (mapcan (lambda (qsearch-format) (let ((qlist-result (shell-command-to-string (format "%s --all --format \"%s\" --name-only --nocolor" qsearch qsearch-format)))) (split-string qlist-result "\n" t))) qsearch-formats)) (t nil)))) (defun company-ebuild--get-tags (file-path tag-name) "Return all tags with TAG-NAME from file at FILE-PATH. For example: \(company-ebuild--get-tags \"/gentoo/eclass/edo.eclass\" \"FUNCTION\")" (let ((tag (concat "# @" tag-name ": ")) (file-lines (with-temp-buffer (insert-file-contents file-path) (split-string (buffer-string) "\n" t)))) ;; Hack with `mapcan' - doing both filter and map. (mapcan (lambda (line) (cond ((string-match-p (concat tag ".*") line) (list (replace-regexp-in-string tag "" line))) (t nil))) file-lines))) (defun company-ebuild--find-repo-root (file-path) "Return the root directory of current Ebuild repository. FILE-PATH is the location from which we start searching for repository root." (and (not (null file-path)) (file-exists-p file-path) (locate-dominating-file file-path "profiles/repo_name"))) (defun company-ebuild--find-eclass-files (file-path) "Return found Eclass files. FILE-PATH is the location from which we start searching for Eclass files." (let ((repo-root (company-ebuild--find-repo-root file-path))) (and repo-root (directory-files (expand-file-name "eclass" repo-root) t ".*\\.eclass" t)))) (defun company-ebuild--regenerate-dynamic-keywords-eclasses () "Set new content of the ‘company-ebuild--dynamic-keywords’ Eclass variables." (let ((repo-root (company-ebuild--find-repo-root buffer-file-name))) (when repo-root (let ((eclass-files (company-ebuild--find-eclass-files repo-root))) (setq company-ebuild--dynamic-keywords-eclasses (apply #'append (mapcar (lambda (f) (mapcar (lambda (s) (replace-regexp-in-string "\\.eclass" "" s)) (company-ebuild--get-tags f "ECLASS"))) eclass-files))) (setq company-ebuild--dynamic-keywords-variables (apply #'append (mapcar (lambda (f) (company-ebuild--get-tags f "ECLASS_VARIABLE")) eclass-files))) (setq company-ebuild--dynamic-keywords-functions (apply #'append (mapcar (lambda (f) (company-ebuild--get-tags f "FUNCTION")) eclass-files))))))) (defun company-ebuild--regenerate-dynamic-keywords-use-flags () "Set new content of the ‘company-ebuild--dynamic-keywords-use-flags’ variable." (let ((repo-root (company-ebuild--find-repo-root buffer-file-name)) (awk-format "awk -F - '{ print $1 }' %s/profiles/use.desc")) (when (and repo-root (file-exists-p (expand-file-name "profiles/use.desc" repo-root))) (setq company-ebuild--dynamic-keywords-use-flags (let ((awk-result (shell-command-to-string (format awk-format repo-root)))) (mapcan (lambda (line) (cond ((not (string-prefix-p "#" line)) (list line)) (t nil))) (split-string awk-result "\n" t))))))) (defun company-ebuild--regenerate-dynamic-keywords-packages () "Set new content of the ‘company-ebuild--dynamic-keywords-packages’ variable." (setq company-ebuild--dynamic-keywords-packages (company-ebuild--packages))) (defun company-ebuild--regenerate-dynamic-keywords-licenses () "Set new content of the ‘company-ebuild--dynamic-keywords-licenses’ variable." (let ((repo-root (company-ebuild--find-repo-root buffer-file-name))) (when repo-root (setq company-ebuild--dynamic-keywords-licenses (directory-files (expand-file-name "licenses" repo-root)))))) (defun company-ebuild--regenerate-dynamic-keywords () "Regenerate dynamic keywords." (company-ebuild--regenerate-dynamic-keywords-eclasses) (company-ebuild--regenerate-dynamic-keywords-use-flags) (company-ebuild--regenerate-dynamic-keywords-packages) (company-ebuild--regenerate-dynamic-keywords-licenses)) ;;;###autoload (defun company-ebuild (command &optional arg &rest ignored) "Company backend for editing Ebuild files. COMMAND, ARG and IGNORED are for Company. COMMAND is matched with `cl-case'. ARG is the completion argument for annotation and candidates." (interactive (list 'interactive)) (cl-case command (annotation (company-ebuild--annotation arg)) (candidates ;; TODO: Complete any string that already appears in current buffer. (cl-remove-if-not (lambda (candidate) (string-prefix-p arg candidate t)) (append company-ebuild--constant-keywords (company-ebuild--dynamic-keywords) (company-ebuild--executables arg)))) (interactive (company-begin-backend 'company-ebuild)) (prefix (and (eq major-mode 'ebuild-mode) (company-grab-symbol))) (require-match nil))) ;;;###autoload (defun company-ebuild-setup () "Setup for Company-Ebuild. To setup the integration correctly, add this function to ‘ebuild-mode-hook’ in your config: \(add-hook 'ebuild-mode-hook 'company-ebuild-setup) or `require' Company-Ebuild: \(require 'company-ebuild)" ;; HACK: Modify syntax to treat "/" as a word constituent. ;; TODO: (Hard mode) write a proper `company-grab-symbol' replacement. (modify-syntax-entry ?/ "w") ;; Force-enable `company-mode'. (when (null company-mode) (company-mode +1)) ;; Regenerate dynamic keywords. (company-ebuild--regenerate-dynamic-keywords) ;; Add the `company-ebuild' backend. (cond ((fboundp 'company-yasnippet) (add-to-list 'company-backends '(company-ebuild company-yasnippet))) (t (add-to-list 'company-backends 'company-ebuild))) ;; Because some completions have length 1: (setq-local company-minimum-prefix-length 1) (setq-local company-require-match nil)) ;;;###autoload (add-hook 'ebuild-mode-hook 'company-ebuild-setup) (provide 'company-ebuild) ;;; company-ebuild.el ends here