IconNode gets out of sync with an element's `classList`
savetheclocktower opened this issue · comments
I've got a symptom that's been happening on Pulsar (the Atom fork) but not on Atom itself: upon first showing of tree-view, the file icons are missing, and don't reappear until some sort of action that forces it to refresh the directory entries. Such actions include collapsing/expanding the root directory or activating the “Tree View: Toggle VCS Ignored Files” command.
I assumed this was a bug in Pulsar, but I think it's actually a bug in this package, and a different ordering of events during startup in Atom was concealing the bug all along.
Here's the simple version.
Tree-view's getIconServices
provides a method called updateFileIcon
that gets called once per tree-view file entry. It ends up calling through to IconNode
, then to IconNode#refresh
, which keeps track of which classes it thinks it's applied to the element, but doesn't actually double-check against the element's own classList
.
But if updateFileIcon
gets called redundantly, it will reset the element's classes; it assumes that the provider service will add the needed icons again. But on the second call, IconNode#refresh
doesn't realize that the classes it added earlier are not there.
But why would updateFileIcon
get called redundantly? Because tree-view consumes two services, and this package provides both. It happens like this:
- Tree-view sets up a
DefaultFileIcons
provider foratom.file-icons
very early on. - The provider and consumer of
file-icons.element-icons
connect with one another, and each entry gets a call toupdateFileIcon
. AnIconNode
instance is created for each file's tree-view entry. - The provider and consumer of
atom.file-icons
connect with one another.getIconServices
recognizes there's a new provider and firesonDidChange
. Tree-view reacts to that, even though it's a different service that just got connected, because there's oneonDidChange
for both services. That's when the redundant call toupdateFileIcon
happens.
I think that collapsing and expanding the tree-view root fixes this because the old IconNode
instances get trashed and new ones are created. And I think that's what ends up masking this bug in Atom: I think that all the above steps happen so early in the Atom initialization lifecycle that they actually precede the point where Atom sets the root directory for the project, so there aren't any tree entries for this bug to manifest on.
Ultimately, I still think it's a bug in this package, because IconNode#refresh
shouldn't assume that the classes it applied earlier will still be present. My suggested fix would be to replace the classesDiffer
check with one that checks the new classes against the element itself.
(Atom is no more, but Pulsar has its own package registry, and an updated file-icons
package could be published there. Or, at the very least, it could be landed on master
in this repo, and would be pushed out to anyone who has installed directly from GitHub ppm install file-icons/atom
.)
For other Pulsar users out there, here's how I worked around this in my init.js
:
// The icons from the `file-icons` package won't show up initially in Pulsar,
// but it's easy enough to fix: collapse and expand the root folder(s).
{
let disposable = atom.packages.onDidActivatePackage(({ name, mainModule }) => {
if (name !== 'tree-view') { return; }
treeView = mainModule.treeView;
treeView.roots.forEach((root) => {
root.collapse();
root.expand();
});
disposable.dispose();
});
}
If tree-view
is activated after the project's roots are known, then this snippet of code will collapse and expand each root folders to force an icon refresh. If it's activated before the project's roots are known, then the bug won't happen in the first place.
Sadly, the dreaded FOUC happens (a square is shown before the icon is done loading) but it's better than nothing.
Thanks for your thorough report. I've just installed Pulsar, but from a cursory glance of an unmodified workspace, the file-icons
package seems to work fine. Could I get a list of packages that you've installed (and enabled)? Also, what OS are you running? (macOS's filesystem APIs might be more responsive than those on other systems).
My suggested fix would be to replace the
classesDiffer
check with one that checks the new classes against the element itself.
It's been a long time since I've touched this codebase, but from memory, the classesDiffer
method is written specifically to compare the output of Icon#getClass
. Now, most of the classes situated under ./lib/icons/
are specific to the icon-compiler, which is an external process that regenerates lib/icons/.icondb.js
from the contents of config.cson
. The compiled database is then used to instantiate Icon
instances at startup (or package activation), during which time their classes are compared with those cached from the last workspace session, indicating whether or not the icon-tables are outdated. If so, they're discarded, and a FOUC may appear before everything else is fully-loaded.
So in other words, it's not a simple matter of interrogating the DOM…
I'm on macOS 11.7.3.
My installed packages
Built-in Atom Packages (92)
├── atom-dark-syntax@0.29.1
├── atom-dark-ui@0.53.3
├── atom-light-syntax@0.29.1
├── atom-light-ui@0.46.3
├── base16-tomorrow-dark-theme@1.6.0
├── base16-tomorrow-light-theme@1.6.0
├── one-dark-ui@1.12.5
├── one-light-ui@1.12.5
├── one-dark-syntax@1.8.4
├── one-light-syntax@1.8.4
├── solarized-dark-syntax@1.3.0
├── solarized-light-syntax@1.3.0
├── about@1.9.1
├── archive-view@0.66.0
├── autocomplete-atom-api@0.10.7
├── autocomplete-css@0.17.5 (disabled)
├── autocomplete-html@0.8.9 (disabled)
├── autocomplete-plus@2.42.6 (disabled)
├── autocomplete-snippets@1.12.1
├── autoflow@0.29.4
├── autosave@0.24.6
├── background-tips@0.28.1
├── bookmarks@0.46.0
├── bracket-matcher@0.92.0
├── command-palette@0.43.5
├── dalek@0.2.2
├── deprecation-cop@0.56.9
├── dev-live-reload@0.48.1
├── encoding-selector@0.23.9
├── exception-reporting@0.43.1
├── find-and-replace@0.220.1
├── fuzzy-finder@1.14.3
├── github@0.36.14 (disabled)
├── git-diff@1.3.9
├── go-to-line@0.33.0
├── grammar-selector@0.50.1
├── image-view@0.64.0
├── incompatible-packages@0.27.3
├── keybinding-resolver@0.39.1
├── line-ending-selector@0.7.7
├── link@0.31.6
├── markdown-preview@0.160.2
├── notifications@0.72.1
├── open-on-github@1.3.2
├── package-generator@1.3.0
├── settings-view@0.261.11
├── snippets@1.6.1
├── spell-check@0.77.1 (disabled)
├── status-bar@1.8.17
├── styleguide@0.49.12
├── symbols-view@0.118.4
├── tabs@0.110.2
├── timecop@0.36.2
├── tree-view@0.229.1
├── update-package-dependencies@0.13.1
├── welcome@0.36.9
├── whitespace@0.37.8
├── wrap-guide@0.41.0
├── language-c@0.60.20
├── language-clojure@0.22.8
├── language-coffee-script@0.50.0
├── language-csharp@1.1.0
├── language-css@0.45.4
├── language-gfm@0.90.8
├── language-git@0.19.1
├── language-go@0.47.3
├── language-html@0.53.1
├── language-hyperlink@0.17.1
├── language-java@0.32.1
├── language-javascript@0.134.2
├── language-json@1.0.5
├── language-less@0.34.3
├── language-make@0.23.0
├── language-mustache@0.14.5
├── language-objective-c@0.16.0
├── language-perl@0.38.1
├── language-php@0.48.1
├── language-property-list@0.9.1
├── language-python@0.53.6
├── language-ruby@0.73.0
├── language-ruby-on-rails@0.25.3
├── language-rust-bundled@0.1.1
├── language-sass@0.62.2
├── language-shellscript@0.28.2
├── language-source@0.9.0
├── language-sql@0.25.10
├── language-text@0.7.4
├── language-todo@0.29.4
├── language-toml@0.20.0
├── language-typescript@0.6.4
├── language-xml@0.35.3
└── language-yaml@0.32.0
Dev Packages (10) /Users/andrew/.pulsar/dev/packages
├── calculator-light-ui@0.3.25
├── language-babel@2.85.0
├── language-html@0.53.1
├── linter@3.4.0
├── linter-eslint-node@1.0.6
├── linter-ui-default@3.4.1
├── settings-view@0.261.11
├── snippets@1.7.0
├── tabs@0.110.2
└── vibrant-ink-redux-syntax@0.3.14
Community Packages (39) /Users/andrew/.pulsar/packages
├── aligner@1.3.0
├── aligner-javascript@1.3.0
├── atom-beautify@0.33.4
├── atom-ternjs@0.20.0
├── autoclose-html@0.23.0
├── autocomplete-jsdoc@0.0.2
├── busy-signal@2.0.1
├── dbclick-tree-view@1.6.3
├── docblockr@0.14.2
├── editorconfig@2.6.1
├── emmet@2.4.3
├── file-icons@2.1.47
├── format-javascript-comment@0.2.5
├── hyperclick@0.1.5 (disabled)
├── hyperlink-helper@0.0.5
├── inline-autocomplete-textmate@0.2.52
├── intentions@2.1.1
├── keybinding-cheatsheet@0.1.1
├── language-arduino@0.4.3
├── language-babel@2.85.0
├── language-ejs-custom@0.0.0
├── language-ini@1.25.0
├── language-latex@1.2.0
├── language-nginx@0.8.0
├── language-pegjs@0.5.0
├── language-svelte-custom@0.0.0
├── language-tree-sitter-scm@0.0.0
├── linter@3.4.0
├── linter-eslint@9.0.1
├── linter-scss-lint@3.2.1
├── linter-ui-default@3.4.1
├── package-settings@1.1.0
├── pigments@0.40.6
├── prettier-atom@0.60.1
├── project-config@1.0.2 (disabled)
├── remote-atom@1.3.12
├── sync-settings@5.2.16
├── toggle-quotes@1.1.4
└── zentabs@0.8.9
I admit I haven't tried to disable everything except for file-icons
. I can try to do that when I get a chance.
So in other words, it's not a simple matter of interrogating the DOM…
I haven't followed that explanation 100%, but if classesDiffer
needs to be present for other reasons, that's fine. But there should be a sanity check in there to account for the situation where the class names present in the DOM aren't what IconNode
thinks they are.
But there should be a sanity check in there to account for the situation where the class names present in the DOM aren't what
IconNode
thinks they are.
That's easier said than done: the base classes differ between the core packages which consume the file-icons.element-icons
service:
Package | Base CSS classes |
---|---|
archive-view | icon , file |
find-and-replace | icon |
fuzzy-finder | icon , file (and possibly also primary-line ) |
tabs | icon , title |
tree-view | icon , name |
And then there are the various community packages (e.g., advanced-open-file) that consume File-Icons's API as well, whose choice of class-names (if any) is impossible to predict.
Honestly, at this point, I'm more in favour of a clean rewrite, specifically one that's environment-agnostic and compatible with anything resembling a browser environment. This has been on my to-do list for years, albeit at a low priority. That might change now that Pulsar is breathing renewed life into the Atom project, and there's more incentive to replace this god-awful spaghetti code from late 2016…
I'm sure I'm missing something simple, but I don't understand what you mean.
Here's the code in question:
refresh(){
if(!this.visible){
this.removeClasses();
this.classes = null;
}
else if(this.resource.icon){
const classes = this.resource.icon.getClasses();
if(this.classesDiffer(classes, this.classes)){
this.removeClasses();
this.classes = classes;
this.addClasses();
}
}
}
IconNode
knows what classes it wants to apply to the element. If it thinks they aren't present yet, it will call addClasses
, which calls this.element.classList.add
. If it can do that, then it can do a sanity check like so:
refresh(){
if(!this.visible){
this.removeClasses();
this.classes = null;
}
else if(this.resource.icon){
const classes = this.resource.icon.getClasses();
const classesPresent = classes.every(c => this.element.classList.contains(c));
if(this.classesDiffer(classes, this.classes) || !classesPresent){
this.removeClasses();
this.classes = classes;
this.addClasses();
}
}
}
I don't see how this runs into any of the complexity you're describing.
@savetheclocktower Out of curiosity, have you been running Atom (or Atom Beta) concurrently or between Pulsar sessions? I've noticed some strange, partial retention of workspace state being shared by both editors, which must've invalidated the icon-cache, forcing a reload at startup (and possibly exacerbating the FOUC you've witnessed).
I've yet to get Pulsar running locally with my Atom setup, which is key to not pulling my hair out while investigating this (and having Atom and Pulsar open concurrently causes… issues…)
I don't see how this runs into any of the complexity you're describing.
Never mind, I think I was over-explaining things (probably more for myself than anything, since I was relearning what different parts of this codebase even did…). Sorry for any confusion. 😅
No worries!
I've switched to Pulsar full-time, but noticing this issue prompted me to launch Atom at the same time and compare the different behaviors. I had them both open while I was troubleshooting this bug, but haven't really touched Atom since.
I think I copied my .atom
folder over to .pulsar
, or maybe just copied over my config files and then reinstalled my packages.