From c6edb6f5ce7a75958a992008f931ce009e2e70cd Mon Sep 17 00:00:00 2001 From: Paul Harrington Date: Fri, 26 Jan 2024 20:18:40 -0800 Subject: [PATCH] Fix CodingConventions support in 17.10 (#125) * In Dev17, use IEditorOptions to retrieve coding conventions instead of the CodingConventions package * Remove global.json Because VS 2019 doesn't support dotnet 8 and VS 2022 doesn't install dotnet 7 Let each version pick the latest supported SDK. * Revert back to 17.0 SDK * Seal CodingConventions class --- global.json | 6 --- src/ColumnGuide/ColumnGuideAdornment.cs | 46 ++++++++-------- src/ColumnGuide/ColumnGuideFactory.cs | 9 ++-- src/VSIX/CodingConventions.cs | 71 ++++++++++++++++++++++++ src/VSIX_Dev17/CodingConventions.cs | 72 +++++++++++++++++++++++++ src/VSIX_Dev17/VSIX_Dev17.csproj | 5 +- 6 files changed, 170 insertions(+), 39 deletions(-) delete mode 100644 global.json create mode 100644 src/VSIX/CodingConventions.cs create mode 100644 src/VSIX_Dev17/CodingConventions.cs diff --git a/global.json b/global.json deleted file mode 100644 index b887553..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "7.0.100", - "rollForward": "feature" - } -} diff --git a/src/ColumnGuide/ColumnGuideAdornment.cs b/src/ColumnGuide/ColumnGuideAdornment.cs index 2fec024..e770974 100644 --- a/src/ColumnGuide/ColumnGuideAdornment.cs +++ b/src/ColumnGuide/ColumnGuideAdornment.cs @@ -1,7 +1,6 @@ // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.VisualStudio.CodingConventions; using Microsoft.VisualStudio.Text.Editor; using System; using System.Collections.Generic; @@ -65,19 +64,18 @@ internal class ColumnGuideAdornment : IDisposable /// The upon which the adornment will be drawn /// The guideline settings. /// The guideline brush. - /// The coding conventions manager for handling .editorconfig settings. - /// Telemetry interface. - public ColumnGuideAdornment(IWpfTextView view, ITextEditorGuidesSettings settings, GuidelineBrush guidelineBrush, ICodingConventionsManager codingConventionsManager) + /// The coding conventions manager for handling .editorconfig settings. + public ColumnGuideAdornment(IWpfTextView view, ITextEditorGuidesSettings settings, GuidelineBrush guidelineBrush, CodingConventions codingConventions) { _view = view; _guidelineBrush = guidelineBrush; _guidelineBrush.BrushChanged += GuidelineBrushChanged; _strokeParameters = StrokeParameters.FromBrush(_guidelineBrush.Brush); - if (codingConventionsManager != null && view.TryGetTextDocument(out var textDocument)) + if (codingConventions != null) { _codingConventionsCancellationTokenSource = new CancellationTokenSource(); - var fireAndForgetTask = LoadGuidelinesFromEditorConfigAsync(codingConventionsManager, textDocument.FilePath); + var fireAndForgetTask = LoadGuidelinesFromEditorConfigAsync(codingConventions, view); } InitializeGuidelines(settings.GuideLinePositionsInChars); @@ -274,32 +272,34 @@ private void AddGuidelinesToAdornmentLayer() /// /// Try to load guideline positions from .editorconfig /// - /// The coding conventions (.editorconfig) manager. - /// Path to the document being edited. + /// The coding conventions (.editorconfig) manager. + /// Editor's WPF view. /// A task which completes when the convention has been loaded and applied. - private async Task LoadGuidelinesFromEditorConfigAsync(ICodingConventionsManager codingConventionsManager, string filePath) + private async Task LoadGuidelinesFromEditorConfigAsync(CodingConventions codingConventions, IWpfTextView view) { - var cancellationToken = _codingConventionsCancellationTokenSource.Token; - var codingConventionContext = await codingConventionsManager.GetConventionContextAsync(filePath, cancellationToken).ConfigureAwait(false); + CancellationToken cancellationToken = _codingConventionsCancellationTokenSource.Token; + CodingConventions.Context context = await codingConventions.CreateContextAsync(view, cancellationToken).ConfigureAwait(false); + if (context is null) + { + return; + } - codingConventionContext.CodingConventionsChangedAsync += OnCodingConventionsChangedAsync; - cancellationToken.Register(() => codingConventionContext.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync); + context.ConventionsChanged += UpdateGuidelinesFromCodingConvention; + cancellationToken.Register(() => context.ConventionsChanged -= UpdateGuidelinesFromCodingConvention); - await UpdateGuidelinesFromCodingConventionAsync(codingConventionContext, cancellationToken).ConfigureAwait(false); + UpdateGuidelinesFromCodingConvention(context); } - private Task OnCodingConventionsChangedAsync(object sender, CodingConventionsChangedEventArgs arg) => UpdateGuidelinesFromCodingConventionAsync((ICodingConventionContext)sender, _codingConventionsCancellationTokenSource.Token); - - private Task UpdateGuidelinesFromCodingConventionAsync(ICodingConventionContext codingConventionContext, CancellationToken cancellationToken) + private void UpdateGuidelinesFromCodingConvention(CodingConventions.Context context) { - if (cancellationToken.IsCancellationRequested) + if (_codingConventionsCancellationTokenSource.Token.IsCancellationRequested) { - return Task.FromCanceled(cancellationToken); + return; } StrokeParameters strokeParameters = null; - if (codingConventionContext.CurrentConventions.TryGetConventionValue("guidelines_style", out string guidelines_style)) + if (context.TryGetCurrentSetting("guidelines_style", out string guidelines_style)) { if (TryParseStrokeParametersFromCodingConvention(guidelines_style, out strokeParameters)) { @@ -310,13 +310,13 @@ private Task UpdateGuidelinesFromCodingConventionAsync(ICodingConventionContext ICollection guidelines = null; - if (codingConventionContext.CurrentConventions.TryGetConventionValue("guidelines", out string guidelinesConventionValue)) + if (context.TryGetCurrentSetting("guidelines", out string guidelinesConventionValue)) { guidelines = ParseGuidelinesFromCodingConvention(guidelinesConventionValue, strokeParameters); } // Also support max_line_length: https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length - if (codingConventionContext.CurrentConventions.TryGetConventionValue("max_line_length", out string max_line_length) && TryParsePosition(max_line_length, out int maxLineLengthValue)) + if (context.TryGetCurrentSetting("max_line_length", out string max_line_length) && TryParsePosition(max_line_length, out int maxLineLengthValue)) { (guidelines ?? (guidelines = new List())).Add(new Guideline(maxLineLengthValue, strokeParameters)); } @@ -354,8 +354,6 @@ private Task UpdateGuidelinesFromCodingConventionAsync(ICodingConventionContext Telemetry.Client.TrackEvent(eventTelemetry); s_sentEditorConfigTelemetry = true; } - - return Task.CompletedTask; } private bool HaveGuidelinesChanged(IEnumerable newGuidelines) diff --git a/src/ColumnGuide/ColumnGuideFactory.cs b/src/ColumnGuide/ColumnGuideFactory.cs index 38fa358..4058ca8 100644 --- a/src/ColumnGuide/ColumnGuideFactory.cs +++ b/src/ColumnGuide/ColumnGuideFactory.cs @@ -1,7 +1,6 @@ // Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.VisualStudio.CodingConventions; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Utilities; using System.Collections.Generic; @@ -42,7 +41,7 @@ public void TextViewCreated(IWpfTextView textView) { // Always create the adornment, even if there are no guidelines, since we // respond to dynamic changes. - var _ = new ColumnGuideAdornment(textView, TextEditorGuidesSettings, GuidelineBrush, CodingConventionsManager); + var _ = new ColumnGuideAdornment(textView, TextEditorGuidesSettings, GuidelineBrush, CodingConventions); } public void OnImportsSatisfied() @@ -120,11 +119,11 @@ internal static void AddGuidelinesToTelemetry(EventTelemetry eventTelemetry, IEn [Import] private GuidelineBrush GuidelineBrush { get; set; } - [Import(AllowDefault = true)] - private ICodingConventionsManager CodingConventionsManager { get; set; } + [Import] + private CodingConventions CodingConventions { get; set; } [Import] private HostServices HostServices { get; set; } } - #endregion //Adornment Factory +#endregion //Adornment Factory } diff --git a/src/VSIX/CodingConventions.cs b/src/VSIX/CodingConventions.cs new file mode 100644 index 0000000..faaf5b0 --- /dev/null +++ b/src/VSIX/CodingConventions.cs @@ -0,0 +1,71 @@ +// Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.CodingConventions; +using Microsoft.VisualStudio.Text.Editor; + +namespace EditorGuidelines +{ + /// + /// Coding conventions support via Microsoft.VisualStudio.CodingConventions assembly. + /// + [Export] + internal sealed class CodingConventions + { + /// + /// Try to import from Microsoft.VisualStudio.CodingConventions. + /// + [Import(AllowDefault = true)] + private ICodingConventionsManager CodingConventionsManager { get; set; } + + public async Task CreateContextAsync(IWpfTextView view, CancellationToken cancellationToken) + { + if (CodingConventionsManager is null) + { + // Coding Conventions not available in this SKU. + return null; + } + + if (!view.TryGetTextDocument(out var textDocument)) + { + return null; + } + + string filePath = textDocument.FilePath; + ICodingConventionContext codingConventionContext = await CodingConventionsManager.GetConventionContextAsync(filePath, cancellationToken).ConfigureAwait(false); + return new Context(codingConventionContext, cancellationToken); + } + + /// + /// Coding conventions narrowed to a single file. + /// + public class Context + { + private readonly ICodingConventionContext _innerContext; + + public Context(ICodingConventionContext innerContext, CancellationToken cancellationToken) + { + _innerContext = innerContext; + _innerContext.CodingConventionsChangedAsync += OnCodingConventionsChangedAsync; + cancellationToken.Register(() => _innerContext.CodingConventionsChangedAsync -= OnCodingConventionsChangedAsync); + } + + private Task OnCodingConventionsChangedAsync(object sender, CodingConventionsChangedEventArgs arg) + { + ConventionsChanged?.Invoke(this); + return Task.CompletedTask; + } + + public delegate void ConventionsChangedEventHandler(Context sender); + + public event ConventionsChangedEventHandler ConventionsChanged; + + public bool TryGetCurrentSetting(string key, out string value) + { + return _innerContext.CurrentConventions.TryGetConventionValue(key, out value); + } + } + } +} diff --git a/src/VSIX_Dev17/CodingConventions.cs b/src/VSIX_Dev17/CodingConventions.cs new file mode 100644 index 0000000..5af8f15 --- /dev/null +++ b/src/VSIX_Dev17/CodingConventions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Paul Harrington. All Rights Reserved. Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.ComponentModel.Composition; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.VisualStudio.Text.Editor; +using System.Collections.Generic; + +namespace EditorGuidelines +{ + /// + /// Dev 17 support for Coding Conventions. Uses the dictionary supplied via . + /// + [Export] + internal sealed class CodingConventions + { + public Task CreateContextAsync(IWpfTextView view, CancellationToken cancellationToken) + { + return Task.FromResult(new Context(view.Options, cancellationToken)); + } + + /// + /// Coding conventions narrowed to a single file. + /// + public class Context + { + private readonly IEditorOptions _editorOptions; + private readonly CancellationToken _cancellationToken; + private IReadOnlyDictionary _currentConventions; + + // This is the same as DefaultOptions.RawCodingConventionsSnapshotOptionName from the 17.6 + // editor SDK. However, by not referencing that constant, we can avoid taking a dependency + // on the 17.6 SDK and can continue to load on earlier versions (albeit without + // CodingConventions support). + private const string c_codingConventionsSnapshotOptionName = "CodingConventionsSnapshot"; + + public Context(IEditorOptions editorOptions, CancellationToken cancellationToken) + { + _editorOptions = editorOptions; + _cancellationToken = cancellationToken; + + _editorOptions.OptionChanged += OnEditorOptionChanged; + _cancellationToken.Register(() => _editorOptions.OptionChanged -= OnEditorOptionChanged); + } + + private void OnEditorOptionChanged(object sender, EditorOptionChangedEventArgs e) + { + if (e.OptionId == c_codingConventionsSnapshotOptionName) + { + _currentConventions = _editorOptions.GetOptionValue>(c_codingConventionsSnapshotOptionName); + ConventionsChanged?.Invoke(this); + } + } + + public delegate void ConventionsChangedEventHandler(Context sender); + + public event ConventionsChangedEventHandler ConventionsChanged; + + public bool TryGetCurrentSetting(string key, out string value) + { + if (_currentConventions is null || !_currentConventions.TryGetValue(key, out object obj) || obj is null) + { + value = null; + return false; + } + + value = obj.ToString(); + return !string.IsNullOrEmpty(value); + } + } + } +} diff --git a/src/VSIX_Dev17/VSIX_Dev17.csproj b/src/VSIX_Dev17/VSIX_Dev17.csproj index 429b11c..28f99b2 100644 --- a/src/VSIX_Dev17/VSIX_Dev17.csproj +++ b/src/VSIX_Dev17/VSIX_Dev17.csproj @@ -39,13 +39,10 @@ - - compile; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive