Skip to content

Commit 7462e12

Browse files
author
N. Taylor Mullen
committed
Add feature switch to use our new Razor LSP editor.
- Added a new Razor LSP content type to identify our new Razor editor experience. - Given that there are already content type associations for the .cshtml/.razor file paths I had to build a RazorEditorFactory that takes precedence over WTE's editor factories and if our feature flags are `true` to change the content type on created editors. - Added a `README.md` to the LanguageServerClient project to indicate how to use the new Razor LSP editor (no functionality currently). - For a feature flag I took a two pronged approach of allowing environment variables or VS' first class feature flag support. We haven't yet made the changes to Visual Studio necessary to surface our feature flag as a properly settable feature flag. Once that is done our feature flag will be toggable via the `Enable Preview Features` dialog. - Didn't feel there was a ton of value in adding tests for our EditorFactory since it acts on things like environment variables and internal VS feature flag functionalities. dotnet/aspnetcore#17787
1 parent 2b76a65 commit 7462e12

File tree

9 files changed

+347
-1
lines changed

9 files changed

+347
-1
lines changed

eng/Versions.props

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
<MicrosoftCodeAnalysisCommonPackageVersion>3.3.0</MicrosoftCodeAnalysisCommonPackageVersion>
9191
<MicrosoftCodeAnalysisCSharpPackageVersion>3.3.0</MicrosoftCodeAnalysisCSharpPackageVersion>
9292
<MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>3.3.0</MicrosoftCodeAnalysisWorkspacesCommonPackageVersion>
93+
<MicrosoftInternalVisualStudioShellInterop140DesignTimeVersion>14.3.25407-alpha</MicrosoftInternalVisualStudioShellInterop140DesignTimeVersion>
9394
<MicrosoftNETCoreApp50PackageVersion>$(MicrosoftNETCoreAppRuntimewinx64PackageVersion)</MicrosoftNETCoreApp50PackageVersion>
9495
<MicrosoftNETFrameworkReferenceAssemblies>1.0.0-alpha-5</MicrosoftNETFrameworkReferenceAssemblies>
9596
<MicrosoftNetRoslynDiagnosticsPackageVersion>2.6.3</MicrosoftNetRoslynDiagnosticsPackageVersion>

src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/Microsoft.VisualStudio.LanguageServerClient.Razor.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="$(MicrosoftVisualStudioShell150PackageVersion)" />
2121
<PackageReference Include="Newtonsoft.Json" Version="$(VS_NewtonsoftJsonPackageVersion)" />
2222
<PackageReference Include="Microsoft.VisualStudio.LanguageServer.Client" Version="$(MicrosoftVisualStudioLanguageServerClientPackageVersion)" />
23+
<PackageReference Include="Microsoft.Internal.VisualStudio.Shell.Interop.14.0.DesignTime" Version="$(MicrosoftInternalVisualStudioShellInterop140DesignTimeVersion)" />
2324
</ItemGroup>
2425

