Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### New features

- [#2199](https://github.com/bbatsov/projectile/pull/2199): Add `projectile-files-transform-functions`, a list of functions applied to every file name a project index produces, so names collected as raw byte escapes (non-ASCII characters under a legacy `file-name-coding-system`, e.g. Cyrillic or Turkish on Windows) can be decoded before they are cached and presented.
- [#2182](https://github.com/bbatsov/projectile/pull/2182): Add `projectile-todos-in-projects` and `projectile-multi-occur-in-projects`, so every sibling command now has a public generic taking any list of projects behind it and a command for a group of your own is a two-line wrapper for all five.
- [#2181](https://github.com/bbatsov/projectile/pull/2181): Sapling and Bazaar projects can now list their own ignored files, via the new `projectile-sapling-ignored-command` and `projectile-bzr-ignored-command`.
- [#2180](https://github.com/bbatsov/projectile/pull/2180): Every shell backend can now open in another window (`s-p x 4 <key>`), not just vterm, eat and ghostel: `shell`, `eshell`, `ielm`, `term` and the backend-dispatching `projectile-run` gained `-other-window` commands.
Expand Down
30 changes: 30 additions & 0 deletions doc/modules/ROOT/pages/indexing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,36 @@ TIP: For the details of each ignore mechanism (`.projectile`, the indexing
tools, and the global ignore/unignore variables), see
xref:ignoring.adoc[Ignoring files].

== Transforming file names

On some platforms file names containing non-ASCII characters (for
example Cyrillic or Turkish) are collected as raw byte escapes like
`\305\264\303\251`, when the file-name coding system doesn't cover
them - a common case is Emacs on Windows with a legacy system code
page. Both the file list and the persistent cache then show the
escapes instead of the actual names.

The `projectile-files-transform-functions` option applies a list of
functions to every file name Projectile collects for a project. The
functions run when the project is (re)indexed - before the list is
cached or presented - so commands such as `projectile-find-file` and
the persistent cache hold the transformed, readable names:

[source,elisp]
----
;; Decode raw-byte file names as UTF-8.
(setq projectile-files-transform-functions
(list (lambda (file) (decode-coding-string file 'utf-8))))
----

The default value is nil: file names are used as collected and no
extra work is done.

NOTE: The transforms run only during (re)indexing. Already cached
file lists keep their previous representation, so after changing the
option clear the cache with `M-x projectile-invalidate-cache`
(`s-p i`).

== Sorting

You can choose how Projectile sorts files by customizing `projectile-sort-order`.
Expand Down
44 changes: 44 additions & 0 deletions projectile.el
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,33 @@ A value of nil means the cache never expires."
(natnum :tag "Seconds"))
:package-version '(projectile . "1.0.0"))

(defcustom projectile-files-transform-functions nil
"Functions applied to each file name produced by project indexing.

Each function is called with a file name and must return a file
name. The functions are applied, in the order they appear in the
list, to every file name Projectile collects for the current
project - before the list is cached and presented in commands such
as `projectile-find-file'.

This is useful on platforms where file names containing non-ASCII
characters (e.g. Cyrillic or Turkish) are collected as raw byte
escapes like \"\\305\\264\\303\\251\", because the file-name coding
system doesn't cover them - for instance Emacs on Windows with a
legacy system code page. Decoding on collection keeps both the
file list and the persistent cache readable:

(setq projectile-files-transform-functions
(list (lambda (file) (decode-coding-string file 'utf-8))))

The default value is nil - file names are used as collected and no
extra work is done. The functions run only when a project is
(re)indexed, so after changing this option clear the cache with
`projectile-invalidate-cache' to re-index with the new transforms."
:group 'projectile
:type 'hook
:package-version '(projectile . "3.5.0"))

(define-obsolete-variable-alias 'projectile-auto-discover
'projectile-auto-discover-projects "3.4.0")

Expand Down Expand Up @@ -5115,6 +5142,19 @@ CALLER is accepted for backward compatibility but no longer used."

;;; Listing a project's files and directories

(defun projectile--transform-file-names (files)
"Apply `projectile-files-transform-functions' to every name in FILES.
Return FILES unchanged when no transform is configured."
(if (null projectile-files-transform-functions)
files
(mapcar
(lambda (file)
(let ((result file))
(dolist (fun projectile-files-transform-functions)
(setq result (funcall fun result)))
result))
files)))

(defun projectile-project-files (project-root)
"Return a list of files for the PROJECT-ROOT."
(let (files)
Expand Down Expand Up @@ -5194,6 +5234,10 @@ CALLER is accepted for backward compatibility but no longer used."
files))))
dirs))))))

;; Apply the user-configured transforms to every collected name,
;; so both the cache and the callers see the final representation.
(setq files (projectile--transform-file-names files))

;; Save the cached list.
(when projectile-enable-caching
(projectile-cache-project project-root files)
Expand Down
26 changes: 26 additions & 0 deletions test/projectile-indexing-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1377,4 +1377,30 @@
(projectile-alien-honors-ignores nil))
(expect (projectile-project-files root) :to-contain "docs/a.text")))))))

(describe "projectile-files-transform-functions"
(it "passes file names through when no transform is configured"
(let (projectile-files-transform-functions)
(expect (projectile--transform-file-names '("a.txt" "b/c.txt"))
:to-equal '("a.txt" "b/c.txt"))))
(it "applies the configured functions in order"
(let ((projectile-files-transform-functions
(list (lambda (file) (concat "x/" file))
#'upcase)))
(expect (projectile--transform-file-names '("a.txt" "b/c.txt"))
:to-equal '("X/A.TXT" "X/B/C.TXT"))))
(it "transforms collected file names before they are cached"
(spy-on 'projectile-project-vcs :and-return-value 'git)
(spy-on 'projectile-get-project-directories :and-return-value '("/my/root/"))
(spy-on 'projectile--dir-files-alien-maybe-async :and-return-value '("a.txt" "b.txt"))
(spy-on 'projectile--alien-apply-ignores :and-call-fake
(lambda (_root _vcs files) files))
(spy-on 'projectile-cache-project)
(let ((projectile-indexing-method 'alien)
(projectile-enable-caching t)
(projectile-files-transform-functions (list #'upcase)))
(expect (projectile-project-files "/my/root/") :to-equal '("A.TXT" "B.TXT"))
(expect 'projectile-cache-project :to-have-been-called-times 1)
(expect 'projectile-cache-project
:to-have-been-called-with "/my/root/" '("A.TXT" "B.TXT")))))

;;; projectile-indexing-test.el ends here