Editing advancements for Xamarin in Visual Studio for Mac
We released this week the first 8.2 preview of Visual Studio for Mac. This release is important for us as it’s the first one to contain our improved editor experience for Android XML resources and Xamarin Forms XAML files both enabled by the port of the Visual Studio editor to the Mac.
You can read through the announcement above for the high-level improvements. In this post I want to focus on how we got there with specifically the Android side (I will speak about XAML in a future entry).
Now if you have read some of my previous posts, we started making heavy investment improving our editing experience for Android XML resources (originally only layout files) last year which first got delivered in the Visual Studio 2017 15.8 release and kept being improved in Visual Studio 2019.
At the time the landscape was quite different, Visual Studio for Mac and Visual Studio had only a few shared APIs (Roslyn mainly) and developing certain features (like editor extensions) required rewriting portion of the same code twice. Worse, on Windows the base XML editor that we were using as our editing canvas did not have many extensibility points.
One of the horror story of that time was around IntelliSense. Because the Visual Studio XML editor does not have an API to extend the completion list directly, we had to resort to generating on-the-fly XSD (XML Schema Definition) files that the XML language service would consume to provide the autocompletion experience.
This was alright at best with very basic elements and attributes completions but providing a comprehensive list that included all your resource references implied generating massive XSD files with a terrible performance cost.
On Visual Studio for Mac, we had the benefit of an existing and extensible XML pipeline complete with an incremental XML parser and a straightforward interface to inject our own completions depending on the caret location (elements, attributes or attributes values). This is the reason why our experience there was so much better and faster at the time.
When maintaining the previous XSD logic on Windows became untenable we went looking for other solutions. To implement an autocompletion experience at the time you had 3 possibilities:
- Use a bespoke system like the XSD files we were using (which in turn is based on a legacy Visual Studio API for language services)
- Use Roslyn editor features layer. Unfortunately this implied having an XML parser that could output Roslyn trees which we didn’t have.
- Use the low-level, MEF-based, synchronous completion API from the Visual Studio editor. This is what Roslyn would use under the hood but had extensively improved to be more asynchronous.
Fortunately for us, we asked ourselves that question right at the time when the Visual Studio editor team were developing a new more modern IntelliSense API that was async
-friendly. It represented the best of both the older API (no semantic dependencies) and Roslyn (asynchronous capabilities).
We made a proof-of-concept by porting part of the Visual Studio for Mac XML service to be layered on top of this new API with great help from the editor team. The result was so much better that we decided this was the way to go. As one of the earliest adopter of this new editor feature we also (hopefully) influenced it positively (Roslyn now uses it too).
Thanks to this newfound knowledge of editor extensibility, we continued implementing more features: syntax highlighting (classification in Visual Studio jargon), semantic operations, go-to-definition and others. It came to the point that we provided enough functionality to simply drop the original XML editor altogether.
We also took advantage of our other work to employ a better XML parsing pipeline in the designer itself to make the interactions between the design surface and the text editor more seamless.
Together, all those improvements switched things around and Visual Studio was now leading in terms of editor experience. We were keeping watch on Visual Studio for Mac though and we knew that, at the very least, work was on-going to provide those same editor extensibility API there to enable other teams at Microsoft to port their existing Visual Studio code over to the Mac.
As it turns out, the team decided to bite the bullet even further and ported the entire Visual Studio editor to the Mac (with its whole extensibility API coming for the ride). This was a boon for us and, from that point on, we started making sure all our Android extension code was portable across both IDEs.
Excitingly, we didn’t have to do much work to make this happen. Most of the available Visual Studio editor extensibility is actually cross-platform (meaning do not depend on a Visual Studio-specific API or a UI toolkit like WPF) and thus our extensions were easy to simply share in our common codebase.
(There is actually enough of that type of code aside from the WPF frontend that it was possible to create a unit-testing library with all of the open-source code available that was also shareable across both Windows and Mac)
The extensions that couldn’t be ported straightaway usually fell into two categories: they either used Visual Studio specific APIs or they referenced WPF. Here are a few example of how we solved those issues:
- For code depending on Visual Studio-specific API we looked to see why. In our case it turns out a lot of it boiled down to initializing various part of the code on-demand. Instead we moved that initialization into a single
ITextViewCreationListener
part that was IDE-specific and thus were able to remove the equivalent calls in our other extensions to make them agnostic. - Another solution was to isolate the offending functionality behind another interface. We had this case with our go-to-definition service where opening a document in the IDE required platform-specific calls. What we did is hide that code behind an extremely simple interface that we could
[Import]
from the now shareable service and only[Export]
the much smaller implementation in each IDE layers. - One last case was where we used
JoinableTaskFactory
from the vs-threading library by querying it directly from Visual Studio. Rather than doing that, we simply[Import]
’ed in our parts theJoinableTaskContext
that is already exported by both IDEs and thus severed that dependency. - For some extensions you may end up providing some UI adornments. In this case, you can leverage the IViewElementFactoryService API to reference a data object from the shared code and only provide the UI-specific portion in your IDE layer.
- Finally, as part of porting the editor to Cocoa, a number of public interfaces had to be de-WPF-ized and new types introduced to replace them. That work is expected to be shared back to Visual Studio but in the meantime you may have to do some creative subclassing to share functionality. A good example of this are adornment tags (think of the inline color previews in our editor).
As a testament of the hard work from the Visual Studio for Mac team to integrate the editor, we expect that by the final 8.2 release we will have ported 100% of our originally Windows-only extensions to be shared on both IDEs.
If you are yourself interested in doing that kind of work, the team has started publishing documentation. Keep an eye out for new content showing up there in the near future.