Visitar URL original
Forget linters of disabled or removed plugins when packages change by dpc00 · Pull Request #1999 · SublimeLinter/SublimeLinter · GitHub
Skip to content

Forget linters of disabled or removed plugins when packages change - #1999

Closed
dpc00 wants to merge 3 commits into
SublimeLinter:masterfrom
dpc00:forget-unloaded-linters
Closed

dpc00 wants to merge 3 commits into
SublimeLinter:masterfrom
dpc00:forget-unloaded-linters

Conversation

@dpc00

@dpc00 dpc00 commented Sep 29, 2026 •

Copy link
Copy Markdown

Fixes #1998.

Reworked twice following the discussion here (thanks @kaste). Latest version, after the review of the observer:

What happens today: when a linter package is disabled or removed, Sublime unloads its plugin, but nothing tells SublimeLinter, so the class stays in persist.linter_classes and keeps linting until Sublime is restarted.

What this does:

  • plugin_loaded remembers the current ignored_packages (module level ignored_packages) and observes Preferences.sublime-settings; plugin_unloaded clears the observer.
  • On each change on_preferences_changed computes the delta: the packages that are in ignored_packages now but were not before. Changes that do not add a package (any other preference, enabling a package, a package that was already ignored) do nothing. No timer and no waiting for Sublime to unload anything.
  • persist.forget_linters_of_packages(packages) unregisters the linters whose klass.plugin_name is one of those packages and returns their names.
  • If any linter was forgotten, sublime.run_command('sublime_linter_config_changed') runs once, so open views re-lint, lose the linter via the existing assign_linters_to_buffer, and their errors are cleared.

Package Control disables a package (disable_packages({REMOVE: ...}), in package_tasks.remove_packages) before it removes it, and does the same before upgrades, so removing and upgrading are covered by the same event. A package that comes back (re-enabled, upgraded) registers its linter again when Sublime loads the plugin.

Not covered: a package that is deleted from disk by hand, without Package Control and without touching ignored_packages, gives no event; that still needs a restart. As discussed, that would need another mechanism (e.g. only electing linters that are actually available).

Verified live (Sublime Text 4215, this branch loaded as SublimeLinter): installed SublimeLinter-pycodestyle (persist.linter_classes == ['pycodestyle']), added the package to ignored_packages: the linter was gone from persist.linter_classes a moment later without restarting; removing it from ignored_packages again brought pycodestyle back. I did not run Package Control's interactive remove command end to end for this version (the earlier version was verified with remove_package).

Tests: tests/test_forget_unloaded_linters.py (7 tests, no windows needed): forget_linters_of_packages forgets only the given packages' linters and nothing for unknown packages; on_preferences_changed forgets the linter and re-lints once when a package is newly disabled, and does nothing for an unrelated change, for a newly disabled package without a linter, for a package that was already disabled, or when a package is enabled. They pass in Sublime Text 4215 (run in the plugin host); flake8 with the project's config is clean. I did not re-run the full suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WtRdpWmSGFACg8HR3Vpk3d

When a linter package is removed, Sublime unloads its plugin module and
pops it from sys.modules, but the linter class stays in
persist.linter_classes and keeps linting every matching view until Sublime
is restarted.

Before electing linters for a view, drop the ones whose module is no
longer in sys.modules. assign_linters_to_buffer already clears the errors
of linters that are no longer assigned to a view.

Fixes #1998

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the check in elect() with an event, as suggested in the PR
discussion. Observe 'ignored_packages' and Package Control's settings and,
shortly after they change, unregister the linters whose plugin is disabled
or whose module Sublime has unloaded (removed package), then call
sublime_linter_config_changed so open views drop the linter and its
errors right away.

Fixes #1998

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@dpc00 dpc00 changed the title Forget linters whose plugin has been unloaded Forget linters of disabled or removed plugins when packages change Sep 29, 2026
Comment thread lint/persist.py Outdated
Comment on lines +73 to +74
klass.__module__ not in sys.modules
or klass.plugin_name in disabled_packages

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling a package should unload it, so the first check is likely enough to detect both cases. However, we depend on the fact that Sublime actually has unloaded them already. And that introduces some number guessing: Does that take 1s or rather 5s.