2526
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Using the Razor LSP Editor
2+
3+
To use the Razor LSP editor set an environment variable under the name `Razor.LSP.Editor` to `true` and then open Razor.sln. Running the `Microsoft.VisualStudio.RazorExtension` project will then result in `.razor` and `.cshtml` files being opened with our LSP editor.
4+
5+
To set the environment variable in powershell you can use the following syntax: `${env:Razor.LSP.Editor}="true"`
6+
7+
# FAQ
8+
9+
**Opening a project results in my Razor file saying "waiting for IntelliSense to initialize", why does it never stop?**
10+
This is a combo issue dealing with how Visual Studio serializes project state after a feature flag / environment variable has been set. Basically, prior to setting `Razor.LSP.Editor` Visual Studio will have serialized project state that says a Razor file was opened with the WTE editor. Therefore, when you first open a project that Razor file will attempt to be opened under the WTE editor but the core editor will conflict saying it should be opened by our editor. This results in the endless behavior of "waiting for IntelliSense to initialize".
11+
12+
Close and re-open the file and it shouldn't re-occur if you re-save the solution.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Microsoft.Internal.VisualStudio.Shell.Interop;
4+
using Microsoft.VisualStudio.ComponentModelHost;
5+
using Microsoft.VisualStudio.Editor;
6+
using Microsoft.VisualStudio.OLE.Interop;
7+
using Microsoft.VisualStudio.Package;
8+
using Microsoft.VisualStudio.Shell;
9+
using Microsoft.VisualStudio.Shell.Interop;
10+
using Microsoft.VisualStudio.TextManager.Interop;
11+
using Microsoft.VisualStudio.Utilities;
12+
13+
namespace Microsoft.VisualStudio.LanguageServerClient.Razor
14+
{
15+
[Guid(EditorFactoryGuidString)]
16+
public class RazorEditorFactory : EditorFactory
17+
{
18+
private const string EditorFactoryGuidString = "3dfdce9e-1799-4372-8aa6-d8e65182fdfc";
19+
private const string RazorLSPEditorFeatureFlag = "Razor.LSP.Editor";
20+
private readonly Lazy<IVsEditorAdaptersFactoryService> _adaptersFactory;
21+
private readonly Lazy<IContentType> _razorLSPContentType;
22+
private readonly Lazy<IVsFeatureFlags> _featureFlags;
23+
24+
public RazorEditorFactory(AsyncPackage package) : base(package)
25+
{
26+
_adaptersFactory = new Lazy<IVsEditorAdaptersFactoryService>(() =>
27+
{
28+
var componentModel = (IComponentModel)AsyncPackage.GetGlobalService(typeof(SComponentModel));
29+
var adaptersFactory = componentModel.GetService<IVsEditorAdaptersFactoryService>();
30+
return adaptersFactory;
31+
});
32+
33+
_razorLSPContentType = new Lazy<IContentType>(() =>
34+
{
35+
var componentModel = (IComponentModel)AsyncPackage.GetGlobalService(typeof(SComponentModel));
36+
var contentTypeService = componentModel.GetService<IContentTypeRegistryService>();
37+
var contentType = contentTypeService.GetContentType(RazorLSPContentTypeDefinition.Name);
38+
return contentType;
39+
});
40+
41+
_featureFlags = new Lazy<IVsFeatureFlags>(() =>
42+
{
43+
var featureFlags = (IVsFeatureFlags)AsyncPackage.GetGlobalService(typeof(SVsFeatureFlags));
44+
return featureFlags;
45+
});
46+
}
47+
48+
private bool IsLSPRazorEditorEnabled
49+
{
50+
get
51+
{
52+
var lspRazorEnabledString = Environment.GetEnvironmentVariable(RazorLSPEditorFeatureFlag);
53+
bool.TryParse(lspRazorEnabledString, out var enabled);
54+
if (enabled)
55+
{
56+
return true;
57+
}
58+
59+
enabled = _featureFlags.Value.IsFeatureEnabled(RazorLSPEditorFeatureFlag, defaultValue: false);
60+
return enabled;
61+
}
62+
}
63+
64+
public override int CreateEditorInstance(
65+
uint createDocFlags,
66+
string moniker,
67+
string physicalView,
68+
IVsHierarchy pHier,
69+
uint itemid,
70+
IntPtr existingDocData,
71+
out IntPtr docView,
72+
out IntPtr docData,
73+
out string editorCaption,
74+
out Guid cmdUI,
75+
out int cancelled)
76+
{
77+
if (!IsLSPRazorEditorEnabled)
78+
{
79+
docView = default;
80+
docData = default;
81+
editorCaption = null;
82+
cmdUI = default;
83+
cancelled = 0;
84+
85+
// Razor LSP is not enabled, allow another editor to handle this document
86+
return VSConstants.VS_E_UNSUPPORTEDFORMAT;
87+
}
88+
89+
var editorInstance = base.CreateEditorInstance(createDocFlags, moniker, physicalView, pHier, itemid, existingDocData, out docView, out docData, out editorCaption, out cmdUI, out cancelled);
90+
var textLines = (IVsTextLines)Marshal.GetObjectForIUnknown(docData);
91+
92+
SetTextBufferContentType(textLines);
93+
94+
return editorInstance;
95+
}
96+
97+
private void SetTextBufferContentType(IVsTextLines textLines)
98+
{
99+
var textBufferDataEventsGuid = typeof(IVsTextBufferDataEvents).GUID;
100+
var connectionPointContainer = textLines as IConnectionPointContainer;
101+
connectionPointContainer.FindConnectionPoint(textBufferDataEventsGuid, out var connectionPoint);
102+
var contentTypeSetter = new TextBufferContentTypeSetter(
103+
textLines,
104+
_adaptersFactory.Value,
105+
_razorLSPContentType.Value);
106+
contentTypeSetter.Attach(connectionPoint);
107+
108+
// Next, the editor typically resets the ContentType after TextBuffer creation. We need to let them know
109+
// to not update the content type.
110+
var userData = textLines as IVsUserData;
111+
var hresult = userData.SetData(VSConstants.VsTextBufferUserDataGuid.VsBufferDetectLangSID_guid, false);
112+
113+
ErrorHandler.ThrowOnFailure(hresult);
114+
}
115+
116+
private class TextBufferContentTypeSetter : IVsTextBufferDataEvents
117+
{
118+
private readonly IVsTextLines _textLines;
119+
private readonly IVsEditorAdaptersFactoryService _adaptersFactory;
120+
private readonly IContentType _razorLSPContentType;
121+
private IConnectionPoint _connectionPoint;
122+
private uint _connectionPointCookie;
123+
124+
public TextBufferContentTypeSetter(
125+
IVsTextLines textLines,
126+
IVsEditorAdaptersFactoryService adaptersFactory,
127+
IContentType razorLSPContentType)
128+
{
129+
_textLines = textLines;
130+
_adaptersFactory = adaptersFactory;
131+
_razorLSPContentType = razorLSPContentType;
132+
}
133+
134+
public void Attach(IConnectionPoint connectionPoint)
135+
{
136+
if (connectionPoint is null)
137+
{
138+
throw new ArgumentNullException(nameof(connectionPoint));
139+
}
140+
141+
_connectionPoint = connectionPoint;
142+
143+
connectionPoint.Advise(this, out _connectionPointCookie);
144+
}
145+
146+
public void OnFileChanged(uint grfChange, uint dwFileAttrs)
147+
{
148+
}
149+
150+
public int OnLoadCompleted(int fReload)
151+
{
152+
try
153+
{
154+
var diskBuffer = _adaptersFactory.GetDocumentBuffer(_textLines);
155+
diskBuffer.ChangeContentType(_razorLSPContentType, editTag: null);
156+
}
157+
finally
158+
{
159+
_connectionPoint.Unadvise(_connectionPointCookie);
160+
}
161+
162+
return VSConstants.S_OK;
163+
}
164+
}
165+
}
166+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.ComponentModel.Composition;
2+
using Microsoft.VisualStudio.LanguageServer.Client;
3+
using Microsoft.VisualStudio.Utilities;
4+
5+
namespace Microsoft.VisualStudio.LanguageServerClient.Razor
6+
{
7+
internal sealed class RazorLSPContentTypeDefinition
8+
{
9+
public const string Name = "RazorLSP";
10+
public const string CSHTMLFileExtension = ".cshtml";
11+
public const string RazorFileExtension = ".razor";
12+
13+
/// <summary>
14+
/// Exports the Razor LSP content type
15+
/// </summary>
16+
[Export]
17+
[Name(Name)]
18+
[BaseDefinition(CodeRemoteContentDefinition.CodeRemoteContentTypeName)]
19+
public ContentTypeDefinition RazorLSPContentType { get; set; }
20+
21+
// We can't assocaite the Razor LSP content type with the above file extensions because there's already a content type
22+
// associated with them. Instead, we utilize our RazorEditorFactory to assign the RazorLSPContentType to .razor/.cshtml
23+
// files.
24+
}
25+
}

src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
6666
</PropertyGroup>
6767
<ItemGroup>
68+
<EmbeddedResource Include="Resources.resx">
69+
<GenerateSource>true</GenerateSource>
70+
<Generator>MSBuild:_GenerateResxSource</Generator>
71+
</EmbeddedResource>
6872
<EmbeddedResource Include="VSPackage.resx">
6973
<MergeWithCTO>true</MergeWithCTO>
7074
<ManifestResourceName>VSPackage</ManifestResourceName>

src/Razor/src/Microsoft.VisualStudio.RazorExtension/RazorPackage.cs

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,32 @@
44
using System;
55
using System.Runtime.InteropServices;
66
using System.Threading;
7+
using Microsoft.VisualStudio.LanguageServerClient.Razor;
78
using Microsoft.VisualStudio.Shell;
89
using Task = System.Threading.Tasks.Task;
910

1011
namespace Microsoft.VisualStudio.RazorExtension
1112
{
12-
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
13+
// We attach to the 51st priority order because the traditional Web + XML editors have priority 50. We need to be loaded prior to them
14+
// since we want to have the option to own the experience for Razor files
15+
[ProvideEditorExtension(typeof(RazorEditorFactory), RazorLSPContentTypeDefinition.CSHTMLFileExtension, 52, NameResourceID = 101)]
16+
[ProvideEditorExtension(typeof(RazorEditorFactory), RazorLSPContentTypeDefinition.RazorFileExtension, 52, NameResourceID = 101)]
17+
[ProvideEditorFactory(typeof(RazorEditorFactory), 101)]
18+
[PackageRegistration(UseManagedResourcesOnly = true)]
1319
[AboutDialogInfo(PackageGuidString, "ASP.NET Core Razor Language Services", "#110", "#112", IconResourceID = "#400")]
1420
[Guid(PackageGuidString)]
1521
public sealed class RazorPackage : AsyncPackage
1622
{
1723
public const string PackageGuidString = "13b72f58-279e-49e0-a56d-296be02f0805";
1824

25+
private RazorEditorFactory _editorFactory;
26+
1927
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
2028
{
2129
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
30+
31+
_editorFactory = new RazorEditorFactory(this);
32+
RegisterEditorFactory(_editorFactory);
2233
}
2334
}
2435
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<root>
3+
<!--
4+
Microsoft ResX Schema
5+
6+
Version 2.0
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
11+
associated with the data types.
12+
13+
Example:
14+
15+
... ado.net/XML headers & schema ...
16+
<resheader name="resmimetype">text/microsoft-resx</resheader>
17+
<resheader name="version">2.0</resheader>
18+
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
19+
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
20+
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
21+
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
22+
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
23+
<value>[base64 mime encoded serialized .NET Framework object]</value>
24+
</data>
25+
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
26+
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
27+
<comment>This is a comment</comment>
28+
</data>
29+
30+
There are any number of "resheader" rows that contain simple
31+
name/value pairs.
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
37+
mimetype set.
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
41+
extensible. For a given mimetype the value must be set accordingly:
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
45+
read any of the formats listed below.
46+
47+
mimetype: application/x-microsoft.net.object.binary.base64
48+
value : The object must be serialized with
49+
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
50+
: and then encoded with base64 encoding.
51+
52+
mimetype: application/x-microsoft.net.object.soap.base64
53+
value : The object must be serialized with
54+
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
55+
: and then encoded with base64 encoding.
56+
57+
mimetype: application/x-microsoft.net.object.bytearray.base64
58+
value : The object must be serialized into a byte array
59+
: using a System.ComponentModel.TypeConverter
60+
: and then encoded with base64 encoding.
61+
-->
62+
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
63+
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
64+
<xsd:element name="root" msdata:IsDataSet="true">
65+
<xsd:complexType>
66+
<xsd:choice maxOccurs="unbounded">
67+
<xsd:element name="metadata">
68+
<xsd:complexType>
69+
<xsd:sequence>
70+
<xsd:element name="value" type="xsd:string" minOccurs="0" />
71+
</xsd:sequence>
72+
<xsd:attribute name="name" use="required" type="xsd:string" />
73+
<xsd:attribute name="type" type="xsd:string" />
74+
<xsd:attribute name="mimetype" type="xsd:string" />
75+
<xsd:attribute ref="xml:space" />
76+
</xsd:complexType>
77+
</xsd:element>
78+
<xsd:element name="assembly">
79+
<xsd:complexType>
80+
<xsd:attribute name="alias" type="xsd:string" />
81+
<xsd:attribute name="name" type="xsd:string" />
82+
</xsd:complexType>
83+
</xsd:element>
84+
<xsd:element name="data">
85+
<xsd:complexType>
86+
<xsd:sequence>
87+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
88+
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
89+
</xsd:sequence>
90+
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
91+
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
92+
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
93+
<xsd:attribute ref="xml:space" />
94+
</xsd:complexType>
95+
</xsd:element>
96+
<xsd:element name="resheader">
97+
<xsd:complexType>
98+
<xsd:sequence>
99+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
100+
</xsd:sequence>
101+
<xsd:attribute name="name" type="xsd:string" use="required" />
102+
</xsd:complexType>
103+
</xsd:element>
104+
</xsd:choice>
105+
</xsd:complexType>
106+
</xsd:element>
107+
</xsd:schema>
108+
<resheader name="resmimetype">
109+
<value>text/microsoft-resx</value>
110+
</resheader>
111+
<resheader name="version">
112+
<value>2.0</value>
113+
</resheader>
114+
<resheader name="reader">
115+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
116+
</resheader>
117+
<resheader name="writer">
118+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119+
</resheader>
120+
<data name="IDS_EDITORNAME" xml:space="preserve">
121+
<value>LSP Razor Editor</value>
122+
</data>
123+
</root>

0 commit comments

Comments
 (0)