Huntk23/ResharperGlyphfriend

Create own IconId

citizenmatt opened this issue ยท 10 comments

Hey. It might be an idea to create your own IconId based class that represents an icon as the CSS class name + resource location. This will be more lightweight than loading all of the resources as images. When your declared element icon provider is called, it uses this new IconId based class, and it'll be at this point that the image is loaded, and cached by ReSharper. So, it will only load the icons you're using in the project.

You load the image itself by implementing a class that implements IIconIdOwner, which gets called with a given IconId. You downcast this to your new icon ID class, retrieve the CSS class name + resource location and load the icon. ReSharper will cache the icons in ThemedIconManagerPerThemeCache, and will re-retrieve the icons only when the Visual Studio theme changes.

Hi Matt,

I had a feeling you would take the time to toss more advice in my direction. I plan on figuring this out very soon hopefully. Work had swamped me and delayed me to continue this project at the moment.

Once again, thanks for your advice and taking the time to make the update to 2016.1. If I run into any issues and have a question, I will most likely reach out to you.

I've done some research on IconId and IIconIdOwner, and read about some codes in ReMoji project and also this guide, but still not very clear how to modify the icon of IconId from R# itself (aka. the css class lookup item).Could you please tell more about it?

The current code in GlyphIconProvider.cs will allow you to override ReSharper's default icon with an IconId-derived class instance that you specify, by looking at the CSS class name to see if it's one we know about.

The plugin has a class - GlyphfriendStorage.cs - that creates a mapping between the CSS class name and an ImageSourceIconId that holds a reference to the bitmap to use for the image. The provider looks up the CSS class name, and if there's a corresponding ImageSourceIconId for it, returns that class instance - a custom icon. Otherwise, it returns null, and the default ReSharper icon is used.

As an alternative, the storage class could create a map between the name of the CSS and the location of the image, rather than loading it. When the provider class is called, if the CSS class name is known, you can create a new instance of a new IconId based class, let's call it MyResourceImageIconId. This class will have a property that is the string of the image location. When ReSharper wants to get the actual bitmap image, it will ask the appropriate IIconIdOwner for it. This class (let's call it MyResourceImageIconIdOwner) will take in an IconId and downcast it to MyResourceImageIconId, access the Location property and use that to actually load the image. Finally, the image is returned to ReSharper, which caches it until the theme changes.

This way, the images are only loaded for CSS classes that ReSharper knows about.

Does that make sense?

Clear...thanks for your information.

But it was a little complex. In most cases, we using those web fonts by import the whole font instead of using those css classes individually. So I'm thinking of how about load the whole imageset of the font on first call instead of load them all on VS startup. What do you think is the better way compared with create own IconId class?

I'm also think of how to make it extensible, that means if fonts updated (for ex Font-Awesome released a new version), we do not need to modify extension or recompile extension to adapt them, just updating some configuration dynamically.

I think you should lean on ReSharper's icon support by implementing your own IconId and IIconIdOwner. It's actually pretty straight forward - the IconId class simply needs a couple of properties to identify the image to load, and the IIconIdOwner class would contain the image loading logic that's currently in GlyphStorage.cs. It means that you'd only create the icons that are needed - i.e. you could support ionicons, but if the project only uses Font Awesome, you never load the ionicons images, which is a good thing for memory usage. It would mean that images are loaded when requested, rather than at VS startup, or on first call.

If you wanted to make it extensible, then things get more interesting :)

I actually started writing a similar extension to this, but didn't get very far, and then found this project, so gave up :) My version would work with the font file directly - e.g. FontAwesome.otf. I created a FontIconId that simply stored the typeface file and the glyph as strings - "FontAwesome.otf" and "\f005". The FontIconIdOwner would take these properties, load the typeface from the project (and cache it) and draw the glyph into a WPF DrawingImage. This would allow things to be extensible (just use a new font file) and also work with themes (just redraw with a new brush when ReSharper asks me to).

