Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
<Preview>true</Preview>
</Metadata>
<Installation>
<InstallationTarget Version="[17.10, 18.0)" Id="Microsoft.VisualStudio.Community">
<InstallationTarget Version="[17.10,)" Id="Microsoft.VisualStudio.Community">
<ProductArchitecture>amd64</ProductArchitecture>
</InstallationTarget>
<InstallationTarget Version="[17.10, 18.0)" Id="Microsoft.VisualStudio.Community">
<InstallationTarget Version="[17.10,)" Id="Microsoft.VisualStudio.Community">
<ProductArchitecture>arm64</ProductArchitecture>
</InstallationTarget>
</Installation>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

using System.ComponentModel.Composition;

using Microsoft.VisualStudio.Language.StandardClassification;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Tagging;

using MonoDevelop.MSBuild.Language.Expressions;

namespace MonoDevelop.MSBuild.Editor.Classification
{
/// <summary>
/// Maps MSBuild syntax constructs to classification tags, for use by <see cref="MSBuildClassificationTagger"/>
/// when the VS TextMate service is unavailable.
/// XML constructs prefer the classification types registered by Visual Studio's built-in XML editor,
/// so files look exactly like the XML editor (including user Fonts &amp; Colors customizations),
/// falling back to built-in theme-aware types in hosts that do not register them.
/// </summary>
[Export]
sealed class MSBuildClassificationTypeMap
{
// classification type names registered by Visual Studio's built-in XML editor
const string XmlNameTypeName = "XML Name";
const string XmlAttributeTypeName = "XML Attribute";
const string XmlAttributeValueTypeName = "XML Attribute Value";
const string XmlAttributeQuotesTypeName = "XML Attribute Quotes";
const string XmlDelimiterTypeName = "XML Delimiter";
const string XmlCommentTypeName = "XML Comment";
const string XmlCDataSectionTypeName = "XML CData Section";
const string XmlTextTypeName = "XML Text";
const string XmlProcessingInstructionTypeName = "XML Processing Instruction";

/// <summary>
/// Creates the map, resolving classification types from the registry.
/// </summary>
/// <param name="classificationTypeRegistry">The editor's classification type registry.</param>
[ImportingConstructor]
public MSBuildClassificationTypeMap (IClassificationTypeRegistryService classificationTypeRegistry)
{
ElementName = CreateTag (classificationTypeRegistry, XmlNameTypeName, PredefinedClassificationTypeNames.MarkupNode);
AttributeName = CreateTag (classificationTypeRegistry, XmlAttributeTypeName, PredefinedClassificationTypeNames.MarkupAttribute);
AttributeValue = CreateTag (classificationTypeRegistry, XmlAttributeValueTypeName, PredefinedClassificationTypeNames.String);
AttributeQuotes = CreateTag (classificationTypeRegistry, XmlAttributeQuotesTypeName, PredefinedClassificationTypeNames.String);
Delimiter = CreateTag (classificationTypeRegistry, XmlDelimiterTypeName, PredefinedClassificationTypeNames.Operator);
Comment = CreateTag (classificationTypeRegistry, XmlCommentTypeName, PredefinedClassificationTypeNames.Comment);
CDataSection = CreateTag (classificationTypeRegistry, XmlCDataSectionTypeName, PredefinedClassificationTypeNames.Literal);
Text = CreateTag (classificationTypeRegistry, XmlTextTypeName, PredefinedClassificationTypeNames.Text);
ProcessingInstruction = CreateTag (classificationTypeRegistry, XmlProcessingInstructionTypeName, PredefinedClassificationTypeNames.PreprocessorKeyword);
// the XML editor has no dedicated entity reference classification, it uses the name color
EntityReference = CreateTag (classificationTypeRegistry, XmlNameTypeName, PredefinedClassificationTypeNames.MarkupNode);

ExpressionName = CreateTag (classificationTypeRegistry, PredefinedClassificationTypeNames.Keyword);
ExpressionDelimiter = CreateTag (classificationTypeRegistry, PredefinedClassificationTypeNames.Operator);
FunctionName = CreateTag (classificationTypeRegistry, PredefinedClassificationTypeNames.Identifier);
BoolLiteral = CreateTag (classificationTypeRegistry, PredefinedClassificationTypeNames.Keyword);
NumberLiteral = CreateTag (classificationTypeRegistry, PredefinedClassificationTypeNames.Number);

ResolvedTypeNames =
$"elementName='{ElementName.ClassificationType.Classification}', " +
$"attributeName='{AttributeName.ClassificationType.Classification}', " +
$"attributeValue='{AttributeValue.ClassificationType.Classification}', " +
$"attributeQuotes='{AttributeQuotes.ClassificationType.Classification}', " +
$"delimiter='{Delimiter.ClassificationType.Classification}', " +
$"comment='{Comment.ClassificationType.Classification}', " +
$"cdata='{CDataSection.ClassificationType.Classification}', " +
$"text='{Text.ClassificationType.Classification}', " +
$"processingInstruction='{ProcessingInstruction.ClassificationType.Classification}'";
}

public ClassificationTag ElementName { get; }
public ClassificationTag AttributeName { get; }
public ClassificationTag AttributeValue { get; }
public ClassificationTag AttributeQuotes { get; }
public ClassificationTag Delimiter { get; }
public ClassificationTag Comment { get; }
public ClassificationTag CDataSection { get; }
public ClassificationTag Text { get; }
public ClassificationTag ProcessingInstruction { get; }
public ClassificationTag EntityReference { get; }
public ClassificationTag ExpressionName { get; }
public ClassificationTag ExpressionDelimiter { get; }
public ClassificationTag FunctionName { get; }
public ClassificationTag BoolLiteral { get; }
public ClassificationTag NumberLiteral { get; }

/// <summary>
/// Describes which classification type each XML bucket resolved to, for logging purposes.
/// </summary>
public string ResolvedTypeNames { get; }

/// <summary>
/// Gets the classification tag for an MSBuild expression node, or null if the node is not classified.
/// </summary>
/// <param name="node">The expression node.</param>
/// <returns>The tag for the node's whole span, or null to leave the span unclassified.</returns>
public ClassificationTag? GetTagForExpressionNode (ExpressionNode node)
=> node switch {
ExpressionPropertyName => ExpressionName,
ExpressionItemName => ExpressionName,
ExpressionFunctionName => FunctionName,
ExpressionArgumentBool => BoolLiteral,
ExpressionArgumentInt => NumberLiteral,
ExpressionArgumentFloat => NumberLiteral,
ExpressionArgumentString => AttributeValue,
_ => null
};

/// <summary>
/// Creates a classification tag for the first classification type name that resolves in the registry,
/// falling back to plain text if none is registered.
/// </summary>
/// <param name="classificationTypeRegistry">The editor's classification type registry.</param>
/// <param name="classificationTypeNames">Candidate classification type names, in order of preference.</param>
/// <returns>A tag for the resolved classification type.</returns>
static ClassificationTag CreateTag (IClassificationTypeRegistryService classificationTypeRegistry, params string[] classificationTypeNames)
{
foreach (string classificationTypeName in classificationTypeNames) {
IClassificationType? classificationType = classificationTypeRegistry.GetClassificationType (classificationTypeName);
if (classificationType is not null) {
return new ClassificationTag (classificationType);
}
}
return new ClassificationTag (classificationTypeRegistry.GetClassificationType (PredefinedClassificationTypeNames.Text));
}
}
}
68 changes: 68 additions & 0 deletions MonoDevelop.MSBuild.Editor/Classification/TextMateSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

