Make php-ide-mode track what it activated, and protect the alists behind its :safe checks - #827
Merged
Conversation
php-ide-mode decided what to deactivate by re-reading php-ide-features, so it could not keep the buffer and the mode line in agreement: - A feature that failed to activate left the mode on. `define-minor-mode' sets the mode variable before running the body, and the body signalled without undoing it, so the mode line claimed PHP-IDE was running while nothing had started -- and turning it back off then called :deactivate on an unavailable feature, signalling void-function. - Editing .dir-locals.el and re-applying it changes php-ide-features in a live buffer, after which deactivation walked the new value: it either signalled on an unknown feature or turned off the wrong one, leaving the feature that was really running behind. - Re-enabling the mode activated everything a second time, which happens whenever hack-local-variables-hook runs again, e.g. on revert-buffer. Record the features actually activated in a buffer-local variable and deactivate exactly those, skip ones already active, and roll back (mode off, features deactivated) when activation fails partway.
php-ide.el requires php-ide-phpactor only at compile time, yet the phpactor entry of php-ide-feature-alist names php-ide-phpactor-activate and -deactivate as its :activate/:deactivate. Those symbols resolved only because the package autoloads happened to define them; loading php-ide.el on its own, as a manual installation does, left the feature usable by :test but void-function on activation. Require it from :test, next to the phpactor package it already loads.
Restricting the :safe predicates of php-ide-features and php-ide-eglot-executable to "what this package already knows about" only holds while a project cannot extend what the package knows. Neither backing alist was risky, so a .dir-locals.el could add an entry to php-ide-lsp-command-alist and then name it in php-ide-eglot-executable, where our own predicate would wave it through as a bundled preset. php-ide-feature-alist is the same story for the functions php-ide-mode calls. Both are now risky, so Emacs always confirms them and never remembers them as safe. The flag is stated with an explicit autoloaded `put' as well as :risky, because the autoloads generator copies only :safe, and the check that matters runs before php-ide.el is loaded. php-ide-lsp-command-alist becomes a defcustom in passing: adding a preset is how a user makes a server selectable from .dir-locals.el without being asked to confirm an executable path.
php-ide-features is a set, so nothing stopped two LSP clients from being enabled together and each trying to manage the same buffer. Tag the clients with :exclusive and warn once when more than one of a tag is requested, while still activating them: an unusual combination stays possible, it just says so. Phpactor's bridge is not an LSP client and pairs with one legitimately, so it is untagged and stays quiet. Eglot's :deactivate also called eglot--managed-mode-off directly. It is internal, and it is still the only way to release one buffer without shutting down a server other buffers use, so php-ide-eglot-deactivate now wraps it: it falls back to eglot--managed-mode and finally to a warning that names eglot-shutdown, rather than failing with void-function if a future Eglot drops it. php-ide-eglot-managed-modes becomes a defcustom while nearby.
"All of these functions, modes and terms are subject to change without notice" told a user configuring php-ide-features nothing useful, and after this round it is no longer true of the options and commands the README documents. Point the caveat at the part that really is in flux: the keywords a php-ide-feature-alist entry may use, and the internals.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #826, which fixed the features that never worked. This one is about
php-ide-modekeeping an accurate account of what it has done, and about closing a gap left in #826's own hardening.Fixed
php-ide-modedecided what to deactivate by re-readingphp-ide-features, so it could not keep the buffer and the mode line in agreement:define-minor-modesets the mode variable before running the body, and the body signalled without undoing it, so the mode line claimed PHP-IDE was running while nothing had started — and turning it back off then called:deactivateon an unavailable feature, signallingvoid-function..dir-locals.eland re-applying it changesphp-ide-featuresin a live buffer, after which deactivation walked the new value: it either signalled on an unknown feature or turned off the wrong one, leaving the feature that was really running behind.hack-local-variables-hookruns again, e.g. onrevert-buffer.php-ide-modenow records what it activated, deactivates exactly that, skips what is already active, and rolls back when activation fails partway.The
phpactorfeature could not activate outside a package installation.php-ide.elrequiresphp-ide-phpactoronly at compile time, so its:activate/:deactivateresolved only because the package autoloads happened to define them; loadingphp-ide.elon its own left the feature usable by:testbutvoid-functionon activation.Security
#826 restricted the
:safepredicates ofphp-ide-featuresandphp-ide-eglot-executableto "what this package already knows about". That only holds while a project cannot extend what the package knows — and neither backing alist was risky. A.dir-locals.elcould add an entry tophp-ide-lsp-command-alistand then name it inphp-ide-eglot-executable, where our own predicate waved it through as a bundled preset: the project supplied both the command and its own approval.php-ide-feature-alistis the same story for the functionsphp-ide-modecalls.Both are now risky, so Emacs always confirms them and never remembers them as safe.
Note that
:risky talone was not enough: unlike:safe, the autoloads generator does not copy it, so it took effect only afterphp-ide.elwas loaded — the same shape of bug as the:safepredicates in #826. The flag is stated with an explicit autoloadedputas well, and a test runs both checks in a subprocess loading only the autoloads file.Changed
php-ide-modewarns whenphp-ide-featuresenables more than one LSP client. Activation still proceeds, so a deliberate combination remains possible. Phpactor's bridge is not an LSP client and pairs with one without a warning.php-ide-eglot-deactivatewraps Eglot's internal buffer-scoped switch instead of naming it in the feature table. Eglot exposes no public way to release one buffer without shutting down a server other buffers use, so the internal call stays — but it falls back toeglot--managed-modeand finally to a warning namingeglot-shutdown, rather than failing withvoid-functionif a future Eglot drops it.php-ide-lsp-command-alistandphp-ide-eglot-managed-modesare now customizable. Adding your own preset to the former is also how you let a project select that server from.dir-locals.elwithout confirming an executable path.php-ide-featuresnothing useful; the caveat now points at thephp-ide-feature-alistentry keywords and the internals, which really are in flux.Testing
eask test ert ./tests/php-mode-test.elpasses (91 tests, 3 skipped). Each fix above has a regression test, including subprocess checks that the:safepredicates and the risky flags both apply beforephp-ide.elis loaded.php-ide.elandphp-ide-phpactor.elare clean under byte-compile and checkdoc.Version headers still read 1.26.1 across the tree while 1.27.0 is released. That looks like a separate release-management commit rather than something to fold into a bugfix PR, so it is left alone here.