From 5eb8df75990854b3aab38d37b5210a4fb4f3ab5b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 10 Jun 2021 20:21:42 -0700 Subject: [PATCH 1/7] Pull diagnostics should just request from the doc, not the whole project. --- .../Handler/Diagnostics/DocumentPullDiagnosticHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index fb96d08a549a7..0c7b1b185f0be 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics @@ -76,14 +77,16 @@ protected override ImmutableArray GetOrderedDocuments(RequestContext c return ImmutableArray.Create(context.Document); } - protected override Task> GetDiagnosticsAsync( + protected override async Task> GetDiagnosticsAsync( RequestContext context, Document document, Option2 diagnosticMode, CancellationToken cancellationToken) { // For open documents, directly use the IDiagnosticAnalyzerService. This will use the actual snapshots // we're passing in. If information is already cached for that snapshot, it will be returned. Otherwise, // it will be computed on demand. Because it is always accurate as per this snapshot, all spans are correct // and do not need to be adjusted. - return _analyzerService.GetDiagnosticsAsync(document.Project.Solution, documentId: document.Id, cancellationToken: cancellationToken); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + return await _analyzerService.GetDiagnosticsForSpanAsync( + document, new TextSpan(0, text.Length), cancellationToken: cancellationToken).ConfigureAwait(false); } } } From 0cc02a9de52420dfef3304278db9259b9f960dfa Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 11 Jun 2021 10:12:56 -0700 Subject: [PATCH 2/7] Just pass null --- .../Core/Portable/CodeFixes/CodeFixService.cs | 2 +- .../Diagnostics/DiagnosticAnalyzerService.cs | 3 ++- ...ncrementalAnalyzer_GetDiagnosticsForSpan.cs | 18 +++++++++++------- .../Diagnostics/IDiagnosticAnalyzerService.cs | 12 ++++++++---- .../DocumentPullDiagnosticHandler.cs | 3 +-- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs index 2fdc0f633c149..06891f6587da5 100644 --- a/src/Features/Core/Portable/CodeFixes/CodeFixService.cs +++ b/src/Features/Core/Portable/CodeFixes/CodeFixService.cs @@ -164,7 +164,7 @@ public async Task> GetFixesAsync(Document docu // invariant: later code gathers & runs CodeFixProviders for diagnostics with one identical diagnostics span (that gets set later as CodeFixCollection's TextSpan) // order diagnostics by span. SortedDictionary>? aggregatedDiagnostics = null; - foreach (var diagnostic in await _diagnosticService.GetDiagnosticsForSpanAsync(document, range, diagnosticIdOpt: null, includeConfigurationFixes, addOperationScope, cancellationToken).ConfigureAwait(false)) + foreach (var diagnostic in await _diagnosticService.GetDiagnosticsForSpanAsync(document, range, diagnosticId: null, includeConfigurationFixes, addOperationScope, cancellationToken).ConfigureAwait(false)) { if (diagnostic.IsSuppressed) { diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService.cs index 3e18d4b153625..07f47402c8b64 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService.cs @@ -75,7 +75,8 @@ public Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan r return SpecializedTasks.False; } - public Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, string? diagnosticId = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default) + public Task> GetDiagnosticsForSpanAsync( + Document document, TextSpan? range, string? diagnosticId = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default) { if (_map.TryGetValue(document.Project.Solution.Workspace, out var analyzer)) { diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index 399553f0d052a..d32bdd624a2cc 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -19,13 +19,15 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { internal partial class DiagnosticIncrementalAnalyzer { - public async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, ArrayBuilder result, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) + public async Task TryAppendDiagnosticsForSpanAsync( + Document document, TextSpan? range, ArrayBuilder result, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) { var getter = await LatestDiagnosticsForSpanGetter.CreateAsync(this, document, range, blockForData, addOperationScope, includeSuppressedDiagnostics, diagnosticId, cancellationToken).ConfigureAwait(false); return await getter.TryGetAsync(result, cancellationToken).ConfigureAwait(false); } - public async Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) + public async Task> GetDiagnosticsForSpanAsync( + Document document, TextSpan? range, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) { using var _ = ArrayBuilder.GetInstance(out var list); var result = await TryAppendDiagnosticsForSpanAsync(document, range, list, diagnosticId, includeSuppressedDiagnostics, blockForData, addOperationScope, cancellationToken).ConfigureAwait(false); @@ -44,7 +46,7 @@ private sealed class LatestDiagnosticsForSpanGetter private readonly IEnumerable _stateSets; private readonly CompilationWithAnalyzers? _compilationWithAnalyzers; - private readonly TextSpan _range; + private readonly TextSpan? _range; private readonly bool _blockForData; private readonly bool _includeSuppressedDiagnostics; private readonly string? _diagnosticId; @@ -55,7 +57,7 @@ private sealed class LatestDiagnosticsForSpanGetter public static async Task CreateAsync( DiagnosticIncrementalAnalyzer owner, Document document, - TextSpan range, + TextSpan? range, bool blockForData, Func? addOperationScope, bool includeSuppressedDiagnostics, @@ -73,7 +75,8 @@ public static async Task CreateAsync( var compilationWithAnalyzers = await CreateCompilationWithAnalyzersAsync(document.Project, stateSets, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return new LatestDiagnosticsForSpanGetter(owner, compilationWithAnalyzers, document, stateSets, diagnosticId, range, blockForData, addOperationScope, includeSuppressedDiagnostics); + return new LatestDiagnosticsForSpanGetter( + owner, compilationWithAnalyzers, document, stateSets, diagnosticId, range, blockForData, addOperationScope, includeSuppressedDiagnostics); } private LatestDiagnosticsForSpanGetter( @@ -82,7 +85,7 @@ private LatestDiagnosticsForSpanGetter( Document document, IEnumerable stateSets, string? diagnosticId, - TextSpan range, + TextSpan? range, bool blockForData, Func? addOperationScope, bool includeSuppressedDiagnostics) @@ -211,7 +214,8 @@ private async Task ComputeDocumentDiagnosticsAsync( private bool ShouldInclude(DiagnosticData diagnostic) { - return diagnostic.DocumentId == _document.Id && _range.IntersectsWith(diagnostic.GetTextSpan()) + return diagnostic.DocumentId == _document.Id && + (_range == null || _range.Value.IntersectsWith(diagnostic.GetTextSpan())) && (_includeSuppressedDiagnostics || !diagnostic.IsSuppressed) && (_diagnosticId == null || _diagnosticId == diagnostic.Id); } diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs index 7d2e2ce009854..e59fb90d3b1c9 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs @@ -72,10 +72,14 @@ internal interface IDiagnosticAnalyzerService /// /// Return up to date diagnostics for the given span for the document - /// - /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. - /// If diagnosticIdOpt is not null, it gets diagnostics only for this given diagnosticIdOpt value + /// + /// This can be expensive since it is force analyzing diagnostics if it doesn't have up-to-date one yet. If + /// is not null, it gets diagnostics only for this given value. + /// /// - Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, string? diagnosticIdOpt = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default); + /// The span of the document to get diagnostics for. If null, diagnostics for the entire + /// document should be returned. + Task> GetDiagnosticsForSpanAsync(Document document, TextSpan? range, string? diagnosticId = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index 0c7b1b185f0be..b25595db2498f 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -84,9 +84,8 @@ protected override async Task> GetDiagnosticsAsyn // we're passing in. If information is already cached for that snapshot, it will be returned. Otherwise, // it will be computed on demand. Because it is always accurate as per this snapshot, all spans are correct // and do not need to be adjusted. - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); return await _analyzerService.GetDiagnosticsForSpanAsync( - document, new TextSpan(0, text.Length), cancellationToken: cancellationToken).ConfigureAwait(false); + document, range: null, cancellationToken: cancellationToken).ConfigureAwait(false); } } } From 04741cb36772ef7aca7016cf36789c64c04f71bb Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 11 Jun 2021 10:39:12 -0700 Subject: [PATCH 3/7] Update test impls --- .../EditAndContinue/Helpers/MockDiagnosticAnalyzerService.cs | 2 +- .../Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EditorFeatures/Test/EditAndContinue/Helpers/MockDiagnosticAnalyzerService.cs b/src/EditorFeatures/Test/EditAndContinue/Helpers/MockDiagnosticAnalyzerService.cs index 31d5d58912d0e..84867188b18d0 100644 --- a/src/EditorFeatures/Test/EditAndContinue/Helpers/MockDiagnosticAnalyzerService.cs +++ b/src/EditorFeatures/Test/EditAndContinue/Helpers/MockDiagnosticAnalyzerService.cs @@ -40,7 +40,7 @@ public Task> GetDiagnosticsAsync(Solution solutio public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId = null, DocumentId? documentId = null, ImmutableHashSet? diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, string? diagnosticIdOpt = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default) + public Task> GetDiagnosticsForSpanAsync(Document document, TextSpan? range, string? diagnosticId = null, bool includeSuppressedDiagnostics = false, Func? addOperationScope = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId = null, ImmutableHashSet? diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default) diff --git a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb index 096686214cc9e..e4fcc5e1edf83 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb @@ -512,7 +512,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Public Sub Reanalyze(workspace As Workspace, Optional projectIds As IEnumerable(Of ProjectId) = Nothing, Optional documentIds As IEnumerable(Of DocumentId) = Nothing, Optional highPriority As Boolean = False) Implements IDiagnosticAnalyzerService.Reanalyze End Sub - Public Function GetDiagnosticsForSpanAsync(document As Document, range As TextSpan, Optional diagnosticId As String = Nothing, Optional includeSuppressedDiagnostics As Boolean = False, Optional addOperationScope As Func(Of String, IDisposable) = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForSpanAsync + Public Function GetDiagnosticsForSpanAsync(document As Document, range As TextSpan?, Optional diagnosticId As String = Nothing, Optional includeSuppressedDiagnostics As Boolean = False, Optional addOperationScope As Func(Of String, IDisposable) = Nothing, Optional cancellationToken As CancellationToken = Nothing) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForSpanAsync Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData) End Function From 1b5c5c380f0cf128ae7492fc8db9a2085b95f192 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 11 Jun 2021 14:00:24 -0700 Subject: [PATCH 4/7] Simplify --- .../Handler/Diagnostics/DocumentPullDiagnosticHandler.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index b25595db2498f..b200b17a2c56f 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -77,15 +77,14 @@ protected override ImmutableArray GetOrderedDocuments(RequestContext c return ImmutableArray.Create(context.Document); } - protected override async Task> GetDiagnosticsAsync( + protected override Task> GetDiagnosticsAsync( RequestContext context, Document document, Option2 diagnosticMode, CancellationToken cancellationToken) { // For open documents, directly use the IDiagnosticAnalyzerService. This will use the actual snapshots // we're passing in. If information is already cached for that snapshot, it will be returned. Otherwise, // it will be computed on demand. Because it is always accurate as per this snapshot, all spans are correct // and do not need to be adjusted. - return await _analyzerService.GetDiagnosticsForSpanAsync( - document, range: null, cancellationToken: cancellationToken).ConfigureAwait(false); + return _analyzerService.GetDiagnosticsForSpanAsync(document, range: null, cancellationToken: cancellationToken); } } } From d0ff965bc8c6c9fde792afc920cfe6adca5788ba Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 11 Jun 2021 14:16:02 -0700 Subject: [PATCH 5/7] Fix comment --- .../UnifiedSuggestions/UnifiedSuggestedActionsSource.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Features/Core/Portable/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs b/src/Features/Core/Portable/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs index 511f61851cdd7..5ae92fc11cad4 100644 --- a/src/Features/Core/Portable/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs +++ b/src/Features/Core/Portable/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs @@ -38,10 +38,8 @@ public static async ValueTask> GetFilt Func addOperationScope, CancellationToken cancellationToken) { - // It may seem strange that we kick off a task, but then immediately 'Wait' on - // it. However, it's deliberate. We want to make sure that the code runs on - // the background so that no one takes an accidentally dependency on running on - // the UI thread. + // Intentionally switch to a threadpool thread to compute fixes. We do not want to accidentally + // run any of this on the UI thread and potentially allow any code to take a dependency on that. var fixes = await Task.Run(() => codeFixService.GetFixesAsync( document, selection, includeSuppressionFixes: true, isBlocking, addOperationScope, cancellationToken), cancellationToken).ConfigureAwait(false); From ec902d60b9f499923965e0696940b13d462241f0 Mon Sep 17 00:00:00 2001 From: Allison Chou Date: Fri, 11 Jun 2021 15:19:18 -0700 Subject: [PATCH 6/7] Remove IRazorDocumentOptionsService inheritance interface (#54047) --- src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsService.cs b/src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsService.cs index 93cd5ff498f15..c79030cb79173 100644 --- a/src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsService.cs +++ b/src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsService.cs @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor { - internal interface IRazorDocumentOptionsService : IDocumentService + internal interface IRazorDocumentOptionsService { Task GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken); } From 31304a58f0a9027e8246d5ff88e40365c7e4ad96 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Fri, 11 Jun 2021 15:48:02 -0700 Subject: [PATCH 7/7] Update Language Feature Status.md (#54015) --- docs/Language Feature Status.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Language Feature Status.md b/docs/Language Feature Status.md index bf69966088de6..5a28acb0fe789 100644 --- a/docs/Language Feature Status.md +++ b/docs/Language Feature Status.md @@ -18,7 +18,7 @@ efforts behind them. | [Parameterless struct constructors](https://github.com/dotnet/csharplang/issues/99) | [struct-ctors](https://github.com/dotnet/roslyn/tree/features/struct-ctors) | [In Progress](https://github.com/dotnet/roslyn/issues/51698) | [cston](https://github.com/cston) | [jcouv](https://github.com/jcouv), [333fred](https://github.com/333fred) | [jcouv](https://github.com/jouv) | | [Lambda improvements](https://github.com/dotnet/csharplang/blob/main/proposals/lambda-improvements.md) | [lambdas](https://github.com/dotnet/roslyn/tree/features/lambdas) | [In Progress](https://github.com/dotnet/roslyn/issues/52192) | [cston](https://github.com/cston) | [333fred](https://github.com/333fred), [jcouv](https://github.com/jcouv) | [jaredpar](https://github.com/jaredpar) | | [nameof(parameter)](https://github.com/dotnet/csharplang/issues/373) | main | [In Progress](https://github.com/dotnet/roslyn/issues/40524) | [jcouv](https://github.com/jcouv) | TBD | [jcouv](https://github.com/jcouv) | -| [Improved Definite Assignment](https://github.com/dotnet/csharplang/issues/4465) | [improved-definite-assignment](https://github.com/dotnet/roslyn/tree/features/improved-definite-assignment) | [In Progress](https://github.com/dotnet/roslyn/issues/51463) | [RikkiGibson](https://github.com/RikkiGibson) | [jcouv](https://github.com/jcouv) | [jaredpar](https://github.com/jaredpar) | +| [Improved Definite Assignment](https://github.com/dotnet/csharplang/issues/4465) | [improved-definite-assignment](https://github.com/dotnet/roslyn/tree/features/improved-definite-assignment) | [Merged into 17.0](https://github.com/dotnet/roslyn/issues/51463) | [RikkiGibson](https://github.com/RikkiGibson) | [jcouv](https://github.com/jcouv) | [jaredpar](https://github.com/jaredpar) | | [Relax ordering of `ref` and `partial` modifiers](https://github.com/dotnet/csharplang/issues/946) | [ref-partial](https://github.com/dotnet/roslyn/tree/features/ref-partial) | In Progress | [alrz](https://github.com/alrz) | [gafter](https://github.com/gafter) | [jcouv](https://github.com/jcouv) | | [Parameter null-checking](https://github.com/dotnet/csharplang/issues/2145) | [param-nullchecking](https://github.com/dotnet/roslyn/tree/features/param-nullchecking) | [In Progress](https://github.com/dotnet/roslyn/issues/36024) | [fayrose](https://github.com/fayrose) | [agocke](https://github.com/agocke) | [jaredpar](https://github.com/jaredpar) | | [Caller expression attribute](https://github.com/dotnet/csharplang/issues/287) | [caller-argument-expression](https://github.com/dotnet/roslyn/tree/features/caller-argument-expression) | [In Progress](https://github.com/dotnet/roslyn/issues/52745) | [Youssef1313](https://github.com/Youssef1313) | [333fred](https://github.com/333fred),[AlekseyTs](https://github.com/AlekseyTs) | [jcouv](https://github.com/jcouv) |