Skip to content

Commit 15bb6ac

Browse files
committed
Add __DIR__ path completion capf and php-dot-context primitive
Redefine php-complete.el as a small collection of dependency-light, offline completion-at-point functions usable both as `M-x` commands and as building blocks for `cape-capf-super'. * lisp/php.el (php-dot-context): New dependency-free primitive that classifies the context before point (`string-or-comment', `next-to-string', `code') for context-sensitive `.' insertion, without cc-engine. Meant to drive smartchr/key-combo setups. * lisp/php-complete.el: State the file's mission in the Commentary. (php-complete-complete-function): Redefine as the offline built-in function-name source (behavior unchanged). (php-complete-path-dir-constants): New option; directory-valued subset of `php-magical-constants'. (php-complete-complete-path): New capf completing a filesystem path in the `__DIR__ . '/...'' idiom, one component at a time, rooted at the directory of the current file. Robust to the unterminated string that is normal while typing. * tests/php-mode-test.el: Cover `php-dot-context' and `php-complete-complete-path'. * README.md, README.ja.md: Document the completion capfs, `cape-capf-super' composition, and a `php-dot-context'-based smartchr recipe for `.'.
1 parent 749fdd5 commit 15bb6ac

5 files changed

Lines changed: 277 additions & 3 deletions

File tree

README.ja.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,51 @@ M-x package-install php-mode
8686

8787
インデントエンジンはもはやCC Modeを使わないため、`php-mode`では`c-basic-offset`はobsoleteです。かわりに`php-indent-offset`をカスタマイズしてください。後方互換のため、プロジェクトが`c-basic-offset`をバッファローカルに設定している場合(`.dir-locals.el`やファイルローカル変数など)は、`php-mode`がその値を`php-indent-offset`に引き継ぎ、警告を表示します。
8888

89+
## 補完
90+
91+
`php-complete.el`は、言語サーバーなしでも使える依存の軽い小さな`completion-at-point`関数(capf)をいくつか提供します。いずれも`M-x`コマンドとしても、[`cape`][cape]`cape-capf-super`で合成する部品としても使えます。
92+
93+
- `php-complete-complete-function` — 組み込み関数名。
94+
- `php-complete-complete-path``__DIR__ . '/...'`イディオムの中でパスを補完します。1階層ずつ辿り、起点は現在のファイルのディレクトリ(実行時に`__DIR__`が解決する先)です。
95+
96+
```elisp
97+
(add-hook 'php-mode-hook
98+
(lambda ()
99+
(add-hook 'completion-at-point-functions
100+
#'php-complete-complete-path nil t)))
101+
102+
;; …または cape で複数のオフラインソースを1つの super-capf に合成する:
103+
(add-hook 'php-mode-hook
104+
(lambda ()
105+
(add-hook 'completion-at-point-functions
106+
(cape-capf-super #'php-complete-complete-function
107+
#'php-complete-complete-path)
108+
nil t)))
109+
```
110+
111+
### 文脈依存の `.` キー
112+
113+
`__DIR__`とパス補completionを橋渡しする`. '/'`の挿入は、あえてcapfの仕事にはしていません。これは編集環境側に委ねます。そのための primitive が`php-dot-context`です。ポイントが文字列/コメント内か(`string-or-comment`)、文字列リテラルや`__DIR__`のようなマジック定数の直後か(`next-to-string`)、素のコードか(`code`)を返します。capf とこの primitive は「文字列」と「マジック定数」の定義を共有するため、キー挿入と補completionの挙動が食い違いません。
114+
115+
たとえば[smartchr][smartchr]では、`.`キーをコード中では`->` / `.` / `. `で循環させ、文字列内ではリテラルの`.`を挿入し、`__DIR__`の直後では`. `を優先(そのまま`php-complete-complete-path`につながる)といった設定が書けます。
116+
117+
```elisp
118+
(defun my-php-smartchr-dot (code within-string next-to-string)
119+
"`php-dot-context' を使って `.' キーの smartchr を作る。"
120+
(let ((select (lambda ()
121+
(pcase (php-dot-context)
122+
('string-or-comment within-string)
123+
('next-to-string next-to-string)
124+
(_ code)))))
125+
(smartchr-make-struct
126+
:cleanup-fn (lambda () (delete-char (- (length (funcall select)))))
127+
:insert-fn (lambda () (insert (funcall select))))))
128+
129+
;; (smartchr (my-php-smartchr-dot "->" "." ". ")
130+
;; (my-php-smartchr-dot ". " ".." "..")
131+
;; "...")
132+
```
133+
89134
## HTMLとPHPが混在するファイルの編集
90135

91136
`php-mode`は純粋なPHPスクリプトのためのメジャーモードです。テンプレートのようにHTMLの中にPHPを埋め込んだファイルは、両方の言語を理解するメジャーモードで編集するほうが適しています。特にインデントは、HTML部分を素の`php-mode`で編集すると正しく動作しません。
@@ -168,9 +213,11 @@ PHP Modeは[GNU General Public License Version 3][gpl-v3] (GPLv3) でライセ
168213
[Authors]: https://github.com/emacs-php/php-mode/wiki/Authors
169214
[Contributors]: https://github.com/emacs-php/php-mode/graphs/contributors
170215
[Supported Version]: https://github.com/emacs-php/php-mode/wiki/Supported-Version
216+
[cape]: https://github.com/minad/cape
171217
[cc-mode-manual]: https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html
172218
[gpl-v3]: https://www.gnu.org/licenses/gpl-3.0
173219
[per-cs]: https://www.php-fig.org/per/coding-style/
220+
[smartchr]: https://github.com/imakado/emacs-smartchr
174221
[#811]: https://github.com/emacs-php/php-mode/issues/811
175222
[nongnu-elpa-badge]: https://elpa.nongnu.org/nongnu/php-mode.svg
176223
[nongnu-elpa]: https://elpa.nongnu.org/nongnu/php-mode.html

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,51 @@ The `symfony2` style has been removed, since PER supersedes the coding style use
8888

8989
Because the indentation engine no longer uses CC Mode, `c-basic-offset` is obsolete in `php-mode`; customize `php-indent-offset` instead. For backward compatibility, when a project sets `c-basic-offset` buffer-locally (for example via `.dir-locals.el` or file-local variables), `php-mode` copies that value into `php-indent-offset` and displays a warning.
9090

91+
## Completion
92+
93+
`php-complete.el` provides a few small, dependency-light `completion-at-point` functions (capfs) for use without a language server. Each is usable both as an `M-x` command and as a building block for [`cape`][cape]'s `cape-capf-super`:
94+
95+
- `php-complete-complete-function` — built-in function names.
96+
- `php-complete-complete-path` — a filesystem path inside the `__DIR__ . '/...'` idiom, completed one component at a time and rooted at the directory of the current file (what `__DIR__` resolves to at runtime).
97+
98+
```elisp
99+
(add-hook 'php-mode-hook
100+
(lambda ()
101+
(add-hook 'completion-at-point-functions
102+
#'php-complete-complete-path nil t)))
103+
104+
;; …or compose several offline sources into one super-capf with cape:
105+
(add-hook 'php-mode-hook
106+
(lambda ()
107+
(add-hook 'completion-at-point-functions
108+
(cape-capf-super #'php-complete-complete-function
109+
#'php-complete-complete-path)
110+
nil t)))
111+
```
112+
113+
### A context-sensitive `.` key
114+
115+
Inserting the `. '/'` that bridges `__DIR__` into path completion is deliberately *not* the capf's job; it is left to your editing setup. `php-dot-context` is the primitive for that: it reports whether point is inside a string or comment (`string-or-comment`), directly after a string literal or a magic constant such as `__DIR__` (`next-to-string`), or in plain code (`code`). Because the capf and this primitive share the same notion of "string" and "magic constant", key-driven insertion and completion stay consistent.
116+
117+
For example, with [smartchr][smartchr] the `.` key can cycle `->` / `.` / `. ` in code, insert a literal `.` inside strings, and prefer `. ` right after `__DIR__` (which then flows straight into `php-complete-complete-path`):
118+
119+
```elisp
120+
(defun my-php-smartchr-dot (code within-string next-to-string)
121+
"Build a smartchr for the `.' key using `php-dot-context'."
122+
(let ((select (lambda ()
123+
(pcase (php-dot-context)
124+
('string-or-comment within-string)
125+
('next-to-string next-to-string)
126+
(_ code)))))
127+
(smartchr-make-struct
128+
:cleanup-fn (lambda () (delete-char (- (length (funcall select)))))
129+
:insert-fn (lambda () (insert (funcall select))))))
130+
131+
;; (smartchr (my-php-smartchr-dot "->" "." ". ")
132+
;; (my-php-smartchr-dot ". " ".." "..")
133+
;; "...")
134+
```
135+
91136
## Editing files that mix HTML and PHP
92137

93138
`php-mode` is designed for pure PHP scripts. Files that embed PHP inside HTML, such as templates, are better edited in a major mode that understands both languages. Indentation in particular is unreliable when the HTML part of a file is edited in plain `php-mode`.
@@ -169,8 +214,10 @@ This project was maintained by [Eric James Michael Ritz][@ejmr] until 2017. Curr
169214
[Authors]: https://github.com/emacs-php/php-mode/wiki/Authors
170215
[Contributors]: https://github.com/emacs-php/php-mode/graphs/contributors
171216
[Supported Version]: https://github.com/emacs-php/php-mode/wiki/Supported-Version
217+
[cape]: https://github.com/minad/cape
172218
[cc-mode-manual]: https://www.gnu.org/software/emacs/manual/html_mono/ccmode.html
173219
[gpl-v3]: https://www.gnu.org/licenses/gpl-3.0
220+
[smartchr]: https://github.com/imakado/emacs-smartchr
174221
[per-cs]: https://www.php-fig.org/per/coding-style/
175222
[#811]: https://github.com/emacs-php/php-mode/issues/811
176223
[nongnu-elpa-badge]: https://elpa.nongnu.org/nongnu/php-mode.svg

lisp/php-complete.el

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,23 @@
2424

2525
;;; Commentary:
2626

27-
;; Provide auto-compiletion functions.
27+
;; php-complete.el provides a small collection of dependency-light,
28+
;; offline completion-at-point functions (capfs) for PHP. It is not an
29+
;; LSP replacement; it targets the cases an LSP server cannot or will not
30+
;; handle well. Each capf is a small, independent unit usable both as an
31+
;; `M-x' command and as a building block composed with `cape-capf-super':
32+
;;
33+
;; - `php-complete-complete-function' -- built-in function name source.
34+
;; - `php-complete-complete-path' -- filesystem path inside the
35+
;; `__DIR__ . '/...'' idiom.
36+
;;
37+
;; Key-driven insertion (e.g. a context-sensitive "." key via smartchr)
38+
;; is intentionally kept out of this file; it belongs to the orthogonal
39+
;; primitive `php-dot-context'. Both share the same notion of "string"
40+
;; and "magic constant", so insertion and completion stay consistent.
2841

29-
;; These functions are copied function from GNU ELPA.
42+
;; The following helpers are copied from cape.el on GNU ELPA; thanks to
43+
;; the original author Daniel Mendler (@minad).
3044
;;
3145
;; - cape--table-with-properties (cape.el)
3246
;; - cape--bounds (cape.el)
@@ -52,6 +66,17 @@
5266
php-defs-functions-alist)))
5367
:safe (lambda (value) (and (listp value) (cl-loop for v in values
5468
always (assq v php-defs-functions-alist)))))
69+
70+
;;;###autoload
71+
(defcustom php-complete-path-dir-constants '("__DIR__")
72+
"Magic constants treated as the current file directory for path completion.
73+
74+
This is the directory-valued subset of `php-magical-constants' used by
75+
`php-complete-complete-path'. Only constants that resolve to a directory
76+
belong here: \"__DIR__\" qualifies, whereas \"__FILE__\" (a file) does not."
77+
:tag "PHP Complete Path Dir Constants"
78+
:type '(repeat string)
79+
:group 'php-complete)
5580

5681
;;; Cape functions:
5782

@@ -101,7 +126,13 @@ SORT should be nil to disable sorting."
101126

102127
;;;###autoload
103128
(defun php-complete-complete-function (&optional interactive)
104-
"Complete PHP keyword at point.
129+
"Complete a PHP built-in function name at point.
130+
131+
This is the offline built-in function-name source: it offers names from
132+
the modules listed in `php-complete-function-modules', and is meant for
133+
environments without an LSP server. It does not fire after `->' or `::',
134+
nor after a variable, so it only suggests where a bare function call makes
135+
sense.
105136
106137
If INTERACTIVE is nil the function acts like a capf."
107138
(interactive (list t))
@@ -119,5 +150,77 @@ If INTERACTIVE is nil the function acts like a capf."
119150
:company-kind (lambda (_) 'keyword)
120151
:exclusive 'no))))
121152

153+
;;; Path completion rooted at `__DIR__':
154+
155+
(defun php-complete--path-directory ()
156+
"Return the directory that `__DIR__' resolves to for the current buffer."
157+
(or (and buffer-file-name (file-name-directory buffer-file-name))
158+
default-directory))
159+
160+
(defun php-complete--path-string-bounds ()
161+
"Return (CONTENT-BEG . STR-END) when point is inside a `__DIR__ . STRING'.
162+
163+
CONTENT-BEG is placed after the opening quote and a single leading slash,
164+
so the path is completed relative to the directory of the current file.
165+
Return nil when point is not inside such a string.
166+
167+
The recognized directory-valued constants are held in
168+
`php-complete-path-dir-constants', a subset of `php-magical-constants',
169+
so this shares its notion of \"magic constant\" with `php-dot-context'."
170+
(when (php-in-string-p)
171+
;; Take the string start from `syntax-ppss' (nth 8): it is reliable even
172+
;; for the unterminated string that is normal while typing
173+
;; ("__DIR__ . '/" before the closing quote exists). Match the preceding
174+
;; "CONST ." with `looking-back' rather than the token scanner, which is
175+
;; not meant to be entered from the opening-quote position.
176+
(let ((str-beg (nth 8 (syntax-ppss))))
177+
(when (save-excursion
178+
(goto-char str-beg)
179+
(looking-back
180+
(concat (regexp-opt php-complete-path-dir-constants 'symbols)
181+
"[ \t\r\n]*\\.[ \t\r\n]*")
182+
(max (point-min) (- str-beg 120))))
183+
(let ((content-beg (1+ str-beg)))
184+
;; Keep a single leading "/" fixed so it stays a separator and the
185+
;; path resolves relative to `__DIR__' instead of the filesystem root.
186+
(when (eq (char-after content-beg) ?/)
187+
(setq content-beg (1+ content-beg)))
188+
(cons content-beg
189+
(or (ignore-errors
190+
(save-excursion (goto-char str-beg) (forward-sexp) (point)))
191+
(point))))))))
192+
193+
(defun php-complete--path-table ()
194+
"Return a file-name completion table rooted at the current file directory.
195+
The directory is what `__DIR__' resolves to at runtime, bound when the
196+
table is called rather than captured from the buffer `default-directory'."
197+
(let ((dir (php-complete--path-directory)))
198+
(lambda (string pred action)
199+
(let ((default-directory dir)
200+
(non-essential t))
201+
(read-file-name-internal string pred action)))))
202+
203+
;;;###autoload
204+
(defun php-complete-complete-path (&optional interactive)
205+
"Complete a filesystem path written as `__DIR__ . \\='/...\\='.'
206+
207+
Inside the string of `__DIR__ . \\='/PATH\\='', complete PATH from the
208+
directory of the current file, one path component at a time. This is the
209+
completion half of the `__DIR__' path idiom; inserting the leading
210+
`. \\='/\\='' is left to the editor (see `php-dot-context' and the smartchr
211+
recipe in the README), keeping key-driven insertion and
212+
completion-at-point orthogonal but consistent.
213+
214+
If INTERACTIVE is nil the function acts like a capf."
215+
(interactive (list t))
216+
(if interactive
217+
(php-complete--cape-interactive #'php-complete-complete-path)
218+
(when-let* ((bounds (php-complete--path-string-bounds)))
219+
`(,(min (car bounds) (point)) ,(point)
220+
,(php-complete--path-table)
221+
:annotation-function ,(lambda (_) " __DIR__ path")
222+
:company-kind ,(lambda (_) 'file)
223+
:exclusive no))))
224+
122225
(provide 'php-complete)
123226
;;; php-complete.el ends here

lisp/php.el

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,32 @@ The order is reversed by calling as follows:
703703
`find-tag-default' from GNU Emacs etags.el."
704704
(car (php-leading-tokens 1)))
705705

706+
(defun php-dot-context ()
707+
"Classify the context immediately before point for the \".\" key.
708+
709+
Return one of the following symbols:
710+
711+
`string-or-comment' -- point is inside a string or comment.
712+
`next-to-string' -- the preceding token is a string literal or one of
713+
`php-magical-constants' (for example, point follows
714+
\"__DIR__\"), so a concatenation such as \". \" reads
715+
naturally.
716+
`code' -- anything else.
717+
718+
This is a dependency-free primitive meant to drive context-sensitive
719+
insertion of the \".\" key with packages such as smartchr or key-combo.
720+
It shares its notion of \"string\" and \"magic constant\" with
721+
`php-complete-complete-path', so key-driven insertion and
722+
completion-at-point stay consistent. See the README for a recipe."
723+
(cond
724+
((php-in-string-or-comment-p) 'string-or-comment)
725+
((when-let* ((token (car (php-leading-tokens 1))))
726+
(or (string-prefix-p "'" token)
727+
(string-prefix-p "\"" token)
728+
(member token php-magical-constants)))
729+
'next-to-string)
730+
(t 'code)))
731+
706732
;;; Provide support for Flymake so that users can see warnings and
707733
;;; errors in real-time as they write code.
708734
(defun php-flymake-php-init ()

tests/php-mode-test.el

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
(require 'php)
3434
(require 'php-mode)
3535
(require 'php-project)
36+
(require 'php-complete)
3637
(require 'ert)
3738
(require 'cl-lib)
3839
(require 'imenu)
@@ -907,6 +908,56 @@ the same logic in an object."
907908
(with-php-mode-test ("indent/issue-227.php" :indent t :magic t :style pear))
908909
(with-php-mode-test ("indent/issue-774.php" :indent t :magic t :style pear)))
909910

911+
(ert-deftest php-mode-test-dot-context ()
912+
"`php-dot-context' classifies the context immediately before point."
913+
(cl-flet ((ctx (code) (with-temp-buffer
914+
(php-mode)
915+
(insert "<?php\n" code)
916+
(php-dot-context))))
917+
;; Preceding a magic constant or a string literal: concatenation reads well.
918+
(should (eq 'next-to-string (ctx "__DIR__")))
919+
(should (eq 'next-to-string (ctx "__FILE__")))
920+
(should (eq 'next-to-string (ctx "'foo'")))
921+
(should (eq 'next-to-string (ctx "\"foo\"")))
922+
;; Plain code.
923+
(should (eq 'code (ctx "$a")))
924+
(should (eq 'code (ctx "foo()")))
925+
;; Inside a string or comment.
926+
(should (eq 'string-or-comment (ctx "'foo")))
927+
(should (eq 'string-or-comment (ctx "// foo")))))
928+
929+
(ert-deftest php-mode-test-complete-path ()
930+
"`php-complete-complete-path' completes the `__DIR__ . \\='/...\\='' idiom."
931+
(let* ((root (make-temp-file "php-complete-path" t))
932+
(file (expand-file-name "src/App.php" root)))
933+
(unwind-protect
934+
(progn
935+
(make-directory (expand-file-name "src/Controller" root) t)
936+
;; Completes filesystem entries rooted at the file's directory,
937+
;; even when `default-directory' points elsewhere.
938+
(with-temp-buffer
939+
(php-mode)
940+
(setq buffer-file-name file
941+
default-directory temporary-file-directory)
942+
(insert "<?php\n$x = __DIR__ . '/")
943+
(let ((res (php-complete-complete-path)))
944+
(should res)
945+
(should (member "Controller/"
946+
(all-completions "" (nth 2 res))))))
947+
;; The leading slash is fixed: BEG sits after "__DIR__ . '/".
948+
(with-temp-buffer
949+
(php-mode)
950+
(setq buffer-file-name file)
951+
(insert "<?php\n$x = __DIR__ . '/")
952+
(should (eq (car (php-complete--path-string-bounds)) (point))))
953+
;; A plain string is not the idiom.
954+
(with-temp-buffer
955+
(php-mode)
956+
(setq buffer-file-name file)
957+
(insert "<?php\n$x = 'plain/")
958+
(should-not (php-complete-complete-path))))
959+
(delete-directory root t))))
960+
910961
;; For developers: How to make .faces list file.
911962
;;
912963
;; 1. Press `M-x eval-buffer' in this file bufffer.

0 commit comments

Comments
 (0)