file-icons / atom

Atom file-specific icons for improved visual grepping.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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:

  1. Tree-view sets up a DefaultFileIcons provider for atom.file-icons very early on.
  2. The provider and consumer of file-icons.element-iconsconnect with one another, and each entry gets a call to updateFileIcon. An IconNode instance is created for each file's tree-view entry.
  3. The provider and consumer of atom.file-icons connect with one another. getIconServices recognizes there's a new provider and fires onDidChange. Tree-view reacts to that, even though it's a different service that just got connected, because there's one onDidChange for both services. That's when the redundant call to updateFileIcon 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.