using System;
using System.Diagnostics;
using System.IO;

using Microsoft.VisualStudio.Text;

namespace MonoDevelop.MSBuild.Editor.Classification
{
/// <summary>
/// Determines whether the Visual Studio TextMate colorization service is expected to work in the host process.
/// In VS 2026 (18.x) the TextMate asset service used by <see cref="MSBuildTextMateTagger"/> no longer
/// produces a working classification tagger, so classification falls back to <see cref="MSBuildClassificationTagger"/>.
/// </summary>
static class TextMateSupport
{
static readonly Lazy<bool> availability = new (ComputeAvailability);

/// <summary>
/// Whether the host is expected to support the legacy TextMate asset service (VS 17.x). Computed once per process.
/// </summary>
public static bool IsAvailable => availability.Value;

/// <summary>
/// Describes the host version probe result, for logging purposes.
/// </summary>
public static string HostDescription { get; private set; } = "unknown host";

/// <summary>
/// Computes whether the host is expected to support the legacy TextMate asset service.
/// </summary>
/// <returns>False if the host is known to be VS 18.0 (VS 2026) or later, true otherwise.</returns>
static bool ComputeAvailability ()
{
// primary probe: the version of the host process (devenv.exe -> 17.x for VS 2022, 18.x for VS 2026)
try {
string? mainModulePath = Process.GetCurrentProcess ().MainModule?.FileName;
if (mainModulePath is not null && string.Equals (Path.GetFileNameWithoutExtension (mainModulePath), "devenv", StringComparison.OrdinalIgnoreCase)) {
int productMajorVersion = FileVersionInfo.GetVersionInfo (mainModulePath).ProductMajorPart;
if (productMajorVersion > 0) {
HostDescription = $"devenv version {productMajorVersion}";
return productMajorVersion < 18;
}
}
} catch (Exception) {
// ignore, fall through to the editor assembly version probe
}

// fallback probe: the version of the loaded editor assemblies
try {
Version? editorAssemblyVersion = typeof (ITextBuffer).Assembly.GetName ().Version;
if (editorAssemblyVersion is not null && editorAssemblyVersion.Major > 0) {
HostDescription = $"editor assembly version {editorAssemblyVersion}";
return editorAssemblyVersion.Major < 18;
}
} catch (Exception) {
// ignore, assume TextMate is available; the null-result fallback in
// MSBuildTextMateTagger.CreateTagger still protects against a missing tagger
}

return true;
}
}
}
84 changes: 74 additions & 10 deletions MonoDevelop.MSBuild.Editor/MSBuildTextMateTagger.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#nullable enable