Comment thread sublime_linter.py Outdated
.get('in_process_packages', [])
)

if names := persist.forget_unloaded_linters(ignored - in_process):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We run here always, when anything has changed in the main Preferences. That is unnecessary often. We need a lens into the actual key/values we're interested in.

Now I think: If we capture the active state on load, and we compute just the delta of active packages or removed packages, then we actually know which packages we need to de-register.

We can't grab packages that are added and removed the hacker way, by cloning and deleting directories, that way.

These we could grab when we just pre-select in elect, only the linters that are actually available. This was in fact the first attempt. The only downside was that we didn't know how to call sublime_linter_config_changed to make changes visible for all. ...

That needs some thinking still.

Address review: instead of reacting to every change of the preferences and
guessing when Sublime has unloaded a plugin, remember the disabled packages
when SublimeLinter loads and compute the delta on each change. The linters of
the newly disabled packages are unregistered, everything else is left alone.

Package Control disables a package before it removes or upgrades it, so this
covers those as well. Packages that are removed by deleting their directory
are not covered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtRdpWmSGFACg8HR3Vpk3d
@dpc00

dpc00 commented Sep 29, 2026

Copy link
Copy Markdown
Author

Thanks, that is a better design. I reworked it along your suggestion (pushed as a new commit):

  • The active state (ignored_packages) is captured in plugin_loaded, and on each preferences change only the delta is looked at: the packages that were newly disabled. Anything else that changes in the preferences does nothing, and the Package Control settings observer and the 1s timer are gone, so there is no guessing how long Sublime needs to unload a plugin.
  • persist.forget_linters_of_packages(packages) unregisters exactly the linters of those packages, and sublime_linter_config_changed runs once if something was forgotten.
  • Package Control disables a package before it removes or upgrades it, so those are covered by the same event.

Not covered, as you said: packages that are deleted by hand. I have not tried the "only elect linters that are actually available" idea from the first attempt, since you said that still needs thinking (how to make sublime_linter_config_changed reach everything); I can look into it if you want that as well.

Checked live: with SublimeLinter-pycodestyle installed, adding it to ignored_packages dropped the linter from persist.linter_classes without a restart, and removing it from the list brought it back. Tests are rewritten for the new behaviour (7, they pass); details are in the description, which I updated.

@kaste

kaste commented Sep 30, 2026

Copy link
Copy Markdown
Member

The last commit is worse than the one before. Donald, that is not mechanical work and you have to think actually and steer the agent.

@dpc00

dpc00 commented Sep 30, 2026 via email

Copy link
Copy Markdown
Author

@dpc00

dpc00 commented Sep 30, 2026 via email

Copy link
Copy Markdown
Author

@dpc00

dpc00 commented Oct 1, 2026 via email

Copy link
Copy Markdown
Author

@dpc00

dpc00 commented Oct 2, 2026

Copy link
Copy Markdown
Author

(Written by Claude, the AI assistant doing this work for dpc00; the earlier replies in this thread from this account were written by dpc00 himself.)

Thank you for the careful review, and sorry that the last commit made it worse. I do not want to push a third guess, so here is where this stands and one question.

  • First commit (6362097): forget_unloaded_linters() in elect.py drops a linter class when klass.__module__ is no longer in sys.modules, and it runs at the start of assignable_linters_for_view(). No settings observer, no timer, no events. A view loses the linter, and its errors, the next time it is linted. What it does not do is refresh the other open views at once (the sublime_linter_config_changed problem you mentioned).
  • Last commit (ca36830): watches ignored_packages and forgets the linters of newly disabled packages, then runs sublime_linter_config_changed. Packages deleted by hand are not covered.

Which would you rather have: the first commit's approach (and accept that other views refresh on their next lint), or something else you have in mind? If you prefer the first one I can reset the branch to it and keep its test. If this needs more design thinking than I can offer, feel free to close it, or to take any part of it: #1998 stays the place for the design.

@dpc00 dpc00 closed this by deleting the head repository Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Uninstalled linter packages stay registered in persist.linter_classes and keep linting until restart

2 participants