├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
└── PULL_REQUEST_TEMPLATE.md
├── CHANGELOG.md
├── README.md
├── helm-projectile.el
└── screenshots
└── helm-projectile.png
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: bbatsov
4 | patreon: bbatsov
5 | custom: https://www.paypal.me/bbatsov
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Expected behavior
2 |
3 |
4 |
5 | ## Actual behavior
6 |
7 |
8 |
9 | ## Steps to reproduce the problem
10 |
11 |
12 |
13 | ## Backtraces if necessary (`M-x toggle-debug-on-error`)
14 |
15 |
16 |
17 | ## Environment & version information
18 |
19 | - `helm-projectile` version:
20 | - `helm` version (in `helm-pkg.el`):
21 | - `projectile` version (`M-x projectile-version`):
22 | - Emacs version (`M-x emacs-version`):
23 | - OS:
24 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Replace this placeholder text with a summary of the changes in your PR.
2 | The more detailed you are, the better.**
3 |
4 | -----------------
5 |
6 | Before submitting a PR make sure the following things have been done (and denote this
7 | by checking the relevant checkboxes):
8 |
9 | - [ ] The commits are consistent with our [contribution guidelines](../blob/master/CONTRIBUTING.md)
10 | - [ ] The new code is not generating bytecode or `M-x checkdoc` warnings
11 | - [ ] You've updated the [changelog](../blob/master/CHANGELOG.md) (if adding/changing user-visible functionality)
12 | - [ ] You've updated the readme (if adding/changing user-visible functionality)
13 |
14 | Thanks!
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## master (unreleased)
4 |
5 | ### Bugs fixed
6 |
7 | * [#188](https://github.com/bbatsov/helm-projectile/pull/178): Fix helm-projectile-projects-source slots
8 |
9 | ## 1.1.0 (2025-02-14)
10 |
11 | ### New features
12 |
13 | * Improve `helm-source-proctile-project-list` by also inheriting the
14 | `helm-type-file` class to benefit of it's functionality such as candidate
15 | transformer or file cache.
16 | * [#180](https://github.com/bbatsov/helm-projectile/pull/180): Introduce `helm-projectile-ignore-strategy` defcustom.
17 |
18 | ### Changes
19 |
20 | * [#151](https://github.com/bbatsov/helm-projectile/pull/157): Teach `helm-projectile-rg` to respect ignored files and directories.
21 | * [#151](https://github.com/bbatsov/helm-projectile/issues/151): Rename `helm-projectile-switch-to-eshell` -> `helm-projectile-switch-to-shell`.
22 |
23 | ### Bugs fixed
24 |
25 | * [#176](https://github.com/bbatsov/helm-projectile/pull/178): Correctly remove current buffer from `helm-source-projectile-buffers-list`.
26 | * [#143](https://github.com/bbatsov/helm-projectile/issues/143): Fix rg command for helm-ag arity.
27 | * [#145](https://github.com/bbatsov/helm-projectile/issues/145): Fix bug in `M-D` / remove from project list action for first project in the list.
28 | * [#143](https://github.com/bbatsov/helm-projectile/issues/143): Fix `rg` command for `helm-ag` arity.
29 | * [#140](https://github.com/bbatsov/helm-projectile/pull/140): Fix interactive options for `helm-projectile-grep` and `helm-projectile-ack`.
30 |
31 | ## 1.0.0 (2020-05-18)
32 |
33 | Initial stable release.
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![License GPL 3][badge-license]](http://www.gnu.org/licenses/gpl-3.0.txt)
2 | [](http://melpa.org/#/helm-projectile)
3 | [](http://stable.melpa.org/#/helm-projectile)
4 | [](https://www.patreon.com/bbatsov)
5 |
6 | ## Helm Projectile
7 |
8 | [Projectile](https://github.com/bbatsov/projectile) can be integrated
9 | with [Helm](https://github.com/emacs-helm/helm) via
10 | `helm-source-projectile-projects`,
11 | `helm-source-projectile-files-list`,
12 | `helm-source-projectile-buffers-list` and
13 | `helm-source-projectile-recentf-list` sources (available in
14 | `helm-projectile.el`). There is also an example function for calling
15 | Helm with the Projectile file source. You can call it like this:
16 |
17 | ```
18 | M-x helm-projectile
19 | ```
20 |
21 | or even better - invoke the key binding h in Projectile's
22 | keymap. Assuming you've opted for one of the recommended keymap prefixes (`C-c p`
23 | or `s-p`), that would mean C-c p h or s-p h.
24 |
25 | See Projectile's [installation docs](https://docs.projectile.mx/projectile/installation.html#installation-via-package-el) for more details.
26 |
27 | ## Project Status
28 |
29 | The project is currently looking for more maintainers. I (Bozhidar) don't use Helm myself and
30 | don't have much time for the project, so I would definitely appreciate some help with it.
31 |
32 | Contact me via e-mail or GitHub if you'd like to become a co-maintainer.
33 |
34 | ## Installation
35 |
36 | The recommended way to install helm-projectile is via `package.el`.
37 |
38 | ### package.el
39 |
40 | #### MELPA
41 |
42 | You can install a snapshot version of helm-projectile from the
43 | [MELPA](http://melpa.org) repository. The version of
44 | Projectile there will always be up-to-date, but it might be unstable
45 | (albeit rarely).
46 |
47 | #### MELPA Stable
48 |
49 | You can install the last stable version of helm-projectile from the
50 | [MELPA Stable](http://stable.melpa.org) repository.
51 |
52 | ### el-get
53 |
54 | helm-projectile is also available for installation from the
55 | [el-get](https://github.com/dimitri/el-get) package manager.
56 |
57 | ### Emacs Prelude
58 |
59 | helm-projectile is naturally part of the
60 | [Emacs Prelude](https://github.com/bbatsov/prelude). If you're a Prelude
61 | user - helm-projectile is already properly configured and ready for
62 | action.
63 |
64 | ### Debian and Ubuntu
65 |
66 | Users of Debian 9 or later or Ubuntu 16.04 or later may `apt
67 | install elpa-helm-projectile`.
68 |
69 | ## Usage
70 |
71 | For those who prefer helm to ido/ivy, the command `helm-projectile-switch-project`
72 | can be used to replace `projectile-switch-project` to switch project. Please
73 | note that this is different from simply setting `projectile-completion-system`
74 | to `helm`, which just enables projectile to use the Helm completion to complete
75 | a project name. The benefit of using `helm-projectile-switch-project` is that on
76 | any selected project we can fire many actions, not limited to just the "switch
77 | to project" action, as in the case of using helm completion by setting
78 | `projectile-completion-system` to `helm`. Currently, there are five actions:
79 | "Switch to project", "Open Dired in project's directory", "Open project root in
80 | vc-dir or magit", "Switch to Eshell" and "Grep project files". We will add more
81 | and more actions in the future.
82 |
83 | `helm-projectile` is capable of opening multiple files by marking the files with
84 | C-SPC or mark all files with M-a. Then, press RET,
85 | all the selected files will be opened.
86 |
87 | Note that the helm grep is different from `projectile-grep` because the helm
88 | grep is incremental. To use it, select your projects (select multiple projects
89 | by pressing C-SPC), press "C-s" (or "C-u C-s" for recursive grep), and type your
90 | regexp. As you type the regexp in the mini buffer, the live grep results are
91 | displayed incrementally.
92 |
93 | `helm-projectile` also provides Helm versions of common Projectile commands. Currently,
94 | these are the supported commands:
95 |
96 | * `helm-projectile-switch-project`
97 | * `helm-projectile-find-file`
98 | * `helm-projectile-find-file-in-known-projects`
99 | * `helm-projectile-find-file-dwim`
100 | * `helm-projectile-find-dir`
101 | * `helm-projectile-recentf`
102 | * `helm-projectile-switch-to-buffer`
103 | * `helm-projectile-grep` (can be used for both grep or ack)
104 | * `helm-projectile-ag`
105 | * `helm-projectile-rg`
106 | * Replace Helm equivalent commands in `projectile-commander`
107 | * A virtual directory manager that is unique to Helm Projectile
108 |
109 | Why should you use these commands compared with the normal Projectile
110 | commands, even if the normal commands use `helm` as
111 | `projectile-completion-system`? The answer is, Helm specific commands
112 | give more useful features. For example,
113 | `helm-projectile-switch-project` allows opening a project in Dired,
114 | Magit or Eshell. `helm-projectile-find-file` reuses actions in
115 | `helm-find-files` (which is plenty) and able to open multiple
116 | files. Another reason is that in a large source tree, helm-projectile
117 | could be slow because it has to open all available sources.
118 |
119 | If you want to use these commands, you have to activate it to replace
120 | the normal Projectile commands:
121 |
122 | ```el
123 | ;; (setq helm-projectile-fuzzy-match nil)
124 | (require 'helm-projectile)
125 | (helm-projectile-on)
126 | ```
127 |
128 | If you already activate helm-projectile key bindings and you don't
129 | like it, you can turn it off and use the normal Projectile bindings
130 | with command `helm-projectile-off`. Similarly, if you want to disable
131 | fuzzy matching in Helm Projectile (it is enabled by default), you must
132 | set `helm-projectile-fuzzy-match` to nil before loading
133 | `helm-projectile`.
134 |
135 | To fully learn Helm Projectile and see what it is capable of, you
136 | should refer to this guide:
137 | [Exploring large projects with Projectile and Helm Projectile](http://tuhdo.github.io/helm-projectile.html).
138 |
139 | Obviously you need to have Helm installed for this to work. :-)
140 |
141 | 
142 |
143 | ## Known issues
144 |
145 | Check out the project's
146 | [issue list](https://github.com/bbatsov/helm-projectile/issues?sort=created&direction=desc&state=open)
147 | a list of unresolved issues. By the way - feel free to fix any of them
148 | and sent me a pull request. :-)
149 |
150 | ## Contributors
151 |
152 | Here's a [list](https://github.com/bbatsov/helm-projectile/contributors) of all the people who have contributed to the
153 | development of Projectile.
154 |
155 | ## Changelog
156 |
157 | A fairly extensive changelog is available [here](CHANGELOG.md).
158 |
159 | **Note:** We started keeping track of changes there after version 1.0.
160 |
161 | ## License
162 |
163 | Copyright © 2011-2025 Bozhidar Batsov and
164 | [contributors](https://github.com/bbatsov/helm-projectile/contributors).
165 |
166 | Distributed under the GNU General Public License, version 3
167 |
168 | [badge-license]: https://img.shields.io/badge/license-GPLv3-blue.svg
169 |
--------------------------------------------------------------------------------
/helm-projectile.el:
--------------------------------------------------------------------------------
1 | ;;; helm-projectile.el --- Helm integration for Projectile -*- lexical-binding: t; -*-
2 |
3 | ;; Copyright (C) 2011-2025 Bozhidar Batsov
4 |
5 | ;; Author: Bozhidar Batsov
6 | ;; URL: https://github.com/bbatsov/helm-projectile
7 | ;; Created: 2011-31-07
8 | ;; Keywords: project, convenience
9 | ;; Version: 1.1.0
10 | ;; Package-Requires: ((helm "3.0") (projectile "2.9") (cl-lib "0.3"))
11 |
12 | ;; This file is NOT part of GNU Emacs.
13 |
14 | ;;; License:
15 |
16 | ;; This program is free software; you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published by
18 | ;; the Free Software Foundation; either version 3, or (at your option)
19 | ;; any later version.
20 | ;;
21 | ;; This program is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with GNU Emacs; see the file COPYING. If not, write to the
28 | ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
29 | ;; Boston, MA 02110-1301, USA.
30 |
31 | ;;; Commentary:
32 | ;;
33 | ;; This library provides easy project management and navigation. The
34 | ;; concept of a project is pretty basic - just a folder containing
35 | ;; special file. Currently git, mercurial and bazaar repos are
36 | ;; considered projects by default. If you want to mark a folder
37 | ;; manually as a project just create an empty .projectile file in
38 | ;; it. See the README for more details.
39 | ;;
40 | ;;; Code:
41 |
42 | ;; built-in libraries
43 | (require 'subr-x)
44 | (require 'cl-lib)
45 | (require 'grep) ;; TODO: Probably we should defer this require
46 |
47 | (require 'helm-core)
48 | (require 'helm-global-bindings)
49 | (require 'helm-types)
50 | (require 'helm-locate)
51 | (require 'helm-buffers)
52 | (require 'helm-files)
53 |
54 | (require 'projectile)
55 |
56 | (declare-function eshell "eshell")
57 | (declare-function helm-do-ag "ext:helm-ag")
58 | (declare-function dired-get-filename "dired")
59 | (defvar helm-ag-base-command)
60 |
61 | (defvar grep-find-ignored-directories)
62 | (defvar grep-find-ignored-files)
63 |
64 | (defgroup helm-projectile nil
65 | "Helm support for projectile."
66 | :prefix "helm-projectile-"
67 | :group 'projectile
68 | :group 'helm
69 | :link `(url-link :tag "GitHub" "https://github.com/bbatsov/helm-projectile"))
70 |
71 | (defvar helm-projectile-current-project-root)
72 |
73 | (defcustom helm-projectile-truncate-lines nil
74 | "Truncate lines in helm projectile commands when non--nil.
75 |
76 | Some `helm-projectile' commands have similar behavior with existing
77 | Helms. In these cases their respective custom var for truncation
78 | of lines will be honored. E.g. `helm-buffers-truncate-lines'
79 | dictates the truncation in `helm-projectile-switch-to-buffer'."
80 | :group 'helm-projectile
81 | :type 'boolean)
82 |
83 | ;;;###autoload
84 | (defcustom helm-projectile-fuzzy-match t
85 | "Enable fuzzy matching for Helm Projectile commands.
86 | This needs to be set before loading helm-projectile.el."
87 | :group 'helm-projectile
88 | :type 'boolean)
89 |
90 | (defmacro helm-projectile-define-key (keymap key def &rest bindings)
91 | "In KEYMAP, define KEY - DEF sequence KEY1 as DEF1, KEY2 as DEF2 ..."
92 | (declare (indent defun))
93 | (let ((ret '(progn)))
94 | (while key
95 | (push
96 | `(define-key ,keymap ,key
97 | (lambda ()
98 | (interactive)
99 | (helm-exit-and-execute-action ,def)))
100 | ret)
101 | (setq key (pop bindings)
102 | def (pop bindings)))
103 | (reverse ret)))
104 |
105 | (defun helm-projectile-hack-actions (actions &rest prescription)
106 | "Given a Helm action list and a prescription, return a hacked Helm action list.
107 | Optionally applies the PRESCRIPTION beforehand.
108 |
109 | The Helm action list ACTIONS is of the form:
110 |
111 | \(\(DESCRIPTION1 . FUNCTION1\)
112 | \(DESCRIPTION2 . FUNCTION2\)
113 | ...
114 | \(DESCRIPTIONn . FUNCTIONn\)\)
115 |
116 | PRESCRIPTION is in the form:
117 |
118 | \(INSTRUCTION1 INSTRUCTION2 ... INSTRUCTIONn\)
119 |
120 | If an INSTRUCTION is a symbol, the action with function name
121 | INSTRUCTION is deleted.
122 |
123 | If an INSTRUCTION is of the form \(FUNCTION1 . FUNCTION2\), the
124 | action with function name FUNCTION1 will change it's function to
125 | FUNCTION2.
126 |
127 | If an INSTRUCTION is of the form \(FUNCTION . DESCRIPTION\), and
128 | if an action with function name FUNCTION exists in the original
129 | Helm action list, the action in the Helm action list, with
130 | function name FUNCTION will change it's description to
131 | DESCRIPTION. Otherwise, (FUNCTION . DESCRIPTION) will be added to
132 | the action list.
133 |
134 | Please check out how `helm-projectile-file-actions' is defined
135 | for an example of how this function is being used."
136 | (let* ((to-delete (cl-remove-if (lambda (entry) (listp entry)) prescription))
137 | (actions (cl-delete-if (lambda (action) (memq (cdr action) to-delete))
138 | (copy-alist actions)))
139 | new)
140 | (cl-dolist (action actions)
141 | (when (setq new (cdr (assq (cdr action) prescription)))
142 | (if (stringp new)
143 | (setcar action new)
144 | (setcdr action new))))
145 | ;; Add new actions from PRESCRIPTION
146 | (setq new nil)
147 | (cl-dolist (instruction prescription)
148 | (when (and (listp instruction)
149 | (null (rassq (car instruction) actions))
150 | (symbolp (car instruction)) (stringp (cdr instruction)))
151 | (push (cons (cdr instruction) (car instruction)) new)))
152 | (append actions (nreverse new))))
153 |
154 | (defun helm-projectile-vc (dir)
155 | "A Helm action for jumping to project root using `vc-dir' or Magit.
156 | DIR is a directory to be switched"
157 | (let ((projectile-require-project-root nil))
158 | (projectile-vc dir)))
159 |
160 | (defun helm-projectile-compile-project (dir)
161 | "A Helm action for compile a project.
162 | DIR is the project root."
163 | (let ((helm--reading-passwd-or-string t)
164 | (default-directory dir))
165 | (projectile-compile-project helm-current-prefix-arg)))
166 |
167 | (defun helm-projectile-test-project (dir)
168 | "A Helm action for test a project.
169 | DIR is the project root."
170 | (let ((helm--reading-passwd-or-string t)
171 | (default-directory dir))
172 | (projectile-test-project helm-current-prefix-arg)))
173 |
174 | (defun helm-projectile-run-project (dir)
175 | "A Helm action for run a project.
176 | DIR is the project root."
177 | (let ((helm--reading-passwd-or-string t)
178 | (default-directory dir))
179 | (projectile-run-project helm-current-prefix-arg)))
180 |
181 | (defun helm-projectile-remove-known-project (_ignore)
182 | "Remove selected projects from projectile project list.
183 | _IGNORE means the argument does not matter.
184 | It is there because Helm requires it."
185 | (let* ((projects (helm-marked-candidates :with-wildcard t))
186 | (len (length projects)))
187 | (with-helm-display-marked-candidates
188 | helm-marked-buffer-name
189 | projects
190 | (if (not (y-or-n-p (format "Remove *%s projects(s)? " len)))
191 | (message "(No removal performed)")
192 | (progn
193 | (mapc (lambda (p)
194 | (setq projectile-known-projects (delete p projectile-known-projects)))
195 | projects)
196 | (projectile-save-known-projects))
197 | (message "%s projects(s) removed" len)))))
198 |
199 | (defvar helm-projectile-projects-map
200 | (let ((map (make-sparse-keymap)))
201 | (set-keymap-parent map helm-map)
202 | (helm-projectile-define-key map
203 | (kbd "C-d") #'dired
204 | (kbd "M-g") #'helm-projectile-vc
205 | (kbd "M-e") #'helm-projectile-switch-to-shell
206 | (kbd "C-s") #'helm-projectile-grep
207 | (kbd "M-c") #'helm-projectile-compile-project
208 | (kbd "M-t") #'helm-projectile-test-project
209 | (kbd "M-r") #'helm-projectile-run-project
210 | (kbd "M-D") #'helm-projectile-remove-known-project)
211 | map)
212 | "Mapping for known projectile projects.")
213 |
214 | (defcustom helm-source-projectile-projects-actions
215 | (helm-make-actions
216 | "Switch to project" (lambda (project)
217 | (let ((projectile-completion-system 'helm))
218 | (projectile-switch-project-by-name project)))
219 | "Open Dired in project's directory `C-d'" #'dired
220 | "Open project root in vc-dir or magit `M-g'" #'helm-projectile-vc
221 | "Switch to Eshell `M-e'" #'helm-projectile-switch-to-shell
222 | "Grep in projects `C-s'" #'helm-projectile-grep
223 | "Compile project `M-c'. With C-u, new compile command" #'helm-projectile-compile-project
224 | "Remove project(s) from project list `M-D'" #'helm-projectile-remove-known-project)
225 | "Actions for `helm-source-projectile-projects'."
226 | :group 'helm-projectile
227 | :type '(alist :key-type string :value-type function))
228 |
229 | (defclass helm-projectile-projects-source (helm-source-sync helm-type-file)
230 | ((candidates :initform (lambda () (with-helm-current-buffer
231 | (mapcar #'copy-sequence
232 | (projectile-known-projects)))))
233 | (fuzzy-match :initform 'helm-projectile-fuzzy-match)
234 | (keymap :initform 'helm-projectile-projects-map)
235 | (mode-line :initform 'helm-read-file-name-mode-line-string)
236 | (action :initform 'helm-source-projectile-projects-actions))
237 | "Helm source for known projectile projects.")
238 |
239 | (cl-defmethod helm--setup-source :after ((source helm-projectile-projects-source))
240 | "Make SOURCE specific to project switching.
241 | The `helm-projectile-projects-source` inherits from
242 | `helm-type-file` (which see), which sets up actions, keymap, and
243 | help message slots to file specific ones. Override these slots
244 | to be specific to `helm-projectile-projects-source'."
245 | (setf (slot-value source 'action) 'helm-source-projectile-projects-actions)
246 | (setf (slot-value source 'keymap) helm-projectile-projects-map)
247 | ;; Use `ignore' as a persistent action, to actually keep `helm' session
248 | ;; when `helm-execute-persistent-action' is executed.
249 | (setf (slot-value source 'persistent-action) #'ignore)
250 | (let ((persistent-help "Do Nothing"))
251 | (setf (slot-value source 'persistent-help) persistent-help)
252 | (setf (slot-value source 'header-line)
253 | (helm-source--persistent-help-string
254 | persistent-help
255 | source)))
256 | (setf (slot-value source 'mode-line)
257 | (list "Project(s)" helm-mode-line-string)))
258 |
259 | (defvar helm-source-projectile-projects
260 | (helm-make-source "Projectile projects" 'helm-projectile-projects-source))
261 |
262 | (defvar helm-projectile-dirty-projects-map
263 | (let ((map (make-sparse-keymap)))
264 | (set-keymap-parent map helm-map)
265 | (helm-projectile-define-key map
266 | (kbd "C-d") #'dired
267 | (kbd "M-o") #'(lambda (project)
268 | (let ((projectile-completion-system 'helm))
269 | (projectile-switch-project-by-name project)))
270 | (kbd "M-e") #'helm-projectile-switch-to-shell
271 | (kbd "C-s") #'helm-projectile-grep
272 | (kbd "M-c") #'helm-projectile-compile-project
273 | (kbd "M-t") #'helm-projectile-test-project
274 | (kbd "M-r") #'helm-projectile-run-project
275 | (kbd "M-D") #'helm-projectile-remove-known-project)
276 | map)
277 | "Mapping for dirty projectile projects.")
278 |
279 | (defvar helm-source-projectile-dirty-projects
280 | (helm-build-sync-source "Projectile dirty projects"
281 | :candidates (lambda () (with-helm-current-buffer (helm-projectile-get-dirty-projects)))
282 | :fuzzy-match helm-projectile-fuzzy-match
283 | :keymap helm-projectile-dirty-projects-map
284 | :mode-line helm-read-file-name-mode-line-string
285 | :action '(("Open project root in vc-dir or magit" . helm-projectile-vc)
286 | ("Switch to project `M-o'" .
287 | (lambda (project)
288 | (let ((projectile-completion-system 'helm))
289 | (projectile-switch-project-by-name project))))
290 | ("Open Dired in project's directory `C-d'" . dired)
291 | ("Switch to Eshell `M-e'" . helm-projectile-switch-to-shell)
292 | ("Grep in projects `C-s'" . helm-projectile-grep)
293 | ("Compile project `M-c'. With C-u, new compile command"
294 | . helm-projectile-compile-project)))
295 | "Helm source for dirty version controlled projectile projects.")
296 |
297 | (defun helm-projectile-get-dirty-projects ()
298 | "Return dirty version controlled known projects as an alist to
299 | have a nice display in Helm."
300 | (message "Checking for dirty known projects...")
301 | (let* ((status (projectile-check-vcs-status-of-known-projects))
302 | (proj-dir (cl-loop for stat in status
303 | collect (car stat)))
304 | (status-display (cl-loop for stat in status collect
305 | (propertize (format "[%s]" (mapconcat 'identity (car (cdr stat)) ", ")) 'face 'helm-match)))
306 | (max-status-display-length (cl-loop for sd in status-display
307 | maximize (length sd)))
308 | (status-display (cl-loop for sd in status-display collect
309 | (format "%s%s " sd (make-string (- max-status-display-length (length sd)) ? ))))
310 | (full-display (cl-mapcar 'concat status-display proj-dir))
311 | (helm-list (cl-pairlis full-display proj-dir)))
312 | helm-list))
313 |
314 | (define-key helm-etags-map (kbd "C-c p f")
315 | (lambda ()
316 | (interactive)
317 | (helm-run-after-exit 'helm-projectile-find-file nil)))
318 |
319 | (defun helm-projectile-file-persistent-action (candidate)
320 | "Persistent action for file-related functionality.
321 |
322 | Previews the contents of a file in a temporary buffer."
323 | (let ((buf (get-buffer-create " *helm-projectile persistent*")))
324 | (cl-flet ((preview (candidate)
325 | (switch-to-buffer buf)
326 | (setq inhibit-read-only t)
327 | (erase-buffer)
328 | (insert-file-contents candidate)
329 | (let ((buffer-file-name candidate))
330 | (set-auto-mode))
331 | (font-lock-ensure)
332 | (setq inhibit-read-only nil)))
333 | (if (and (helm-get-attr 'previewp)
334 | (string= candidate (helm-get-attr 'current-candidate)))
335 | (progn
336 | (kill-buffer buf)
337 | (helm-set-attr 'previewp nil))
338 | (preview candidate)
339 | (helm-set-attr 'previewp t)))
340 | (helm-set-attr 'current-candidate candidate)))
341 |
342 | (defun helm-projectile-find-files-eshell-command-on-file-action (candidate)
343 | (interactive)
344 | (let* ((helm-ff-default-directory (file-name-directory candidate)))
345 | (helm-find-files-eshell-command-on-file candidate)))
346 |
347 | (defun helm-projectile-ff-etags-select-action (candidate)
348 | (interactive)
349 | (let* ((helm-ff-default-directory (file-name-directory candidate)))
350 | (helm-ff-etags-select candidate)))
351 |
352 | (defun helm-projectile-switch-to-shell (dir)
353 | "Within DIR, switch to a shell corresponding to `helm-ff-preferred-shell-mode'."
354 | (interactive)
355 | (let* ((projectile-require-project-root nil)
356 | (helm-ff-default-directory (file-name-directory (projectile-expand-root dir))))
357 | (helm-ff-switch-to-shell dir)))
358 |
359 | (defun helm-projectile-files-in-current-dired-buffer ()
360 | "Return a list of files (only) in the current dired buffer."
361 | (let (flist)
362 | (cl-flet ((fpush (fname) (push fname flist)))
363 | (save-excursion
364 | (let (file buffer-read-only)
365 | (goto-char (point-min))
366 | (while (not (eobp))
367 | (save-excursion
368 | (and (not (eolp))
369 | (setq file (dired-get-filename t t)) ; nil on non-file
370 | (progn (end-of-line)
371 | (funcall #'fpush file))))
372 | (forward-line 1)))))
373 | (mapcar 'file-truename (nreverse flist))))
374 |
375 | (defun helm-projectile-all-dired-buffers ()
376 | "Get all current Dired buffers."
377 | (mapcar (lambda (b)
378 | (with-current-buffer b (buffer-name)))
379 | (cl-remove-if-not
380 | (lambda (b)
381 | (with-current-buffer b
382 | (and (eq major-mode 'dired-mode)
383 | (buffer-name))))
384 | (buffer-list))))
385 |
386 | (defvar helm-projectile-virtual-dired-remote-enable nil
387 | "Enable virtual Dired manager on remote host.
388 | Disabled by default.")
389 |
390 | (defun helm-projectile-dired-files-new-action (candidate)
391 | "Create a Dired buffer from chosen files.
392 | CANDIDATE is the selected file, but choose the marked files if available."
393 | (if (and (file-remote-p (projectile-project-root))
394 | (not helm-projectile-virtual-dired-remote-enable))
395 | (message "Virtual Dired manager is disabled in remote host. Enable with %s."
396 | (propertize "helm-projectile-virtual-dired-remote-enable" 'face 'font-lock-keyword-face))
397 | (let ((files (cl-remove-if-not
398 | (lambda (f)
399 | (not (string= f "")))
400 | (mapcar (lambda (file)
401 | (replace-regexp-in-string (projectile-project-root) "" file))
402 | (helm-marked-candidates :with-wildcard t))))
403 | (new-name (completing-read "Select or enter a new buffer name: "
404 | (helm-projectile-all-dired-buffers)))
405 | (helm--reading-passwd-or-string t)
406 | (default-directory (projectile-project-root)))
407 | ;; create a unique buffer that is unique to any directory in default-directory
408 | ;; or opened buffer; when Dired is passed with a non-existence directory name,
409 | ;; it only creates a buffer and insert everything. If a new name user supplied
410 | ;; exists as default-directory, Dired throws error when insert anything that
411 | ;; does not exist in current directory.
412 | (with-current-buffer (dired (cons (make-temp-name new-name)
413 | (if files
414 | files
415 | (list candidate))))
416 | (when (get-buffer new-name)
417 | (kill-buffer new-name))
418 | (rename-buffer new-name)))))
419 |
420 | (defun helm-projectile-dired-files-add-action (candidate)
421 | "Add files to a Dired buffer.
422 | CANDIDATE is the selected file. Used when no file is explicitly marked."
423 | (if (and (file-remote-p (projectile-project-root))
424 | (not helm-projectile-virtual-dired-remote-enable))
425 | (message "Virtual Dired manager is disabled in remote host. Enable with %s."
426 | (propertize "helm-projectile-virtual-dired-remote-enable" 'face 'font-lock-keyword-face))
427 | (if (eq (with-helm-current-buffer major-mode) 'dired-mode)
428 | (let* ((marked-files (helm-marked-candidates :with-wildcard t))
429 | (helm--reading-passwd-or-string t)
430 | (root (projectile-project-root)) ; store root for later use
431 | (dired-buffer-name (or (and (eq major-mode 'dired-mode) (buffer-name))
432 | (completing-read "Select a Dired buffer:"
433 | (helm-projectile-all-dired-buffers))))
434 | (dired-files (with-current-buffer dired-buffer-name
435 | (helm-projectile-files-in-current-dired-buffer)))
436 | (files (sort (mapcar (lambda (file)
437 | (replace-regexp-in-string (projectile-project-root) "" file))
438 | (cl-nunion (if marked-files
439 | marked-files
440 | (list candidate))
441 | dired-files
442 | :test #'string-equal))
443 | 'string-lessp)))
444 | (kill-buffer dired-buffer-name)
445 | ;; Rebind default-directory because after killing a buffer, we
446 | ;; could be in any buffer and default-directory is set to that
447 | ;; random buffer
448 | ;;
449 | ;; Also use saved root directory, because after killing a buffer,
450 | ;; we could be outside of current project
451 | (let ((default-directory root))
452 | (with-current-buffer (dired (cons (make-temp-name dired-buffer-name)
453 | (if files
454 | (mapcar (lambda (file)
455 | (replace-regexp-in-string root "" file))
456 | files)
457 | (list candidate))))
458 | (rename-buffer dired-buffer-name))))
459 | (error "You're not in a Dired buffer to add"))))
460 |
461 | (defun helm-projectile-dired-files-delete-action (candidate)
462 | "Delete selected entries from a Dired buffer.
463 | CANDIDATE is the selected file. Used when no file is explicitly marked."
464 | (if (and (file-remote-p (projectile-project-root))
465 | (not helm-projectile-virtual-dired-remote-enable))
466 | (message "Virtual Dired manager is disabled in remote host. Enable with %s."
467 | (propertize "helm-projectile-virtual-dired-remote-enable" 'face 'font-lock-keyword-face))
468 | (let* ((helm--reading-passwd-or-string t)
469 | (root (projectile-project-root))
470 | (dired-buffer-name (with-helm-current-buffer (buffer-name)))
471 | (dired-files (with-current-buffer dired-buffer-name
472 | (helm-projectile-files-in-current-dired-buffer)))
473 | (files (sort (cl-set-exclusive-or (helm-marked-candidates :with-wildcard t)
474 | dired-files
475 | :test #'string-equal) #'string-lessp)))
476 | (kill-buffer dired-buffer-name)
477 | ;; similar reason to `helm-projectile-dired-files-add-action'
478 | (let ((default-directory root))
479 | (with-current-buffer (dired (cons (make-temp-name dired-buffer-name)
480 | (if files
481 | (mapcar (lambda (file)
482 | (replace-regexp-in-string root "" file))
483 | files)
484 | (list candidate))))
485 | (rename-buffer dired-buffer-name))))))
486 |
487 | (defun helm-projectile-run-projectile-hooks-after-find-file (_orig-fun &rest _args)
488 | "Run `projectile-find-file-hook' if using projectile."
489 | (when (and projectile-mode (projectile-project-p))
490 | (run-hooks 'projectile-find-file-hook)))
491 |
492 | (advice-add 'helm-find-file-or-marked
493 | :after #'helm-projectile-run-projectile-hooks-after-find-file)
494 |
495 | (defvar helm-projectile-find-file-map
496 | (let ((map (copy-keymap helm-find-files-map)))
497 | (helm-projectile-define-key map
498 | (kbd "C-c f") #'helm-projectile-dired-files-new-action
499 | (kbd "C-c a") #'helm-projectile-dired-files-add-action
500 | (kbd "M-e") #'helm-projectile-switch-to-shell
501 | (kbd "M-.") #'helm-projectile-ff-etags-select-action
502 | (kbd "M-!") #'helm-projectile-find-files-eshell-command-on-file-action)
503 | (define-key map (kbd "") #'helm-previous-source)
504 | (define-key map (kbd "") #'helm-next-source)
505 | (dolist (cmd '(helm-find-files-up-one-level
506 | helm-find-files-down-last-level))
507 | (substitute-key-definition cmd nil map))
508 | map)
509 | "Mapping for file commands in Helm Projectile.")
510 |
511 | (defvar helm-projectile-file-actions
512 | (helm-projectile-hack-actions
513 | helm-find-files-actions
514 | ;; Delete these actions
515 | 'helm-ff-browse-project
516 | 'helm-insert-file-name-completion-at-point
517 | 'helm-ff-find-sh-command
518 | 'helm-ff-cache-add-file
519 | ;; Substitute these actions
520 | '(helm-ff-switch-to-shell . helm-projectile-switch-to-shell)
521 | '(helm-ff-etags-select . helm-projectile-ff-etags-select-action)
522 | '(helm-find-files-eshell-command-on-file
523 | . helm-projectile-find-files-eshell-command-on-file-action)
524 | ;; Change action descriptions
525 | '(helm-find-file-as-root . "Find file as root `C-c r'")
526 | ;; New actions
527 | '(helm-projectile-dired-files-new-action
528 | . "Create Dired buffer from files `C-c f'")
529 | '(helm-projectile-dired-files-add-action
530 | . "Add files to Dired buffer `C-c a'"))
531 | "Action for files.")
532 |
533 | (defun helm-projectile--move-selection-p (selection)
534 | "Return non-nil if should move Helm selector after SELECTION.
535 |
536 | SELECTION should be moved unless it's one of:
537 |
538 | - Non-string
539 | - Existing file
540 | - Non-remote file that matches `helm-tramp-file-name-regexp'"
541 | (not (or (not (stringp selection))
542 | (file-exists-p selection)
543 | (and (string-match helm-tramp-file-name-regexp selection)
544 | (not (file-remote-p selection nil t))))))
545 |
546 | (defun helm-projectile--move-to-real ()
547 | "Move to first real candidate.
548 |
549 | Similar to `helm-ff--move-to-first-real-candidate', but without
550 | unnecessary complexity."
551 | (while (let* ((src (helm-get-current-source))
552 | (selection (and (not (helm-empty-source-p))
553 | (helm-get-selection nil nil src))))
554 | (and (not (helm-end-of-source-p))
555 | (helm-projectile--move-selection-p selection)))
556 | (helm-next-line)))
557 |
558 | (defun helm-projectile--remove-move-to-real ()
559 | "Hook function to remove `helm-projectile--move-to-real'.
560 |
561 | Meant to be added to `helm-cleanup-hook', from which it removes
562 | itself at the end."
563 | (remove-hook 'helm-after-update-hook #'helm-projectile--move-to-real)
564 | (remove-hook 'helm-cleanup-hook #'helm-projectile--remove-move-to-real))
565 |
566 | (defvar helm-source-projectile-files-list-before-init-hook
567 | (lambda ()
568 | (add-hook 'helm-after-update-hook #'helm-projectile--move-to-real)
569 | (add-hook 'helm-cleanup-hook #'helm-projectile--remove-move-to-real)))
570 |
571 | (defvar helm-source-projectile-files-list
572 | (helm-build-sync-source "Projectile files"
573 | :before-init-hook 'helm-source-projectile-files-list-before-init-hook
574 | :candidates (lambda ()
575 | (when (projectile-project-p)
576 | (with-helm-current-buffer
577 | (cl-loop with root = (projectile-project-root)
578 | for display in (projectile-current-project-files)
579 | collect (cons display (expand-file-name display root))))))
580 | :filtered-candidate-transformer
581 | (lambda (files _source)
582 | (with-helm-current-buffer
583 | (let* ((root (projectile-project-root))
584 | (file-at-root (file-relative-name (expand-file-name helm-pattern root))))
585 | (if (or (string-empty-p helm-pattern)
586 | (assoc helm-pattern files))
587 | files
588 | (if (equal helm-pattern file-at-root)
589 | (cl-acons (helm-ff-prefix-filename helm-pattern nil t)
590 | (expand-file-name helm-pattern)
591 | files)
592 | (cl-pairlis (list (helm-ff-prefix-filename helm-pattern nil t)
593 | (helm-ff-prefix-filename file-at-root nil t))
594 | (list (expand-file-name helm-pattern)
595 | (expand-file-name helm-pattern root))
596 | files))))))
597 | :fuzzy-match helm-projectile-fuzzy-match
598 | :keymap helm-projectile-find-file-map
599 | :help-message 'helm-ff-help-message
600 | :mode-line helm-read-file-name-mode-line-string
601 | :action helm-projectile-file-actions
602 | :persistent-action #'helm-projectile-file-persistent-action
603 | :persistent-help "Preview file")
604 | "Helm source definition for Projectile files.")
605 |
606 | (defvar helm-source-projectile-files-in-all-projects-list
607 | (helm-build-sync-source "Projectile files in all Projects"
608 | :candidates (lambda ()
609 | (with-helm-current-buffer
610 | (let ((projectile-require-project-root nil))
611 | (projectile-all-project-files))))
612 | :keymap helm-projectile-find-file-map
613 | :help-message 'helm-ff-help-message
614 | :mode-line helm-read-file-name-mode-line-string
615 | :action helm-projectile-file-actions
616 | :persistent-action #'helm-projectile-file-persistent-action
617 | :persistent-help "Preview file")
618 | "Helm source definition for all Projectile files in all projects.")
619 |
620 | (defvar helm-projectile-dired-file-actions
621 | (helm-projectile-hack-actions
622 | helm-projectile-file-actions
623 | ;; New actions
624 | '(helm-projectile-dired-files-delete-action . "Remove entry(s) from Dired buffer `C-c d'")))
625 |
626 | (defvar helm-source-projectile-dired-files-list
627 | (helm-build-in-buffer-source "Projectile files in current Dired buffer"
628 | :data (lambda ()
629 | (if (and (file-remote-p (projectile-project-root))
630 | (not helm-projectile-virtual-dired-remote-enable))
631 | nil
632 | (when (eq major-mode 'dired-mode)
633 | (helm-projectile-files-in-current-dired-buffer))))
634 | :filter-one-by-one (lambda (file)
635 | (let ((helm-ff-transformer-show-only-basename t))
636 | (helm-ff-filter-candidate-one-by-one file)))
637 | :action-transformer 'helm-find-files-action-transformer
638 | :keymap (let ((map (copy-keymap helm-projectile-find-file-map)))
639 | (helm-projectile-define-key map
640 | (kbd "C-c d") 'helm-projectile-dired-files-delete-action)
641 | map)
642 | :help-message 'helm-ff-help-message
643 | :mode-line helm-read-file-name-mode-line-string
644 | :action helm-projectile-dired-file-actions)
645 | "Helm source definition for Projectile delete files.")
646 |
647 | (defun helm-projectile-dired-find-dir (dir)
648 | "Jump to a selected directory DIR from `helm-projectile'."
649 | (dired (expand-file-name dir (projectile-project-root)))
650 | (run-hooks 'projectile-find-dir-hook))
651 |
652 | (defun helm-projectile-dired-find-dir-other-window (dir)
653 | "Jump to a selected directory DIR from `helm-projectile'."
654 | (dired-other-window (expand-file-name dir (projectile-project-root)))
655 | (run-hooks 'projectile-find-dir-hook))
656 |
657 | (defvar helm-source-projectile-directories-list
658 | (helm-build-sync-source "Projectile directories"
659 | :candidates (lambda ()
660 | (when (projectile-project-p)
661 | (with-helm-current-buffer
662 | (let ((dirs (if projectile-find-dir-includes-top-level
663 | (append '("./") (projectile-current-project-dirs))
664 | (projectile-current-project-dirs))))
665 | (helm-projectile--files-display-real dirs (projectile-project-root))))))
666 | :fuzzy-match helm-projectile-fuzzy-match
667 | :action-transformer 'helm-find-files-action-transformer
668 | :keymap (let ((map (make-sparse-keymap)))
669 | (set-keymap-parent map helm-map)
670 | (helm-projectile-define-key map
671 | (kbd "") #'helm-previous-source
672 | (kbd "") #'helm-next-source
673 | (kbd "C-c o") #'helm-projectile-dired-find-dir-other-window
674 | (kbd "M-e") #'helm-projectile-switch-to-shell
675 | (kbd "C-c f") #'helm-projectile-dired-files-new-action
676 | (kbd "C-c a") #'helm-projectile-dired-files-add-action
677 | (kbd "C-s") #'helm-projectile-grep)
678 | map)
679 | :help-message 'helm-ff-help-message
680 | :mode-line helm-read-file-name-mode-line-string
681 | :action '(("Open Dired" . helm-projectile-dired-find-dir)
682 | ("Open Dired in other window `C-c o'" . helm-projectile-dired-find-dir)
683 | ("Switch to Eshell `M-e'" . helm-projectile-switch-to-shell)
684 | ("Grep in projects `C-s'" . helm-projectile-grep)
685 | ("Create Dired buffer from files `C-c f'" . helm-projectile-dired-files-new-action)
686 | ("Add files to Dired buffer `C-c a'" . helm-projectile-dired-files-add-action)))
687 | "Helm source for listing project directories.")
688 |
689 | (defvar helm-projectile-buffers-list-cache nil)
690 |
691 | (defclass helm-source-projectile-buffer (helm-source-sync helm-type-buffer)
692 | ((init :initform (lambda ()
693 | ;; Issue #51 Create the list before `helm-buffer' creation.
694 | (setq helm-projectile-buffers-list-cache
695 | (ignore-errors (remove (buffer-name) (projectile-project-buffer-names))))
696 | (let ((result (cl-loop for b in helm-projectile-buffers-list-cache
697 | maximize (length b) into len-buf
698 | maximize (length (with-current-buffer b
699 | (symbol-name major-mode)))
700 | into len-mode
701 | finally return (cons len-buf len-mode))))
702 | (unless helm-buffer-max-length
703 | (setq helm-buffer-max-length (car result)))
704 | (unless helm-buffer-max-len-mode
705 | ;; If a new buffer is longer that this value
706 | ;; this value will be updated
707 | (setq helm-buffer-max-len-mode (cdr result))))))
708 | (candidates :initform 'helm-projectile-buffers-list-cache)
709 | (matchplugin :initform nil)
710 | (match :initform 'helm-buffers-match-function)
711 | (persistent-action :initform 'helm-buffers-list-persistent-action)
712 | (keymap :initform 'helm-buffer-map)
713 | (volatile :initform t)
714 | (persistent-help
715 | :initform
716 | "Show this buffer / C-u \\[helm-execute-persistent-action]: Kill this buffer")))
717 |
718 | (defvar helm-source-projectile-buffers-list (helm-make-source "Project buffers" 'helm-source-projectile-buffer))
719 |
720 | (defvar helm-source-projectile-recentf-list
721 | (helm-build-sync-source "Projectile recent files"
722 | :candidates (lambda ()
723 | (when (projectile-project-p)
724 | (with-helm-current-buffer
725 | (helm-projectile--files-display-real (projectile-recentf-files)
726 | (projectile-project-root)))))
727 | :fuzzy-match helm-projectile-fuzzy-match
728 | :keymap helm-projectile-find-file-map
729 | :help-message 'helm-ff-help-message
730 | :mode-line helm-read-file-name-mode-line-string
731 | :action helm-projectile-file-actions
732 | :persistent-action #'helm-projectile-file-persistent-action
733 | :persistent-help "Preview file")
734 | "Helm source definition for recent files in current project.")
735 |
736 | (defvar helm-source-projectile-files-and-dired-list
737 | '(helm-source-projectile-dired-files-list
738 | helm-source-projectile-files-list))
739 |
740 | (defvar helm-source-projectile-directories-and-dired-list
741 | '(helm-source-projectile-dired-files-list
742 | helm-source-projectile-directories-list))
743 |
744 | (defcustom helm-projectile-git-grep-command
745 | "git --no-pager grep --no-color -n%c -e %p -- %f"
746 | "Command to execute when performing `helm-grep' inside a projectile git project.
747 | See documentation of `helm-grep-default-command' for the format."
748 | :type 'string
749 | :group 'helm-projectile
750 | )
751 |
752 | (defcustom helm-projectile-grep-command
753 | "grep -a -r %e -n%cH -e %p %f ."
754 | "Command to execute when performing `helm-grep' outside a projectile git project.
755 | See documentation of `helm-grep-default-command' for the format."
756 | :type 'string
757 | :group 'helm-projectile
758 | )
759 |
760 |
761 | (defcustom helm-projectile-sources-list
762 | '(helm-source-projectile-buffers-list
763 | helm-source-projectile-files-list
764 | helm-source-projectile-projects)
765 | "Default sources for `helm-projectile'."
766 | :type '(repeat symbol)
767 | :group 'helm-projectile)
768 |
769 | (defmacro helm-projectile-command (command source prompt &optional not-require-root truncate-lines-var)
770 | "Template for generic `helm-projectile' commands.
771 | COMMAND is a command name to be appended with \"helm-projectile\" prefix.
772 | SOURCE is a Helm source that should be Projectile specific.
773 | PROMPT is a string for displaying as a prompt.
774 | NOT-REQUIRE-ROOT specifies the command doesn't need to be used in a
775 | project root.
776 | TRUNCATE-LINES-VAR is the symbol used dictate truncation of lines.
777 | Defaults is `helm-projectile-truncate-lines'."
778 | (unless truncate-lines-var (setq truncate-lines-var 'helm-projectile-truncate-lines))
779 | `(defun ,(intern (concat "helm-projectile-" command)) (&optional arg)
780 | "Use projectile with Helm for finding files in project
781 |
782 | With a prefix ARG invalidates the cache first."
783 | (interactive "P")
784 | (if (projectile-project-p)
785 | (projectile-maybe-invalidate-cache arg)
786 | (unless ,not-require-root
787 | (error "You're not in a project")))
788 | (let ((helm-ff-transformer-show-only-basename nil)
789 | ;; for consistency, we should just let Projectile take care of ignored files
790 | (helm-boring-file-regexp-list nil))
791 | (helm :sources ,source
792 | :buffer (concat "*helm projectile: " (projectile-project-name) "*")
793 | :truncate-lines ,truncate-lines-var
794 | :prompt (projectile-prepend-project-name ,prompt)))))
795 |
796 | (helm-projectile-command "switch-project" 'helm-source-projectile-projects "Switch to project: " t)
797 | (helm-projectile-command "find-file" helm-source-projectile-files-and-dired-list "Find file: ")
798 | (helm-projectile-command "find-file-in-known-projects" 'helm-source-projectile-files-in-all-projects-list "Find file in projects: " t)
799 | (helm-projectile-command "find-dir" helm-source-projectile-directories-and-dired-list "Find dir: ")
800 | (helm-projectile-command "recentf" 'helm-source-projectile-recentf-list "Recently visited file: ")
801 | (helm-projectile-command "switch-to-buffer" 'helm-source-projectile-buffers-list "Switch to buffer: " nil helm-buffers-truncate-lines)
802 | (helm-projectile-command "browse-dirty-projects" 'helm-source-projectile-dirty-projects "Select a project: " t)
803 |
804 | (defun helm-projectile--files-display-real (files root)
805 | "Create (DISPLAY . REAL) pairs with FILES and ROOT.
806 |
807 | DISPLAY is the short file name. REAL is the full path."
808 | (cl-loop for display in files
809 | collect (cons display (expand-file-name display root))))
810 |
811 | ;;;###autoload
812 | (defun helm-projectile-find-file-dwim ()
813 | "Find file at point based on context."
814 | (interactive)
815 | (let* ((project-root (projectile-project-root))
816 | (project-files (projectile-current-project-files))
817 | (files (projectile-select-files project-files)))
818 | (if (= (length files) 1)
819 | (find-file (expand-file-name (car files) (projectile-project-root)))
820 | (helm :sources (helm-build-sync-source "Projectile files"
821 | :candidates (if (> (length files) 1)
822 | (helm-projectile--files-display-real files project-root)
823 | (helm-projectile--files-display-real project-files project-root))
824 | :fuzzy-match helm-projectile-fuzzy-match
825 | :action-transformer 'helm-find-files-action-transformer
826 | :keymap helm-projectile-find-file-map
827 | :help-message helm-ff-help-message
828 | :mode-line helm-read-file-name-mode-line-string
829 | :action helm-projectile-file-actions
830 | :persistent-action #'helm-projectile-file-persistent-action
831 | :persistent-help "Preview file")
832 | :buffer "*helm projectile*"
833 | :truncate-lines helm-projectile-truncate-lines
834 | :prompt (projectile-prepend-project-name "Find file: ")))))
835 |
836 | ;;;###autoload
837 | (defun helm-projectile-find-other-file (&optional flex-matching)
838 | "Switch between files with the same name but different extensions using Helm.
839 | With FLEX-MATCHING, match any file that contains the base name of
840 | current file. Other file extensions can be customized with the
841 | variable `projectile-other-file-alist'."
842 | (interactive "P")
843 | (let* ((project-root (projectile-project-root))
844 | (other-files (projectile-get-other-files (buffer-file-name)
845 | flex-matching)))
846 | (if other-files
847 | (if (= (length other-files) 1)
848 | (find-file (expand-file-name (car other-files) project-root))
849 | (progn
850 | (let* ((helm-ff-transformer-show-only-basename nil))
851 | (helm :sources (helm-build-sync-source "Projectile other files"
852 | :candidates (helm-projectile--files-display-real other-files project-root)
853 | :keymap helm-projectile-find-file-map
854 | :help-message helm-ff-help-message
855 | :mode-line helm-read-file-name-mode-line-string
856 | :action helm-projectile-file-actions
857 | :persistent-action #'helm-projectile-file-persistent-action
858 | :persistent-help "Preview file")
859 | :buffer "*helm projectile*"
860 | :truncate-lines helm-projectile-truncate-lines
861 | :prompt (projectile-prepend-project-name "Find other file: ")))))
862 | (error "No other file found"))))
863 |
864 | (defcustom helm-projectile-ignore-strategy 'projectile
865 | "Allow projectile to compute ignored files and directories.
866 |
867 | When set to `projectile', the package will compute ignores and
868 | explicitely add additionally command line arguments to the search
869 | tool. Note that this might override search tool specific
870 | behaviors (for instance ag would not use VCS ignore files).
871 |
872 | When set to `search-tool', the above does not happen."
873 | :group 'helm-projectile
874 | :type '(choice (const :tag "Allow projectile to compute ignores" projectile)
875 | (const :tag "Let the search tool compute ignores" search-tool)))
876 |
877 | (defun helm-projectile--projectile-ignore-strategy ()
878 | "True if the ignore strategy is `projectile'."
879 | (eq 'projectile helm-projectile-ignore-strategy))
880 |
881 | (defun helm-projectile--ignored-files ()
882 | "Compute ignored files."
883 | (cl-union (projectile-ignored-files-rel) grep-find-ignored-files))
884 |
885 | (defun helm-projectile--ignored-directories ()
886 | "Compute ignored directories."
887 | (cl-union (projectile-ignored-directories-rel) grep-find-ignored-directories))
888 |
889 | (defcustom helm-projectile-grep-or-ack-actions
890 | '("Find file" helm-grep-action
891 | "Find file other frame" helm-grep-other-frame
892 | (lambda () (and (locate-library "elscreen")
893 | "Find file in Elscreen"))
894 | helm-grep-jump-elscreen
895 | "Save results in grep buffer" helm-grep-save-results
896 | "Find file other window" helm-grep-other-window)
897 | "Available actions for `helm-projectile-grep-or-ack'.
898 | The contents of this list are passed as the arguments to `helm-make-actions'."
899 | :type 'symbol
900 | :group 'helm-projectile)
901 |
902 | (defcustom helm-projectile-set-input-automatically t
903 | "If non-nil, attempt to set search input automatically.
904 | Automatic input selection uses the region (if there is an active
905 | region), otherwise it uses the current symbol at point (if there
906 | is one). Applies to `helm-projectile-grep' and
907 | `helm-projectile-ack' only. If the `helm-ag' package is
908 | installed, then automatic input behavior for `helm-projectile-ag'
909 | can be customized using `helm-ag-insert-at-point'."
910 | :group 'helm-projectile
911 | :type 'boolean)
912 |
913 | (defun helm-projectile-grep-or-ack (&optional dir use-ack-p ack-ignored-pattern ack-executable)
914 | "Perform helm-grep at project root.
915 | DIR directory where to search
916 | USE-ACK-P indicates whether to use ack or not.
917 | ACK-IGNORED-PATTERN is a file regex to exclude from searching.
918 | ACK-EXECUTABLE is the actual ack binary name.
919 | It is usually \"ack\" or \"ack-grep\".
920 | If it is nil, or ack/ack-grep not found then use default grep command."
921 | (when (helm-projectile--projectile-ignore-strategy)
922 | (setq helm-grep-ignored-files (helm-projectile--ignored-files)
923 | helm-grep-ignored-directories (mapcar 'directory-file-name (helm-projectile--ignored-directories))))
924 |
925 | (let* ((default-directory (or dir (projectile-project-root)))
926 | (helm-ff-default-directory default-directory)
927 | (helm-grep-in-recurse t)
928 | (helm-grep-default-command (if use-ack-p
929 | (concat ack-executable " -H --no-group --no-color "
930 | (when ack-ignored-pattern (concat ack-ignored-pattern " "))
931 | "%p %f")
932 | (if (and projectile-use-git-grep (eq (projectile-project-vcs) 'git))
933 | helm-projectile-git-grep-command
934 | helm-projectile-grep-command)))
935 | (helm-grep-default-recurse-command helm-grep-default-command))
936 |
937 | (setq helm-source-grep
938 | (helm-build-async-source
939 | (capitalize (helm-grep-command t))
940 | :header-name (lambda (_name)
941 | (let ((name (if use-ack-p
942 | "Helm Projectile Ack"
943 | "Helm Projectile Grep")))
944 | (concat name " " "(C-c ? Help)")))
945 | :candidates-process 'helm-grep-collect-candidates
946 | :filter-one-by-one 'helm-grep-filter-one-by-one
947 | :candidate-number-limit 9999
948 | :nohighlight t
949 | ;; We need to specify keymap here and as :keymap arg [1]
950 | ;; to make it available in further resuming.
951 | :keymap helm-grep-map
952 | :history 'helm-grep-history
953 | :action (apply #'helm-make-actions helm-projectile-grep-or-ack-actions)
954 | :persistent-action 'helm-grep-persistent-action
955 | :persistent-help "Jump to line (`C-u' Record in mark ring)"
956 | :requires-pattern 2))
957 | (helm
958 | :sources 'helm-source-grep
959 | :input (when helm-projectile-set-input-automatically
960 | (if (region-active-p)
961 | (buffer-substring-no-properties (region-beginning) (region-end))
962 | (thing-at-point 'symbol)))
963 | :buffer (format "*helm %s*" (if use-ack-p
964 | "ack"
965 | "grep"))
966 | :default-directory default-directory
967 | :keymap helm-grep-map
968 | :history 'helm-grep-history
969 | :truncate-lines helm-grep-truncate-lines)))
970 |
971 | ;;;###autoload
972 | (defun helm-projectile-on ()
973 | "Turn on `helm-projectile' key bindings."
974 | (interactive)
975 | (message "Turn on helm-projectile key bindings")
976 | (helm-projectile-toggle 1))
977 |
978 | ;;;###autoload
979 | (defun helm-projectile-off ()
980 | "Turn off `helm-projectile' key bindings."
981 | (interactive)
982 | (message "Turn off helm-projectile key bindings")
983 | (helm-projectile-toggle -1))
984 |
985 | ;;;###autoload
986 | (defun helm-projectile-grep (&optional dir)
987 | "Helm version of `projectile-grep'.
988 | DIR is the project root, if not set then current directory is used"
989 | (interactive)
990 | (let ((project-root (or dir (projectile-project-root) (error "You're not in a project"))))
991 | (funcall 'run-with-timer 0.01 nil
992 | #'helm-projectile-grep-or-ack project-root nil)))
993 |
994 | ;;;###autoload
995 | (defun helm-projectile-ack (&optional dir)
996 | "Helm version of projectile-ack.
997 | DIR directory where to search"
998 | (interactive)
999 | (let* ((project-root (or dir (projectile-project-root) (error "You're not in a project")))
1000 | (ignored (when (helm-projectile--projectile-ignore-strategy)
1001 | (mapconcat
1002 | 'identity
1003 | (cl-union (mapcar (lambda (path)
1004 | (concat "--ignore-dir=" (file-name-nondirectory (directory-file-name path))))
1005 | (helm-projectile--ignored-directories))
1006 | (mapcar (lambda (path)
1007 | (concat "--ignore-file=match:" (shell-quote-argument path)))
1008 | (append (helm-projectile--ignored-files)
1009 | (projectile-patterns-to-ignore))))
1010 | " ")))
1011 | (helm-ack-grep-executable (cond
1012 | ((executable-find "ack") "ack")
1013 | ((executable-find "ack-grep") "ack-grep")
1014 | (t (error "ack or ack-grep is not available")))))
1015 | (funcall 'run-with-timer 0.01 nil
1016 | #'helm-projectile-grep-or-ack project-root t ignored helm-ack-grep-executable)))
1017 |
1018 | ;;;###autoload
1019 |
1020 | (defun helm-projectile-ag (&optional options)
1021 | "Helm version of `projectile-ag'.
1022 | OPTIONS explicit command line arguments to ag"
1023 | (interactive (if current-prefix-arg (list (helm-read-string "option: " "" 'helm-ag--extra-options-history))))
1024 | (if (require 'helm-ag nil t)
1025 | (if (projectile-project-p)
1026 | (progn
1027 | (when (helm-projectile--projectile-ignore-strategy)
1028 | (setq grep-find-ignored-files (helm-projectile--ignored-files)
1029 | grep-find-ignored-directories (helm-projectile--ignored-directories)))
1030 | (let* ((ignored (when (helm-projectile--projectile-ignore-strategy)
1031 | (mapconcat (lambda (i)
1032 | (concat "--ignore " i))
1033 | (append grep-find-ignored-files
1034 | grep-find-ignored-directories
1035 | (cadr (projectile-parse-dirconfig-file)))
1036 | " ")))
1037 | (helm-ag-base-command (concat helm-ag-base-command
1038 | (when ignored (concat " " ignored))
1039 | " " options))
1040 | (current-prefix-arg nil))
1041 | (helm-do-ag (projectile-project-root) (car (projectile-parse-dirconfig-file)))))
1042 | (error "You're not in a project"))
1043 | (when (yes-or-no-p "`helm-ag' is not installed. Install? ")
1044 | (condition-case nil
1045 | (progn
1046 | (package-install 'helm-ag)
1047 | (helm-projectile-ag options))
1048 | (error (error "`helm-ag' is not available. Is MELPA in your `package-archives'?"))))))
1049 |
1050 | ;; Declare/define these to satisfy the byte compiler
1051 | (defvar helm-rg-prepend-file-name-line-at-top-of-matches)
1052 | (defvar helm-rg-include-file-on-every-match-line)
1053 | (declare-function helm-rg "ext:helm-rg")
1054 | (declare-function helm-rg--get-thing-at-pt "ext:helm-rg")
1055 |
1056 | (defun helm-projectile-rg--region-selection ()
1057 | (when helm-projectile-set-input-automatically
1058 | (if (region-active-p)
1059 | (buffer-substring-no-properties (region-beginning) (region-end))
1060 | (helm-rg--get-thing-at-pt))))
1061 |
1062 | (defun glob-quote (string)
1063 | "Quote the special glob characters: *, ?, [, and ].
1064 | STRING the string in which to escape special characters."
1065 | (replace-regexp-in-string "[]*?[]" "\\\\\\&" string))
1066 |
1067 | ;;;###autoload
1068 | (defun helm-projectile-rg ()
1069 | "Projectile version of `helm-rg'."
1070 | (interactive)
1071 | (if (require 'helm-rg nil t)
1072 | (if (projectile-project-p)
1073 | (let* ((helm-rg-prepend-file-name-line-at-top-of-matches nil)
1074 | (helm-rg-include-file-on-every-match-line t)
1075 | (default-directory (projectile-project-root)))
1076 | (when (helm-projectile--projectile-ignore-strategy)
1077 | (setq helm-rg--extra-args
1078 | (mapconcat
1079 | 'identity
1080 | (cl-union (mapcan (lambda (path)
1081 | (list "--glob" (concat "!" (glob-quote path))))
1082 | (helm-projectile--ignored-files))
1083 | (mapcan (lambda (path)
1084 | (list "--glob" (concat "!" (glob-quote path) "/**")))
1085 | (mapcar 'directory-file-name (helm-projectile--ignored-directories))))
1086 | " ")))
1087 | (helm-rg (helm-projectile-rg--region-selection)
1088 | nil))
1089 | (error "You're not in a project"))
1090 | (when (yes-or-no-p "`helm-rg' is not installed. Install? ")
1091 | (condition-case nil
1092 | (progn
1093 | (package-install 'helm-rg)
1094 | (helm-projectile-rg))
1095 | (error "`helm-rg' is not available. Is MELPA in your `package-archives'?")))))
1096 |
1097 | (defun helm-projectile-commander-bindings ()
1098 | (def-projectile-commander-method ?a
1099 | "Run ack on project."
1100 | (call-interactively 'helm-projectile-ack))
1101 |
1102 | (def-projectile-commander-method ?A
1103 | "Find ag on project."
1104 | (call-interactively 'helm-projectile-ag))
1105 |
1106 | (def-projectile-commander-method ?f
1107 | "Find file in project."
1108 | (helm-projectile-find-file))
1109 |
1110 | (def-projectile-commander-method ?b
1111 | "Switch to project buffer."
1112 | (helm-projectile-switch-to-buffer))
1113 |
1114 | (def-projectile-commander-method ?d
1115 | "Find directory in project."
1116 | (helm-projectile-find-dir))
1117 |
1118 | (def-projectile-commander-method ?g
1119 | "Run grep on project."
1120 | (helm-projectile-grep))
1121 |
1122 | (def-projectile-commander-method ?s
1123 | "Switch project."
1124 | (helm-projectile-switch-project))
1125 |
1126 | (def-projectile-commander-method ?e
1127 | "Find recently visited file in project."
1128 | (helm-projectile-recentf))
1129 |
1130 | (def-projectile-commander-method ?V
1131 | "Find dirty projects."
1132 | (helm-projectile-browse-dirty-projects)))
1133 |
1134 | ;;;###autoload
1135 | (defun helm-projectile-toggle (toggle)
1136 | "Toggle Helm version of Projectile commands."
1137 | (if (> toggle 0)
1138 | (progn
1139 | (when (eq projectile-switch-project-action #'projectile-find-file)
1140 | (setq projectile-switch-project-action #'helm-projectile-find-file))
1141 | (define-key projectile-mode-map [remap projectile-find-other-file] #'helm-projectile-find-other-file)
1142 | (define-key projectile-mode-map [remap projectile-find-file] #'helm-projectile-find-file)
1143 | (define-key projectile-mode-map [remap projectile-find-file-in-known-projects] #'helm-projectile-find-file-in-known-projects)
1144 | (define-key projectile-mode-map [remap projectile-find-file-dwim] #'helm-projectile-find-file-dwim)
1145 | (define-key projectile-mode-map [remap projectile-find-dir] #'helm-projectile-find-dir)
1146 | (define-key projectile-mode-map [remap projectile-switch-project] #'helm-projectile-switch-project)
1147 | (define-key projectile-mode-map [remap projectile-recentf] #'helm-projectile-recentf)
1148 | (define-key projectile-mode-map [remap projectile-switch-to-buffer] #'helm-projectile-switch-to-buffer)
1149 | (define-key projectile-mode-map [remap projectile-grep] #'helm-projectile-grep)
1150 | (define-key projectile-mode-map [remap projectile-ack] #'helm-projectile-ack)
1151 | (define-key projectile-mode-map [remap projectile-ag] #'helm-projectile-ag)
1152 | (define-key projectile-mode-map [remap projectile-ripgrep] #'helm-projectile-rg)
1153 | (define-key projectile-mode-map [remap projectile-browse-dirty-projects] #'helm-projectile-browse-dirty-projects)
1154 | (helm-projectile-commander-bindings))
1155 | (progn
1156 | (when (eq projectile-switch-project-action #'helm-projectile-find-file)
1157 | (setq projectile-switch-project-action #'projectile-find-file))
1158 | (define-key projectile-mode-map [remap projectile-find-other-file] nil)
1159 | (define-key projectile-mode-map [remap projectile-find-file] nil)
1160 | (define-key projectile-mode-map [remap projectile-find-file-in-known-projects] nil)
1161 | (define-key projectile-mode-map [remap projectile-find-file-dwim] nil)
1162 | (define-key projectile-mode-map [remap projectile-find-dir] nil)
1163 | (define-key projectile-mode-map [remap projectile-switch-project] nil)
1164 | (define-key projectile-mode-map [remap projectile-recentf] nil)
1165 | (define-key projectile-mode-map [remap projectile-switch-to-buffer] nil)
1166 | (define-key projectile-mode-map [remap projectile-grep] nil)
1167 | (define-key projectile-mode-map [remap projectile-ag] nil)
1168 | (define-key projectile-mode-map [remap projectile-ripgrep] nil)
1169 | (define-key projectile-mode-map [remap projectile-browse-dirty-projects] nil)
1170 | (projectile-commander-bindings))))
1171 |
1172 | ;;;###autoload
1173 | (defun helm-projectile (&optional arg)
1174 | "Use projectile with Helm instead of ido.
1175 |
1176 | With a prefix ARG invalidates the cache first.
1177 | If invoked outside of a project, displays a list of known projects to jump."
1178 | (interactive "P")
1179 | (if (not (projectile-project-p))
1180 | (helm-projectile-switch-project arg)
1181 | (projectile-maybe-invalidate-cache arg)
1182 | (let ((helm-ff-transformer-show-only-basename nil))
1183 | (helm :sources helm-projectile-sources-list
1184 | :buffer "*helm projectile*"
1185 | :truncate-lines helm-projectile-truncate-lines
1186 | :prompt (projectile-prepend-project-name (if (projectile-project-p)
1187 | "pattern: "
1188 | "Switch to project: "))))))
1189 |
1190 | ;;;###autoload
1191 | (eval-after-load 'projectile
1192 | '(progn
1193 | (define-key projectile-command-map (kbd "h") #'helm-projectile)))
1194 |
1195 | (provide 'helm-projectile)
1196 |
1197 | ;;; helm-projectile.el ends here
1198 |
--------------------------------------------------------------------------------
/screenshots/helm-projectile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bbatsov/helm-projectile/041076e35a6663302a91a0fa672f847c7d64bf29/screenshots/helm-projectile.png
--------------------------------------------------------------------------------