Skip to content
Merged
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
@@ -1,4 +1,8 @@
{
"MarkdownLinter.RunLinterOnCurrentFileCommand.DisplayName": "Run Linter on open file",
"MarkdownLinter.RunLinterOnSolutionCommand.DisplayName": "Run Linter on solution"
"MarkdownLinter.RunLinterOnSolutionCommand.DisplayName": "Run Linter on solution",
"MarkdownLinter.Settings.Category.DisplayName": "Markdown Linter",
"MarkdownLinter.Settings.Category.Description": "Markdown Linter Settings",
"MarkdownLinter.Settings.DisabledRules.DisplayName": "Disabled rules",
"MarkdownLinter.Settings.DisabledRules.Description": "List of disabled rules to pass to markdown-cli"
}
47 changes: 31 additions & 16 deletions New_Extensibility_Model/Samples/MarkdownLinter/LinterUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace MarkdownLinter;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft;
using Microsoft.VisualStudio.Extensibility.Editor;
Expand All @@ -19,27 +20,44 @@ namespace MarkdownLinter;
using Microsoft.VisualStudio.RpcContracts.DiagnosticManagement;
using Microsoft.VisualStudio.Threading;

#pragma warning disable VSEXTPREVIEW_SETTINGS // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

/// <summary>
/// Helper class for running linter on a string or file.
/// </summary>
internal static class LinterUtilities
internal class LinterUtilities
{
private static readonly Regex LinterOutputRegex = new(@"(?<File>[^:]+):(?<Line>\d*)(:(?<Column>\d*))? (?<Error>.*)/(?<Description>.*)", RegexOptions.Compiled);

private readonly Settings.MarkdownLinterCategoryObserver settingsObserver;

public LinterUtilities(Settings.MarkdownLinterCategoryObserver settingsObserver)
{
this.settingsObserver = settingsObserver;
}

/// <summary>
/// Runs markdown linter on a file uri and returns diagnostic entries.
/// </summary>
/// <param name="fileUri">File uri to run markdown linter on.</param>
/// <param name="cancellationToken">Cancellation token to monitor.</param>
/// <returns>an enumeration of <see cref="DocumentDiagnostic"/> entries for warnings in the markdown file.</returns>
public static async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnFileAsync(Uri fileUri)
public async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnFileAsync(Uri fileUri, CancellationToken cancellationToken)
{
using var linter = new Process();
var lineQueue = new AsyncQueue<string>();

var snapshot = await this.settingsObserver.GetSnapshotAsync(cancellationToken);
string disabledRules = snapshot.DisabledRules.ValueOrDefault(string.Empty);

string args = "/c \"npx markdownlint-cli" +
(disabledRules.Length > 0 ? $" --disable {disabledRules} --" : string.Empty) +
$" \"{fileUri.LocalPath}\"\"";

linter.StartInfo = new ProcessStartInfo()
{
FileName = "node.exe",
Arguments = $"\"{Environment.ExpandEnvironmentVariables("%APPDATA%\\npm\\node_modules\\markdownlint-cli\\markdownlint.js")}\" \"{fileUri.LocalPath}\"",
FileName = "cmd.exe",
Arguments = args,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
Expand Down Expand Up @@ -76,18 +94,25 @@ public static async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnFileAsync(U
/// Runs markdown linter on a given text document and returns diagnostic entries.
/// </summary>
/// <param name="textDocument">Document to run markdown linter on.</param>
/// <param name="cancellationToken">Cancellation token to monitor.</param>
/// <returns>an enumeration of <see cref="DocumentDiagnostic"/> entries for warnings in the markdown file.</returns>
public static async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnDocumentAsync(ITextDocumentSnapshot textDocument)
public async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnDocumentAsync(ITextDocumentSnapshot textDocument, CancellationToken cancellationToken)
{
using var linter = new Process();
var lineQueue = new AsyncQueue<string>();

var content = textDocument.Text.CopyToString();

var snapshot = await this.settingsObserver.GetSnapshotAsync(cancellationToken);
string disabledRules = snapshot.DisabledRules.ValueOrDefault(string.Empty);

string args = "/k \"npx markdownlint-cli --stdin" +
(disabledRules.Length > 0 ? $" --disable {disabledRules}" : string.Empty) + "\"";

linter.StartInfo = new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = $"/k \"{Environment.ExpandEnvironmentVariables("%APPDATA%\\npm\\markdownlint.cmd")}\" -s",
Arguments = args,
RedirectStandardError = true,
RedirectStandardInput = true,
UseShellExecute = false,
Expand Down Expand Up @@ -125,16 +150,6 @@ public static async Task<IEnumerable<DocumentDiagnostic>> RunLinterOnDocumentAsy
return CreateDocumentDiagnosticsForOpenDocument(textDocument, markdownDiagnostics);
}

/// <summary>
/// Checks if the given path is a valid markdown file.
/// </summary>
/// <param name="localPath">Local file path to verify.</param>
/// <returns>true if file is a markdown file, false otherwise.</returns>
public static bool IsValidMarkdownFile(string localPath)
{
return localPath is not null && Path.GetExtension(localPath).Equals(".md", StringComparison.OrdinalIgnoreCase);
}

private static IEnumerable<DocumentDiagnostic> CreateDocumentDiagnosticsForOpenDocument(ITextDocumentSnapshot document, IEnumerable<MarkdownDiagnosticInfo> diagnostics)
{
foreach (var diagnostic in diagnostics)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace MarkdownLinter;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.Animation;
using Microsoft;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Documents;
Expand All @@ -28,17 +29,20 @@ internal class MarkdownDiagnosticsService : DisposableObject
#pragma warning restore CA2213 // Disposable fields should be disposed
private readonly Dictionary<Uri, CancellationTokenSource> documentCancellationTokens;
private readonly Task initializationTask;
private readonly LinterUtilities linterUtilities;
private OutputChannel? outputChannel;
private DiagnosticsReporter? diagnosticsReporter;

/// <summary>
/// Initializes a new instance of the <see cref="MarkdownDiagnosticsService"/> class.
/// </summary>
/// <param name="extensibility">Extensibility object.</param>
public MarkdownDiagnosticsService(VisualStudioExtensibility extensibility)
/// <param name="linterUtilities">Service for running the linter on markdown files.</param>
public MarkdownDiagnosticsService(VisualStudioExtensibility extensibility, LinterUtilities linterUtilities)
{
this.extensibility = Requires.NotNull(extensibility, nameof(extensibility));
this.documentCancellationTokens = new Dictionary<Uri, CancellationTokenSource>();
this.linterUtilities = Requires.NotNull(linterUtilities, nameof(linterUtilities));
this.initializationTask = Task.Run(this.InitializeAsync);
}

Expand Down Expand Up @@ -71,7 +75,7 @@ public async Task ProcessFileAsync(Uri documentUri, CancellationToken cancellati

try
{
var diagnostics = await LinterUtilities.RunLinterOnFileAsync(documentUri);
var diagnostics = await this.linterUtilities.RunLinterOnFileAsync(documentUri, cancellationToken);

await this.diagnosticsReporter!.ClearDiagnosticsAsync(documentUri, cancellationToken);
await this.diagnosticsReporter!.ReportDiagnosticsAsync(diagnostics, cancellationToken);
Expand Down Expand Up @@ -151,7 +155,7 @@ private async Task ProcessDocumentAsync(ITextDocumentSnapshot documentSnapshot,

try
{
var diagnostics = await LinterUtilities.RunLinterOnDocumentAsync(documentSnapshot);
var diagnostics = await this.linterUtilities.RunLinterOnDocumentAsync(documentSnapshot, cancellationToken);

await this.diagnosticsReporter!.ClearDiagnosticsAsync(documentSnapshot, cancellationToken);
await this.diagnosticsReporter!.ReportDiagnosticsAsync(diagnostics, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ protected override void InitializeServices(IServiceCollection serviceCollection)
{
base.InitializeServices(serviceCollection);

// Add the settings observer created by MarkdownLinterSettingDefinitions to use in LinterUtilies.
serviceCollection.AddSettingsObservers();

// Add linter utilities as a singleton, it depends on settings observer.
serviceCollection.AddSingleton<LinterUtilities>();

// As of now, any instance that ingests VisualStudioExtensibility is required to be added as a scoped
// service.
serviceCollection.AddScoped<MarkdownDiagnosticsService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace MarkdownLinter;

using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Settings;

#pragma warning disable VSEXTPREVIEW_SETTINGS // The settings API is currently in preview and marked as experimental

internal static class MarkdownLinterSettingDefinitions
{
[VisualStudioContribution]
internal static SettingCategory MarkdownLinterCategory { get; } = new("markdownLinter", "%MarkdownLinter.Settings.Category.DisplayName%")
{
Description = "%MarkdownLinter.Settings.Category.Description%",
GenerateObserverClass = true,
};

[VisualStudioContribution]
internal static Setting.String DisabledRules { get; } = new("disabledRules", "%MarkdownLinter.Settings.DisabledRules.DisplayName%", MarkdownLinterCategory, defaultValue: string.Empty)
{
Description = "%MarkdownLinter.Settings.DisabledRules.Description%",
};
}
Loading