Skip to content

Commit

Permalink
Merge pull request #742 from JetBrains/develop.ra/trigger-description
Browse files Browse the repository at this point in the history
Azure Functions: Inlay Hints with Timer Trigger Cron description
  • Loading branch information
rafaelldi authored Nov 27, 2023
2 parents 04c246e + aef605d commit 6582c43
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 12 deletions.
3 changes: 2 additions & 1 deletion PluginsAndFeatures/azure-toolkit-for-rider/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ tasks {
"$outputFolder/Azure.Intellisense/bin/$dotnetBuildConfiguration/JetBrains.ReSharper.Azure.Intellisense.pdb",
"$outputFolder/Azure.Daemon/bin/$dotnetBuildConfiguration/JetBrains.ReSharper.Azure.Daemon.dll",
"$outputFolder/Azure.Daemon/bin/$dotnetBuildConfiguration/JetBrains.ReSharper.Azure.Daemon.pdb",
"$outputFolder/Azure.Daemon/bin/$dotnetBuildConfiguration/NCrontab.Signed.dll"
"$outputFolder/Azure.Daemon/bin/$dotnetBuildConfiguration/NCrontab.Signed.dll",
"$outputFolder/Azure.Daemon/bin/$dotnetBuildConfiguration/CronExpressionDescriptor.dll"
)

for (f in dllFiles) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CronExpressionDescriptor" Version="2.21.0" />
<PackageReference Include="NCrontab.Signed" Version="3.3.3" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2018-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.

using System.Collections.Generic;
using JetBrains.Application.UI.Controls.BulbMenu.Items;
using JetBrains.Application.UI.Controls.Utils;
using JetBrains.Application.UI.PopupLayout;
using JetBrains.TextControl.DocumentMarkup.Adornments;
using JetBrains.Util;

namespace JetBrains.ReSharper.Azure.Daemon.FunctionApp.InlayHints;