using System.ComponentModel.Composition;
using System.Linq;

using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;

using MonoDevelop.MSBuild.Editor.Classification;
using MonoDevelop.Xml.Editor.Logging;
using MonoDevelop.Xml.Editor.Parsing;

namespace MonoDevelop.MSBuild.Editor
{
/// <summary>
/// Provides classification and structure taggers for MSBuild buffers, delegating to the host's
/// TextMate service when it is available, and falling back to <see cref="MSBuildClassificationTagger"/>
/// for classification when it is not (e.g. VS 2026, issue #279).
/// </summary>
[Export (typeof (ITaggerProvider))]
[TagType (typeof (IClassificationTag))]
[TagType (typeof (IStructureTag))]
[ContentType (MSBuildContentType.Name)]
sealed class MSBuildTextMateTagger : ITaggerProvider
sealed partial class MSBuildTextMateTagger : ITaggerProvider
{
readonly ICommonEditorAssetServiceFactory? assetServiceFactory;
readonly XmlParserProvider parserProvider;
readonly MSBuildClassificationTypeMap typeMap;
readonly JoinableTaskContext joinableTaskContext;
readonly IEditorLoggerFactory loggerFactory;

/// <summary>
/// Creates the tagger provider.
/// </summary>
/// <param name="assetServiceFactory">The host's TextMate asset service factory. May be missing in hosts that do not provide it.</param>
/// <param name="parserProvider">Provider used to obtain per-buffer XML background parsers for the fallback tagger.</param>
/// <param name="typeMap">Shared map from MSBuild syntax constructs to classification tags for the fallback tagger.</param>
/// <param name="joinableTaskContext">The host's joinable task context.</param>
/// <param name="loggerFactory">Factory for per-buffer loggers.</param>
[ImportingConstructor]
public MSBuildTextMateTagger (ICommonEditorAssetServiceFactory assetServiceFactory)
public MSBuildTextMateTagger (
[Import (AllowDefault = true)] ICommonEditorAssetServiceFactory? assetServiceFactory,
XmlParserProvider parserProvider,
MSBuildClassificationTypeMap typeMap,
JoinableTaskContext joinableTaskContext,
IEditorLoggerFactory loggerFactory)
{
AssetServiceFactory = assetServiceFactory;
this.assetServiceFactory = assetServiceFactory;
this.parserProvider = parserProvider;
this.typeMap = typeMap;
this.joinableTaskContext = joinableTaskContext;
this.loggerFactory = loggerFactory;
}

public ICommonEditorAssetServiceFactory AssetServiceFactory { get; }
/// <summary>
/// Creates a tagger for the buffer, preferring the host's TextMate tagger and falling back
/// to <see cref="MSBuildClassificationTagger"/> for classification tags.
/// </summary>
/// <param name="buffer">The text buffer to tag.</param>
/// <returns>The tagger, or null if no tagger is available for the requested tag type.</returns>
public ITagger<T>? CreateTagger<T> (ITextBuffer buffer) where T : ITag
{
if (TextMateSupport.IsAvailable && assetServiceFactory is not null) {
ITagger<T>? textMateTagger = assetServiceFactory.GetOrCreate (buffer)
.FindAsset<ITaggerProvider> (
(metadata) => metadata.TagTypes.Any (tagType => typeof (T).IsAssignableFrom (tagType))
)
?.CreateTagger<T> (buffer);
if (textMateTagger is not null) {
return textMateTagger;
}
}

// The host's TextMate service is unavailable, so fall back to classification based on our
// own parsers. Structure tags need no fallback, as MonoDevelop.Xml's StructureTaggerProvider
// independently handles xmlcore-derived content types.
if (typeof (T).IsAssignableFrom (typeof (ClassificationTag))) {
return (ITagger<T>)(object)buffer.Properties.GetOrCreateSingletonProperty (() => {
ILogger logger = loggerFactory.GetLogger<MSBuildClassificationTagger> (buffer);
LogUsingFallbackClassifier (logger, TextMateSupport.HostDescription, typeMap.ResolvedTypeNames);
return new MSBuildClassificationTagger (buffer, parserProvider, typeMap, joinableTaskContext, logger);
});
}

return null;
}

public ITagger<T> CreateTagger<T> (ITextBuffer buffer) where T : ITag =>
AssetServiceFactory.GetOrCreate (buffer)
.FindAsset<ITaggerProvider> (
(metadata) => metadata.TagTypes.Any (tagType => typeof (T).IsAssignableFrom (tagType))
)
?.CreateTagger<T> (buffer);
[LoggerMessage (EventId = 0, Level = LogLevel.Information, Message = "TextMate classification unavailable ({hostDescription}), using built-in MSBuild classification tagger with classification types: {resolvedTypeNames}")]
static partial void LogUsingFallbackClassifier (ILogger logger, string hostDescription, string resolvedTypeNames);
}
}
Loading