The bit I didn't actually write was the fun bit - getting the mapping between CSS class name and the typeface/glyph. I wasn't sure if I'd just have a static map, or if I'd parse the CSS file directly. The static map is easiest, but doesn't allow for updates. Parsing the CSS is cooler, but trickier. I suspect I'd have to have a list of known icon sets, and the name of the CSS file, e.g. recognise that I need to parse font-awesome.css. As long as the file is added to the project, I could get ReSharper's syntax tree, and walk it, looking for the @font-face to get the typeface name, and then find rules matching the following pattern to get the glyph (i.e. the rule includes a .before and content):

.fa-star.before: {
  content: "\f005";
}

There are a few more details to the parsing (and caching) of the required class names, but I think it would be possible, as long as the .css and .otf files were added to the project.

I'll try and dig out the FontIconId and FontIconIdOwner classes.

I know what's your mean, and some interesting point...
I think using IconId do not have any benefit in this case. Think about this situation:

  • I using Font-Awesome and have already added css file to my project
  • I am writing a html file now, and plan to add css classes to a i code
  • I typed fa- in editor and expect intellisense to popup

In this situation, R# should list all css classes as well as preview in the popup. That means almost ALL icons from Font-Awesome should be loaded. In other words, there's no differect load full Font-Awesome set on first call. And in this situation, I can only load Font-Awesome into GlyphStorage and leave others not loaded.
If I missed some points, such as R# can not cache the icons that provided by current method, then forgot what I've said ๐Ÿ˜‚

I think load ttf font file and parsing css file would be nice but there are some problems:

  • If css file and fonts was referenced by remote url or not included in current project, how to recognise them
  • If css file using different css rules to using the font, is there a way to recognise them all without problem
  • will parsing css files degrade IDE's performance

I think for well known web fonts, using static map will be better. But for private web fonts, parsing css file will be interesting ๐Ÿ‘ .

You're right that the code completion will likely load all of the icons, so in some respects, there's not much difference. But the system is actually designed to load icons on-demand, without a backing store. It's better to lean on ReSharper's own icon cache than re-implement one of your own, especially since it also handles theming, which GlyphStorage doesn't. It also means that your image loading would be very simple, without even having to worry about special casing loading an entire image set at a time.

Remote CSS files and fonts would definitely be a problem ๐Ÿ˜„

If the CSS had a different pattern to the .before and content:, then the parsing code would have to also understand that pattern. Adding this parsing wouldn't degrade perf, as it would be implemented as a persistent cache (like this one supporting angularjs). This means the files get parsed once, the first time the solution is opened, the results automatically cached to disk, and are kept up to date when the files change. The icon provider would then simply ask the cache if an element was an icon class or not, and build an IconId from the result. But yes, a static map is simpler ๐Ÿ˜„

Hi @citizenmatt,

I want to use the ReSharper SDK ThemedIconPng build action to make the icons theme aware. Do you know any examples to properly use the generated .cs and .xaml file?

If I use the generated files, do I even need IIconIdOwner and IconId inheritance anymore? With some testing I can just return the generated IconId in the DeclaredElementIconProvider. Only problem is, the generated files change the naming convention of the icon png's. Exmaple: fa-adjust to FaAdjust

So I was thinking about just loading a text or json file of all the icon names with modified casing and removed dashes. Then using reflection to find it's correct class name and IconId generated in the .cs.

Thoughts?

I actually don't know of any examples of theme aware png files. I try and use XAML files when I can, but they're a complete pain to set up (they need to be 16x16, or they display too large, which is kind of annoying for a vector format! And resizing existing SVG icons is not as easy as it should be). PNG should be fairly easy, just call the file Whatever[DarkGray].png, Whatever[Gray].png or Whatever[Color].png. There are some details here.

If you do this, then you won't need an IIconIdOwner or IconId, and you'll just have to set up the icon. Reflection might not be a great idea as DeclaredElementIconProvider will get called frequently, so you should avoid heavy work.

If you were going to stick with IIconIdOwner, you'd get the IconTheme, which would allow you to find out what the current theme is.

I was actually going to use the reflection piece in the SolutionComponent GlyphfriendStorage to just load the <string name, IconId> and continue using the DeclaredElementProvider the same way. This way just the IconId is stored in the dictionary instead of the ImageSource.

DeclaredElementProvider was aware of the theme because of the generated class when I tested.