public class TimerTriggerCronExpressionAdornmentDataModel(string description) : IAdornmentDataModel
{
public void ExecuteNavigation(PopupWindowContextSource popupWindowContextSource)
{
MessageBox.ShowInfo($"{nameof(TimerTriggerCronExpressionAdornmentDataModel)}.{nameof(ExecuteNavigation)}",
"ReSharper SDK");
}

public AdornmentData Data { get; } = new AdornmentData()
.WithText($"({description})")
.WithFlags(AdornmentFlags.IsNavigable)
.WithMode(PushToHintMode.Always);

public IPresentableItem? ContextMenuTitle => null;
public IEnumerable<BulbMenuItem>? ContextMenuItems => null;
public TextRange? SelectionRange => null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2018-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.

using JetBrains.ProjectModel;
using JetBrains.TextControl.DocumentMarkup;
using JetBrains.TextControl.DocumentMarkup.Adornments;

namespace JetBrains.ReSharper.Azure.Daemon.FunctionApp.InlayHints;

[SolutionComponent]
public class TimerTriggerCronExpressionAdornmentProvider : IHighlighterAdornmentProvider
{
public bool IsValid(IHighlighter highlighter)
{
return highlighter.UserData is TimerTriggerCronExpressionHint;
}

public IAdornmentDataModel? CreateDataModel(IHighlighter highlighter)
{
return highlighter.UserData is TimerTriggerCronExpressionHint hint
? new TimerTriggerCronExpressionAdornmentDataModel(hint.ToolTip)
: null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2018-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.

using JetBrains.DocumentModel;
using JetBrains.ReSharper.Daemon;
using JetBrains.ReSharper.Feature.Services.Daemon;
using JetBrains.ReSharper.Feature.Services.InlayHints;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.TextControl.DocumentMarkup;
using JetBrains.TextControl.DocumentMarkup.VisualStudio;
using JetBrains.UI.RichText;

namespace JetBrains.ReSharper.Azure.Daemon.FunctionApp.InlayHints;

[RegisterHighlighter(
HighlightAttributeId,
ForegroundColor = "#707070",
BackgroundColor = "#EBEBEB",
DarkForegroundColor = "#787878",
DarkBackgroundColor = "#3B3B3C",
EffectType = EffectType.INTRA_TEXT_ADORNMENT,
Layer = HighlighterLayer.ADDITIONAL_SYNTAX,
VsGenerateClassificationDefinition = VsGenerateDefinition.VisibleClassification,
VsBaseClassificationType = VsPredefinedClassificationType.Text,
TransmitUpdates = true)]
[DaemonAdornmentProvider(typeof(TimerTriggerCronExpressionAdornmentProvider))]
[DaemonTooltipProvider(typeof(InlayHintTooltipProvider))]
[StaticSeverityHighlighting(Severity.INFO, typeof(HighlightingGroupIds.CodeInsights), AttributeId = HighlightAttributeId)]
public class TimerTriggerCronExpressionHint(string description, ITreeNode node, DocumentOffset offset)
: IInlayHintWithDescriptionHighlighting
{
private const string HighlightAttributeId = nameof(TimerTriggerCronExpressionHint);

public RichText Description => description;

public string ToolTip => description;

public string ErrorStripeToolTip => description;

public bool IsValid() => node.IsValid();

public DocumentRange CalculateRange() => new(offset);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Copyright 2018-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using CronExpressionDescriptor;
using JetBrains.ReSharper.Azure.Daemon.Errors.FunctionAppErrors;
using JetBrains.ReSharper.Azure.Daemon.FunctionApp.InlayHints;
using JetBrains.ReSharper.Azure.Psi.FunctionApp;
using JetBrains.ReSharper.Feature.Services.Daemon;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.Util;
using Microsoft.CodeAnalysis.Elfie.Extensions;
using NCrontab;

namespace JetBrains.ReSharper.Azure.Daemon.FunctionApp.Stages.Analysis;
Expand Down Expand Up @@ -34,9 +39,10 @@ namespace JetBrains.ReSharper.Azure.Daemon.FunctionApp.Stages.Analysis;
/// </summary>
[ElementProblemAnalyzer(typeof(IAttribute), HighlightingTypes = new[]
{
typeof(TimerTriggerCronExpressionError)
typeof(TimerTriggerCronExpressionError),
typeof(TimerTriggerCronExpressionHint)
})]
public class TimerTriggerCronProblemAnalyzer : ElementProblemAnalyzer<IAttribute>
public class TimerTriggerCronExpressionAnalyzer : ElementProblemAnalyzer<IAttribute>
{
protected override void Run(IAttribute element, ElementProblemAnalyzerData data, IHighlightingConsumer consumer)
{
Expand All @@ -48,7 +54,7 @@ protected override void Run(IAttribute element, ElementProblemAnalyzerData data,

if (!FunctionAppFinder.IsTimerTriggerAttribute(typeElement)) return;

var expressionArgument = element.Arguments.First().Value;
var expressionArgument = element.Arguments.FirstOrDefault()?.Value;
if (expressionArgument is null || !expressionArgument.Type().IsString()) return;

var literal = (expressionArgument as ICSharpLiteralExpression)?.ConstantValue.StringValue;
Expand All @@ -59,46 +65,79 @@ protected override void Run(IAttribute element, ElementProblemAnalyzerData data,
var mayBeTimeSpanSchedule = literal.Contains(":");
if (mayBeTimeSpanSchedule)
{
if (!IsValidTimeSpanSchedule(literal, out var errorMessage))
if (IsValidTimeSpanSchedule(literal, out var errorMessage, out var description) &&
!string.IsNullOrEmpty(description))
{
consumer.AddHighlighting(new TimerTriggerCronExpressionHint(description, expressionArgument,
expressionArgument.GetDocumentEndOffset()));
}
else
{
consumer.AddHighlighting(new TimerTriggerCronExpressionError(expressionArgument, errorMessage));
}
}
else if (!IsValidCrontabSchedule(literal, out var errorMessage))
else
{
consumer.AddHighlighting(new TimerTriggerCronExpressionError(expressionArgument, errorMessage));
if (IsValidCrontabSchedule(literal, out var errorMessage, out var description) &&
!string.IsNullOrEmpty(description))
{
consumer.AddHighlighting(new TimerTriggerCronExpressionHint(description, expressionArgument,
expressionArgument.GetDocumentEndOffset()));
}
else
{
consumer.AddHighlighting(new TimerTriggerCronExpressionError(expressionArgument, errorMessage));
}
}
}

private static bool IsValidCrontabSchedule(string literal, out string? errorMessage)
private static bool IsValidCrontabSchedule(
string literal,
[NotNullWhen(false)] out string? errorMessage,
[NotNullWhen(true)] out string? description)
{
try
{
var normalizedLiteral = NormalizeCronSchedule(literal);
CrontabSchedule.Parse(normalizedLiteral, new CrontabSchedule.ParseOptions {IncludingSeconds = true});
CrontabSchedule.Parse(normalizedLiteral, new CrontabSchedule.ParseOptions { IncludingSeconds = true });
errorMessage = null;
try
{
description = ExpressionDescriptor.GetDescription(normalizedLiteral);
}
catch (Exception)
{
description = "";
}

return true;
}
catch (CrontabException e)
{
errorMessage = e.Message;
description = null;
return false;
}
}

private static bool IsValidTimeSpanSchedule(string literal, out string? errorMessage)
private static bool IsValidTimeSpanSchedule(
string literal,
[NotNullWhen(false)] out string? errorMessage,
[NotNullWhen(true)] out string? description)
{
// See https://github.com/Azure/azure-webjobs-sdk-extensions/blob/dev/src/WebJobs.Extensions/Extensions/Timers/Scheduling/TimerSchedule.cs#L77
try
{
// ReSharper disable once ReturnValueOfPureMethodIsNotUsed
TimeSpan.Parse(literal);
var timeSpan = TimeSpan.Parse(literal);
errorMessage = null;
description = $"Every {timeSpan.ToFriendlyString()}";
return true;
}
catch (FormatException e)
{
errorMessage = e.Message;
description = null;
return false;
}
}
Expand All @@ -115,7 +154,7 @@ private static string NormalizeCronSchedule(string cronSchedule)
{
var numberOfFields = cronSchedule.Trim()
.Replace(@"\t", " ")
.Split(new [] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Length;

if (numberOfFields == 5)
Expand Down

0 comments on commit 6582c43

Please sign in to comment.