提交 4f1a0cf0 编写于 作者: C Cyrus Najmabadi

Initial TODO work

上级 289aae87
......@@ -24,41 +24,6 @@ public TodoCommentTokens()
{
}
private ImmutableArray<TodoCommentDescriptor> Parse(string data)
{
if (string.IsNullOrWhiteSpace(data))
{
return ImmutableArray<TodoCommentDescriptor>.Empty;
}
var tuples = data.Split('|');
var result = new List<TodoCommentDescriptor>(tuples.Length);
foreach (var tuple in tuples)
{
if (string.IsNullOrWhiteSpace(tuple))
{
continue;
}
var pair = tuple.Split(':');
if (pair.Length != 2 || string.IsNullOrWhiteSpace(pair[0]))
{
continue;
}
if (!int.TryParse(pair[1], NumberStyles.None, CultureInfo.InvariantCulture, out var priority))
{
continue;
}
result.Add(new TodoCommentDescriptor(pair[0].Trim(), priority));
}
return result.ToImmutableArray();
}
private class TokenInfo
{
internal readonly string OptionText;
......
......@@ -3,11 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.TodoComments;
using Roslyn.Utilities;
......@@ -22,7 +23,7 @@ public CSharpTodoCommentServiceFactory()
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
=> new CSharpTodoCommentService(languageServices.WorkspaceServices.Workspace);
=> new CSharpTodoCommentService();
}
internal class CSharpTodoCommentService : AbstractTodoCommentService
......@@ -30,11 +31,10 @@ internal class CSharpTodoCommentService : AbstractTodoCommentService
private static readonly int s_multilineCommentPostfixLength = "*/".Length;
private const string SingleLineCommentPrefix = "//";
public CSharpTodoCommentService(Workspace workspace) : base(workspace)
{
}
protected override void AppendTodoComments(IList<TodoCommentDescriptor> commentDescriptors, SyntacticDocument document, SyntaxTrivia trivia, List<TodoComment> todoList)
protected override void AppendTodoComments(
ImmutableArray<TodoCommentDescriptor> commentDescriptors,
SyntacticDocument document, SyntaxTrivia trivia,
ArrayBuilder<TodoComment> todoList)
{
if (PreprocessorHasComment(trivia))
{
......@@ -43,7 +43,7 @@ protected override void AppendTodoComments(IList<TodoCommentDescriptor> commentD
var index = message.IndexOf(SingleLineCommentPrefix, StringComparison.Ordinal);
var start = trivia.FullSpan.Start + index;
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, message.Substring(index), start, todoList);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, message.Substring(index), start, todoList);
return;
}
......@@ -63,14 +63,10 @@ protected override void AppendTodoComments(IList<TodoCommentDescriptor> commentD
}
protected override string GetNormalizedText(string message)
{
return message;
}
=> message;
protected override bool IsIdentifierCharacter(char ch)
{
return SyntaxFacts.IsIdentifierPartCharacter(ch);
}
=> SyntaxFacts.IsIdentifierPartCharacter(ch);
protected override int GetCommentStartingIndex(string message)
{
......@@ -94,13 +90,9 @@ protected override bool PreprocessorHasComment(SyntaxTrivia trivia)
}
protected override bool IsSingleLineComment(SyntaxTrivia trivia)
{
return trivia.IsSingleLineComment() || trivia.IsSingleLineDocComment();
}
=> trivia.IsSingleLineComment() || trivia.IsSingleLineDocComment();
protected override bool IsMultilineComment(SyntaxTrivia trivia)
{
return trivia.IsMultiLineComment() || trivia.IsMultiLineDocComment();
}
=> trivia.IsMultiLineComment() || trivia.IsMultiLineDocComment();
}
}
......@@ -34,7 +34,7 @@ public virtual Task DocumentResetAsync(Document document, CancellationToken canc
return Task.CompletedTask;
}
public bool NeedsReanalysisOnOptionChanged(object sender, OptionChangedEventArgs e)
public virtual bool NeedsReanalysisOnOptionChanged(object sender, OptionChangedEventArgs e)
{
return false;
}
......
......@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
......@@ -15,17 +17,6 @@ namespace Microsoft.CodeAnalysis.TodoComments
{
internal abstract class AbstractTodoCommentService : ITodoCommentService
{
// we hold onto workspace to make sure given input (Document) belong to right workspace.
// since remote host is from workspace service, different workspace can have different expectation
// on remote host, so we need to make sure given input always belong to right workspace where
// the session belong to.
private readonly Workspace _workspace;
protected AbstractTodoCommentService(Workspace workspace)
{
_workspace = workspace;
}
protected abstract bool PreprocessorHasComment(SyntaxTrivia trivia);
protected abstract bool IsSingleLineComment(SyntaxTrivia trivia);
protected abstract bool IsMultilineComment(SyntaxTrivia trivia);
......@@ -33,42 +24,12 @@ protected AbstractTodoCommentService(Workspace workspace)
protected abstract string GetNormalizedText(string message);
protected abstract int GetCommentStartingIndex(string message);
protected abstract void AppendTodoComments(IList<TodoCommentDescriptor> commentDescriptors, SyntacticDocument document, SyntaxTrivia trivia, List<TodoComment> todoList);
public async Task<IList<TodoComment>> GetTodoCommentsAsync(Document document, IList<TodoCommentDescriptor> commentDescriptors, CancellationToken cancellationToken)
{
// make sure given input is right one
Contract.ThrowIfFalse(_workspace == document.Project.Solution.Workspace);
// run todo scanner on remote host.
// we only run closed files to make open document to have better responsiveness.
// also we cache everything related to open files anyway, no saving by running
// them in remote host
if (!document.IsOpen())
{
var client = await RemoteHostClient.TryGetClientAsync(document.Project, cancellationToken).ConfigureAwait(false);
if (client != null)
{
var result = await client.TryRunRemoteAsync<IList<TodoComment>>(
WellKnownServiceHubServices.CodeAnalysisService,
nameof(IRemoteTodoCommentService.GetTodoCommentsAsync),
document.Project.Solution,
new object[] { document.Id, commentDescriptors },
callbackTarget: null,
cancellationToken).ConfigureAwait(false);
if (result.HasValue)
{
return result.Value;
}
}
}
return await GetTodoCommentsInCurrentProcessAsync(document, commentDescriptors, cancellationToken).ConfigureAwait(false);
}
protected abstract void AppendTodoComments(ImmutableArray<TodoCommentDescriptor> commentDescriptors, SyntacticDocument document, SyntaxTrivia trivia, ArrayBuilder<TodoComment> todoList);
private async Task<IList<TodoComment>> GetTodoCommentsInCurrentProcessAsync(
Document document, IList<TodoCommentDescriptor> commentDescriptors, CancellationToken cancellationToken)
public async Task<ImmutableArray<TodoComment>> GetTodoCommentsAsync(
Document document,
ImmutableArray<TodoCommentDescriptor> commentDescriptors,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
......@@ -76,29 +37,28 @@ public async Task<IList<TodoComment>> GetTodoCommentsAsync(Document document, IL
var syntaxDoc = await SyntacticDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);
// reuse list
var todoList = new List<TodoComment>();
using var _ = ArrayBuilder<TodoComment>.GetInstance(out var todoList);
foreach (var trivia in syntaxDoc.Root.DescendantTrivia())
{
cancellationToken.ThrowIfCancellationRequested();
if (!ContainsComments(trivia))
{
continue;
}
AppendTodoComments(commentDescriptors, syntaxDoc, trivia, todoList);
}
return todoList;
return todoList.ToImmutable();
}
private bool ContainsComments(SyntaxTrivia trivia)
{
return PreprocessorHasComment(trivia) || IsSingleLineComment(trivia) || IsMultilineComment(trivia);
}
=> PreprocessorHasComment(trivia) || IsSingleLineComment(trivia) || IsMultilineComment(trivia);
protected void AppendTodoCommentInfoFromSingleLine(IList<TodoCommentDescriptor> commentDescriptors, SyntacticDocument document, string message, int start, List<TodoComment> todoList)
protected void AppendTodoCommentInfoFromSingleLine(
ImmutableArray<TodoCommentDescriptor> commentDescriptors,
string message, int start,
ArrayBuilder<TodoComment> todoList)
{
var index = GetCommentStartingIndex(message);
if (index >= message.Length)
......@@ -130,7 +90,11 @@ protected void AppendTodoCommentInfoFromSingleLine(IList<TodoCommentDescriptor>
}
}
protected void ProcessMultilineComment(IList<TodoCommentDescriptor> commentDescriptors, SyntacticDocument document, SyntaxTrivia trivia, int postfixLength, List<TodoComment> todoList)
protected void ProcessMultilineComment(
ImmutableArray<TodoCommentDescriptor> commentDescriptors,
SyntacticDocument document,
SyntaxTrivia trivia, int postfixLength,
ArrayBuilder<TodoComment> todoList)
{
// this is okay since we know it is already alive
var text = document.Text;
......@@ -145,20 +109,20 @@ protected void ProcessMultilineComment(IList<TodoCommentDescriptor> commentDescr
if (startLine.LineNumber == endLine.LineNumber)
{
var message = postfixLength == 0 ? fullString : fullString.Substring(0, fullSpan.Length - postfixLength);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, message, fullSpan.Start, todoList);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, message, fullSpan.Start, todoList);
return;
}
// multiline
var startMessage = text.ToString(TextSpan.FromBounds(fullSpan.Start, startLine.End));
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, startMessage, fullSpan.Start, todoList);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, startMessage, fullSpan.Start, todoList);
for (var lineNumber = startLine.LineNumber + 1; lineNumber < endLine.LineNumber; lineNumber++)
{
var line = text.Lines[lineNumber];
var message = line.ToString();
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, message, line.Start, todoList);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, message, line.Start, todoList);
}
var length = fullSpan.End - endLine.Start;
......@@ -168,7 +132,7 @@ protected void ProcessMultilineComment(IList<TodoCommentDescriptor> commentDescr
}
var endMessage = text.ToString(new TextSpan(endLine.Start, length));
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, endMessage, endLine.Start, todoList);
AppendTodoCommentInfoFromSingleLine(commentDescriptors, endMessage, endLine.Start, todoList);
}
}
}
......@@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
namespace Microsoft.CodeAnalysis.TodoComments
{
......@@ -22,6 +24,33 @@ public TodoCommentDescriptor(string text, int priority) : this()
Text = text;
Priority = priority;
}
public static ImmutableArray<TodoCommentDescriptor> Parse(string data)
{
if (string.IsNullOrWhiteSpace(data))
return ImmutableArray<TodoCommentDescriptor>.Empty;
var tuples = data.Split('|');
var result = ArrayBuilder<TodoCommentDescriptor>.GetInstance();
foreach (var tuple in tuples)
{
if (string.IsNullOrWhiteSpace(tuple))
continue;
var pair = tuple.Split(':');
if (pair.Length != 2 || string.IsNullOrWhiteSpace(pair[0]))
continue;
if (!int.TryParse(pair[1], NumberStyles.None, CultureInfo.InvariantCulture, out var priority))
continue;
result.Add(new TodoCommentDescriptor(pair[0].Trim(), priority));
}
return result.ToImmutableAndFree();
}
}
/// <summary>
......@@ -43,6 +72,6 @@ public TodoComment(TodoCommentDescriptor descriptor, string message, int positio
internal interface ITodoCommentService : ILanguageService
{
Task<IList<TodoComment>> GetTodoCommentsAsync(Document document, IList<TodoCommentDescriptor> commentDescriptors, CancellationToken cancellationToken);
Task<ImmutableArray<TodoComment>> GetTodoCommentsAsync(Document document, ImmutableArray<TodoCommentDescriptor> commentDescriptors, CancellationToken cancellationToken);
}
}
......@@ -7,6 +7,7 @@ Imports System.Composition
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Host
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.TodoComments
Namespace Microsoft.CodeAnalysis.VisualBasic.TodoComments
......@@ -19,7 +20,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.TodoComments
End Sub
Public Function CreateLanguageService(languageServices As HostLanguageServices) As ILanguageService Implements ILanguageServiceFactory.CreateLanguageService
Return New VisualBasicTodoCommentService(languageServices.WorkspaceServices.Workspace)
Return New VisualBasicTodoCommentService()
End Function
End Class
......@@ -27,15 +28,15 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.TodoComments
Friend Class VisualBasicTodoCommentService
Inherits AbstractTodoCommentService
Public Sub New(workspace As Workspace)
MyBase.New(workspace)
End Sub
Protected Overrides Sub AppendTodoComments(commentDescriptors As IList(Of TodoCommentDescriptor), document As SyntacticDocument, trivia As SyntaxTrivia, todoList As List(Of TodoComment))
Protected Overrides Sub AppendTodoComments(
commentDescriptors As ImmutableArray(Of TodoCommentDescriptor),
document As SyntacticDocument,
trivia As SyntaxTrivia,
todoList As ArrayBuilder(Of TodoComment))
If PreprocessorHasComment(trivia) Then
Dim commentTrivia = trivia.GetStructure().DescendantTrivia().First(Function(t) t.RawKind = SyntaxKind.CommentTrivia)
AppendTodoCommentInfoFromSingleLine(commentDescriptors, document, commentTrivia.ToFullString(), commentTrivia.FullSpan.Start, todoList)
AppendTodoCommentInfoFromSingleLine(commentDescriptors, commentTrivia.ToFullString(), commentTrivia.FullSpan.Start, todoList)
Return
End If
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Threading;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TodoComments
{
/// <summary>
/// In process service responsible for listening to OOP todo comment notifications.
/// </summary>
internal interface ITodoCommentsService : IWorkspaceService
{
/// <summary>
/// Called by a host to let this service know that it should start background
/// analysis of the workspace to find todo comments
/// </summary>
void Start(CancellationToken cancellationToken);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ProjectTelemetry;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.Internal.VisualStudio.Shell;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TodoComments
{
internal class VisualStudioTodoCommentsService
: ForegroundThreadAffinitizedObject, ITodoCommentsService, IProjectTelemetryServiceCallback
{
private const string EventPrefix = "VS/Compilers/Compilation/";
private const string PropertyPrefix = "VS.Compilers.Compilation.Inputs.";
private const string TelemetryEventPath = EventPrefix + "Inputs";
private const string TelemetryExceptionEventPath = EventPrefix + "TelemetryUnhandledException";
private const string TelemetryProjectIdName = PropertyPrefix + "ProjectId";
private const string TelemetryProjectGuidName = PropertyPrefix + "ProjectGuid";
private const string TelemetryLanguageName = PropertyPrefix + "Language";
private const string TelemetryAnalyzerReferencesCountName = PropertyPrefix + "AnalyzerReferences.Count";
private const string TelemetryProjectReferencesCountName = PropertyPrefix + "ProjectReferences.Count";
private const string TelemetryMetadataReferencesCountName = PropertyPrefix + "MetadataReferences.Count";
private const string TelemetryDocumentsCountName = PropertyPrefix + "Documents.Count";
private const string TelemetryAdditionalDocumentsCountName = PropertyPrefix + "AdditionalDocuments.Count";
private readonly VisualStudioWorkspaceImpl _workspace;
/// <summary>
/// Our connections to the remote OOP server. Created on demand when we startup and then
/// kept around for the lifetime of this service.
/// </summary>
private KeepAliveSession? _keepAliveSession;
/// <summary>
/// Queue where we enqueue the information we get from OOP to process in batch in the future.
/// </summary>
private AsyncBatchingWorkQueue<ProjectTelemetryInfo> _workQueue = null!;
public VisualStudioTodoCommentsService(VisualStudioWorkspaceImpl workspace, IThreadingContext threadingContext) : base(threadingContext)
=> _workspace = workspace;
void ITodoCommentsService.Start(CancellationToken cancellationToken)
=> _ = StartAsync(cancellationToken);
private async Task StartAsync(CancellationToken cancellationToken)
{
// Have to catch all exceptions coming through here as this is called from a
// fire-and-forget method and we want to make sure nothing leaks out.
try
{
await StartWorkerAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Cancellation is normal (during VS closing). Just ignore.
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
{
// Otherwise report a watson for any other exception. Don't bring down VS. This is
// a BG service we don't want impacting the user experience.
}
}
private async Task StartWorkerAsync(CancellationToken cancellationToken)
{
_workQueue = new AsyncBatchingWorkQueue<ProjectTelemetryInfo>(
TimeSpan.FromSeconds(1),
NotifyTelemetryServiceAsync,
cancellationToken);
var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false);
if (client == null)
return;
// Pass ourselves in as the callback target for the OOP service. As it discovers
// designer attributes it will call back into us to notify VS about it.
_keepAliveSession = await client.TryCreateKeepAliveSessionAsync(
WellKnownServiceHubServices.RemoteTodoCommentsService,
callbackTarget: this, cancellationToken).ConfigureAwait(false);
if (_keepAliveSession == null)
return;
// Now kick off scanning in the OOP process.
var success = await _keepAliveSession.TryInvokeAsync(
nameof(IRemoteTodoCommentsService.ComputeTodoCommentsAsync),
solution: null,
arguments: Array.Empty<object>(),
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Callback from the OOP service back into us.
/// </summary>
public Task RegisterProjectTelemetryInfoAsync(ProjectTelemetryInfo info, CancellationToken cancellationToken)
{
_workQueue.AddWork(info);
return Task.CompletedTask;
}
private async Task NotifyTelemetryServiceAsync(
ImmutableArray<ProjectTelemetryInfo> infos, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using var _1 = ArrayBuilder<ProjectTelemetryInfo>.GetInstance(out var filteredInfos);
AddFilteredInfos(infos, filteredInfos);
using var _2 = ArrayBuilder<Task>.GetInstance(out var tasks);
foreach (var info in filteredInfos)
tasks.Add(Task.Run(() => NotifyTelemetryService(info), cancellationToken));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
private void AddFilteredInfos(ImmutableArray<ProjectTelemetryInfo> infos, ArrayBuilder<ProjectTelemetryInfo> filteredInfos)
{
using var _ = PooledHashSet<ProjectId>.GetInstance(out var seenProjectIds);
// Walk the list of telemetry items in reverse, and skip any items for a project once
// we've already seen it once. That way, we're only reporting the most up to date
// information for a project, and we're skipping the stale information.
for (var i = infos.Length - 1; i >= 0; i--)
{
var info = infos[i];
if (seenProjectIds.Add(info.ProjectId))
filteredInfos.Add(info);
}
}
private void NotifyTelemetryService(ProjectTelemetryInfo info)
{
try
{
var telemetryEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryEventPath);
telemetryEvent.SetStringProperty(TelemetryProjectIdName, info.ProjectId.Id.ToString());
telemetryEvent.SetStringProperty(TelemetryProjectGuidName, Guid.Empty.ToString());
telemetryEvent.SetStringProperty(TelemetryLanguageName, info.Language);
telemetryEvent.SetIntProperty(TelemetryAnalyzerReferencesCountName, info.AnalyzerReferencesCount);
telemetryEvent.SetIntProperty(TelemetryProjectReferencesCountName, info.ProjectReferencesCount);
telemetryEvent.SetIntProperty(TelemetryMetadataReferencesCountName, info.MetadataReferencesCount);
telemetryEvent.SetIntProperty(TelemetryDocumentsCountName, info.DocumentsCount);
telemetryEvent.SetIntProperty(TelemetryAdditionalDocumentsCountName, info.AdditionalDocumentsCount);
TelemetryHelper.DefaultTelemetrySession.PostEvent(telemetryEvent);
}
catch (Exception e)
{
// The telemetry service itself can throw.
// So, to be very careful, put this in a try/catch too.
try
{
var exceptionEvent = TelemetryHelper.TelemetryService.CreateEvent(TelemetryExceptionEventPath);
exceptionEvent.SetStringProperty("Type", e.GetTypeDisplayName());
exceptionEvent.SetStringProperty("Message", e.Message);
exceptionEvent.SetStringProperty("StackTrace", e.StackTrace);
TelemetryHelper.DefaultTelemetrySession.PostEvent(exceptionEvent);
}
catch
{
}
}
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Composition;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TodoComments
{
[ExportWorkspaceServiceFactory(typeof(ITodoCommentsService), ServiceLayer.Host), Shared]
internal class VisualStudioTodoCommentsServiceFactory : IWorkspaceServiceFactory
{
private readonly IThreadingContext _threadingContext;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public VisualStudioTodoCommentsServiceFactory(IThreadingContext threadingContext)
=> _threadingContext = threadingContext;
public IWorkspaceService? CreateService(HostWorkspaceServices workspaceServices)
{
if (!(workspaceServices.Workspace is VisualStudioWorkspaceImpl workspace))
return null;
return new VisualStudioTodoCommentsService(workspace, _threadingContext);
}
}
}
......@@ -29,6 +29,7 @@
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectTelemetry;
using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource;
using Microsoft.VisualStudio.LanguageServices.Implementation.TodoComments;
using Microsoft.VisualStudio.LanguageServices.Telemetry;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
......@@ -164,6 +165,10 @@ private async Task LoadComponentsBackgroundAsync(CancellationToken cancellationT
// Load the telemetry service and tell it to start watching the solution for project info.
var projectTelemetryService = _workspace.Services.GetRequiredService<IProjectTelemetryService>();
projectTelemetryService.Start(this.DisposalToken);
// Load the todo comments service and tell it to start watching the solution for new comments
var todoCommentsService = _workspace.Services.GetRequiredService<ITodoCommentsService>();
todoCommentsService.Start(this.DisposalToken);
}
private async Task LoadInteractiveMenusAsync(CancellationToken cancellationToken)
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.CodeAnalysis.ProjectTelemetry
{
/// <summary>
/// Interface to allow host (VS) to inform the OOP service to start incrementally analyzing and
/// reporting results back to the host.
/// </summary>
internal interface IRemoteTodoCommentsService
{
Task ComputeTodoCommentsAsync(CancellationToken cancellation);
}
}
......@@ -2,18 +2,20 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Remote;
namespace Microsoft.CodeAnalysis.TodoComments
{
/// <summary>
/// interface exist to strongly type todo comment remote service
/// Callback the host (VS) passes to the OOP service to allow it to send batch notifications
/// about todo comments.
/// </summary>
internal interface IRemoteTodoCommentService
internal interface ITodoCommentsServiceCallback
{
Task<IList<TodoComment>> GetTodoCommentsAsync(PinnedSolutionInfo solutionInfo, DocumentId documentId, IList<TodoCommentDescriptor> commentDescriptors, CancellationToken cancellationToken);
Task ReportTodoCommentsAsync(List<TodoCommentInfo> infos, CancellationToken cancellationToken);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.TodoComments
{
/// <summary>
/// Serialization typed used to pass information to/from OOP and VS.
/// </summary>
internal struct TodoCommentInfo
{
public int Priority;
public string Message;
public DocumentId DocumentId;
public string? MappedFilePath;
public string? OriginalFilePath;
public int MappedLine;
public int MappedColumn;
public int OriginalLine;
public int OriginalColumn;
public override bool Equals(object? obj)
=> obj is TodoCommentInfo other && Equals(other);
public override int GetHashCode()
=> GetHashCode(this);
public override string ToString()
=> $"{Priority} {Message} {MappedFilePath ?? ""} ({MappedLine.ToString()}, {MappedColumn.ToString()}) [original: {OriginalFilePath ?? ""} ({OriginalLine.ToString()}, {OriginalColumn.ToString()})";
public bool Equals(TodoCommentInfo right)
{
return DocumentId == right.DocumentId &&
Priority == right.Priority &&
Message == right.Message &&
OriginalLine == right.OriginalLine &&
OriginalColumn == right.OriginalColumn;
}
public static int GetHashCode(TodoCommentInfo item)
=> Hash.Combine(item.DocumentId,
Hash.Combine(item.Priority,
Hash.Combine(item.Message,
Hash.Combine(item.OriginalLine,
Hash.Combine(item.OriginalColumn, 0)))));
internal void WriteTo(ObjectWriter writer)
{
writer.WriteInt32(Priority);
writer.WriteString(Message);
DocumentId.WriteTo(writer);
writer.WriteString(MappedFilePath);
writer.WriteString(OriginalFilePath);
writer.WriteInt32(MappedLine);
writer.WriteInt32(MappedColumn);
writer.WriteInt32(OriginalLine);
writer.WriteInt32(OriginalColumn);
}
internal static TodoCommentInfo ReadFrom(ObjectReader reader)
{
return new TodoCommentInfo
{
Priority = reader.ReadInt32(),
Message = reader.ReadString(),
DocumentId = DocumentId.ReadFrom(reader),
MappedFilePath = reader.ReadString(),
OriginalFilePath = reader.ReadString(),
MappedLine = reader.ReadInt32(),
MappedColumn = reader.ReadInt32(),
OriginalLine = reader.ReadInt32(),
OriginalColumn = reader.ReadInt32(),
};
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.TodoComments;
namespace Microsoft.CodeAnalysis.Remote.Services.TodoComments
{
internal class DescriptorInfo
{
public readonly string OptionText;
public readonly ImmutableArray<TodoCommentDescriptor> Descriptors;
public DescriptorInfo(string optionText, ImmutableArray<TodoCommentDescriptor> descriptors)
{
this.OptionText = optionText;
this.Descriptors = descriptors;
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media.TextFormatting;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Implementation.TodoComments;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ProjectTelemetry;
using Microsoft.CodeAnalysis.Remote.Services.TodoComments;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.TodoComments;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Remote
{
internal partial class RemoteTodoCommentsIncrementalAnalyzer : IncrementalAnalyzerBase
{
private const string DataKey = "TodoComments";
/// <summary>
/// Channel back to VS to inform it of the designer attributes we discover.
/// </summary>
private readonly RemoteEndPoint _endPoint;
private readonly IPersistentStorageService _storageService;
private readonly object _gate = new object();
private DescriptorInfo? _lastDescriptorInfo;
public RemoteTodoCommentsIncrementalAnalyzer(Workspace workspace, RemoteEndPoint endPoint)
{
_endPoint = endPoint;
_storageService = workspace.Services.GetRequiredService<IPersistentStorageService>();
}
public override bool NeedsReanalysisOnOptionChanged(object sender, OptionChangedEventArgs e)
=> e.Option == TodoCommentOptions.TokenList;
private DescriptorInfo GetDescriptorInfo(Document document)
{
var optionText = document.Project.Solution.Options.GetOption(TodoCommentOptions.TokenList);
lock (_gate)
{
if (_lastDescriptorInfo == null || _lastDescriptorInfo.OptionText != optionText)
_lastDescriptorInfo = new DescriptorInfo(optionText, TodoCommentDescriptor.Parse(optionText));
return _lastDescriptorInfo;
}
}
public override async Task AnalyzeSyntaxAsync(Document document, InvocationReasons reasons, CancellationToken cancellationToken)
{
var todoCommentService = document.GetLanguageService<ITodoCommentService>();
if (todoCommentService == null)
return;
using var storage = _storageService.GetStorage(document.Project.Solution);
var version = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false);
var descriptorInfo = GetDescriptorInfo(document);
var persistedInfo = await TryReadExistingCommentInfoAsync(
storage, document, cancellationToken).ConfigureAwait(false);
if (persistedInfo != null &&
persistedInfo.Version == version &&
persistedInfo.OptionText == descriptorInfo.OptionText)
{
// Our info for this file is up to date.
return;
}
// We're out of date. Recompute this info.
var todoComments = await todoCommentService.GetTodoCommentsAsync(
document, descriptorInfo.Descriptors, cancellationToken).ConfigureAwait(false);
// Convert the roslyn-level results to the more VS oriented line/col data.
using var _ = ArrayBuilder<TodoCommentInfo>.GetInstance(out var converted);
await ConvertAsync(
document, todoComments, converted, cancellationToken).ConfigureAwait(false);
// Now inform VS about this new information
await _endPoint.InvokeAsync(
nameof(ITodoCommentsServiceCallback.ReportTodoCommentsAsync),
new object[] { converted },
cancellationToken).ConfigureAwait(false);
persistedInfo = new PersistedTodoCommentInfo
{
Version = version,
OptionText = descriptorInfo.OptionText,
TodoComments = converted.ToImmutable(),
};
// now that we've informed VS, save this information for the future.
await PersistTodoCommentsAsync(
storage, document, persistedInfo, cancellationToken).ConfigureAwait(false);
}
private async Task ConvertAsync(
Document document,
ImmutableArray<TodoComment> todoComments,
ArrayBuilder<TodoCommentInfo> converted,
CancellationToken cancellationToken)
{
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
foreach (var comment in todoComments)
converted.Add(Convert(document, sourceText, syntaxTree, comment));
}
private TodoCommentInfo Convert(
Document document, SourceText text, SyntaxTree tree, TodoComment comment)
{
// make sure given position is within valid text range.
var textSpan = new TextSpan(Math.Min(text.Length, Math.Max(0, comment.Position)), 0);
var location = tree.GetLocation(textSpan);
// var location = tree == null ? Location.Create(document.FilePath, textSpan, text.Lines.GetLinePositionSpan(textSpan)) : tree.GetLocation(textSpan);
var originalLineInfo = location.GetLineSpan();
var mappedLineInfo = location.GetMappedLineSpan();
return new TodoCommentInfo
{
Priority = comment.Descriptor.Priority,
Message = comment.Message,
DocumentId = document.Id,
OriginalLine = originalLineInfo.StartLinePosition.Line,
OriginalColumn = originalLineInfo.StartLinePosition.Character,
OriginalFilePath = document.FilePath,
MappedLine = mappedLineInfo.StartLinePosition.Line,
MappedColumn = mappedLineInfo.StartLinePosition.Character,
MappedFilePath = mappedLineInfo.GetMappedFilePathIfExist(),
};
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using Microsoft.CodeAnalysis.SolutionCrawler;
namespace Microsoft.CodeAnalysis.Remote
{
/// <remarks>Note: this is explicitly <b>not</b> exported. We don't want the <see
/// cref="RemoteWorkspace"/> to automatically load this. Instead, VS waits until it is ready
/// and then calls into OOP to tell it to start analyzing the solution. At that point we'll get
/// created and added to the solution crawler.
/// </remarks>
internal class RemoteTodoCommentsIncrementalAnalyzerProvider : IIncrementalAnalyzerProvider
{
private readonly RemoteEndPoint _endPoint;
public RemoteTodoCommentsIncrementalAnalyzerProvider(RemoteEndPoint endPoint)
{
_endPoint = endPoint;
}
public IIncrementalAnalyzer CreateIncrementalAnalyzer(Workspace workspace)
=> new RemoteTodoCommentsIncrementalAnalyzer(_endPoint);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.TodoComments;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Remote
{
internal partial class RemoteTodoCommentsIncrementalAnalyzer
{
private const string SerializationFormat = "1";
private async Task<PersistedTodoCommentInfo?> TryReadExistingCommentInfoAsync(
IPersistentStorage storage, Document document, CancellationToken cancellationToken)
{
using var stream = await storage.ReadStreamAsync(document, DataKey, cancellationToken).ConfigureAwait(false);
using var reader = ObjectReader.TryGetReader(stream, cancellationToken: cancellationToken);
return TryReadPersistedInfo(reader);
}
private async Task PersistTodoCommentsAsync(
IPersistentStorage storage,
Document document,
PersistedTodoCommentInfo info,
CancellationToken cancellationToken)
{
using var memoryStream = new MemoryStream();
using var writer = new ObjectWriter(memoryStream);
writer.WriteString(SerializationFormat);
info.Version.WriteTo(writer);
writer.WriteString(info.OptionText);
writer.WriteInt32(info.TodoComments.Length);
foreach (var comment in info.TodoComments)
comment.WriteTo(writer);
memoryStream.Position = 0;
await storage.WriteStreamAsync(
document, DataKey, memoryStream, cancellationToken).ConfigureAwait(false);
}
private static PersistedTodoCommentInfo? TryReadPersistedInfo(ObjectReader reader)
{
if (reader == null)
return null;
try
{
var serializationFormat = reader.ReadString();
if (serializationFormat != SerializationFormat)
return null;
var version = VersionStamp.ReadFrom(reader);
var optionText = reader.ReadString();
var count = reader.ReadInt32();
using var _ = ArrayBuilder<TodoCommentInfo>.GetInstance(out var comments);
for (int i = 0; i < count; i++)
comments.Add(TodoCommentInfo.ReadFrom(reader));
return new PersistedTodoCommentInfo
{
Version = version,
OptionText = optionText,
TodoComments = comments.ToImmutable(),
};
}
catch
{
}
return null;
}
private class PersistedTodoCommentInfo
{
public VersionStamp Version;
public string? OptionText;
public ImmutableArray<TodoCommentInfo> TodoComments;
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ProjectTelemetry;
using Microsoft.CodeAnalysis.SolutionCrawler;
namespace Microsoft.CodeAnalysis.Remote
{
internal partial class RemoteTodoCommentsService : ServiceBase, IRemoteTodoCommentsService
{
public RemoteTodoCommentsService(
Stream stream, IServiceProvider serviceProvider)
: base(serviceProvider, stream)
{
StartService();
}
public Task ComputeTodoCommentsAsync(CancellationToken cancellation)
{
return RunServiceAsync(() =>
{
var workspace = SolutionService.PrimaryWorkspace;
var endpoint = this.EndPoint;
var registrationService = workspace.Services.GetRequiredService<ISolutionCrawlerRegistrationService>();
var analyzerProvider = new RemoteTodoCommentsIncrementalAnalyzerProvider(endpoint);
registrationService.AddAnalyzerProvider(
analyzerProvider,
new IncrementalAnalyzerProviderMetadata(
nameof(RemoteTodoCommentsIncrementalAnalyzerProvider),
highPriorityForActiveFile: false,
workspaceKinds: WorkspaceKind.RemoteWorkspace));
return Task.CompletedTask;
}, cancellation);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册