Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure Functions: Inlay Hints with Timer Trigger Cron description #740 #741

Merged
merged 3 commits into from
Nov 24, 2023
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
Expand Up @@ -8,6 +8,7 @@
<ul>
<li>Compatibility with Rider 2023.3</li>
<li>Azure Functions: Update file templates (<a href="https://github.com/JetBrains/azure-tools-for-intellij/issues/730">#730</a>)</li>
<li>Azure Functions: Inlay Hints with Timer Trigger Cron description (<a href="https://github.com/JetBrains/azure-tools-for-intellij/issues/740">#740</a>)</li>
</ul>
<h4>Fixed bugs:</h4>
<ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CronExpressionDescriptor" Version="2.21.0" />
<PackageReference Include="JetBrains.Rider.SDK" Version="$(RiderSDKVersion)" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2020-2023 JetBrains s.r.o.
//
// All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

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 : IAdornmentDataModel
{
public TimerTriggerCronExpressionAdornmentDataModel(string description)
{
Data = new AdornmentData()
.WithText($"({description})")
.WithFlags(AdornmentFlags.IsNavigable)
.WithMode(PushToHintMode.Always);
}

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

public AdornmentData Data { get; }
public IPresentableItem ContextMenuTitle { get; }
public IEnumerable<BulbMenuItem> ContextMenuItems { get; }
public TextRange? SelectionRange { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) 2020-2023 JetBrains s.r.o.
//
// All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

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,77 @@
// Copyright (c) 2020-2023 JetBrains s.r.o.
//
// All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of
// the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

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 : IInlayHintWithDescriptionHighlighting
{
public const string HighlightAttributeId = nameof(TimerTriggerCronExpressionHint);

private readonly string _description;
private readonly ITreeNode _node;
private readonly DocumentOffset _offset;

public TimerTriggerCronExpressionHint(string description, ITreeNode node, DocumentOffset offset)
{
_description = description;
_node = node;
_offset = offset;
}

public RichText Description => _description;

public string ToolTip => _description;

public string ErrorStripeToolTip => _description;

public bool IsValid()
{
return _node.IsValid();
}

public DocumentRange CalculateRange()
{
return new DocumentRange(_offset);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2020-2021 JetBrains s.r.o.
// Copyright (c) 2020-2023 JetBrains s.r.o.
//
// All rights reserved.
//
Expand All @@ -19,13 +19,17 @@
// SOFTWARE.

using System;
using CronExpressionDescriptor;
using JetBrains.Annotations;
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 @@ -53,9 +57,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 @@ -67,8 +72,8 @@ protected override void Run(IAttribute element, ElementProblemAnalyzerData data,

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

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

var literal = (expressionArgument as ICSharpLiteralExpression)?.ConstantValue.StringValue;
if (literal.IsEmpty()) return;
Expand All @@ -78,46 +83,71 @@ 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 bool IsValidCrontabSchedule(string literal, [CanBeNull] out string errorMessage)
private bool IsValidCrontabSchedule(
string literal,
[CanBeNull] out string errorMessage,
[CanBeNull] 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 = null;
rafaelldi marked this conversation as resolved.
Show resolved Hide resolved
}
return true;
}
catch (CrontabException e)
{
errorMessage = e.Message;
description = null;
return false;
}
}

private bool IsValidTimeSpanSchedule(string literal, out string errorMessage)
private bool IsValidTimeSpanSchedule(string literal, [CanBeNull] out string errorMessage, [CanBeNull] 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ task buildReSharperPlugin {
"**/bin/$BuildConfiguration/net472/JetBrains.ReSharper.Azure*.dll",
"**/bin/$BuildConfiguration/net472/JetBrains.ReSharper.Azure*.pdb",
"Azure.Daemon/bin/$BuildConfiguration/net472/NCrontab.Signed.dll",
"Azure.Daemon/bin/$BuildConfiguration/net472/CronExpressionDescriptor.dll",
]

def srcPath = "${resharperPluginPath.absolutePath}/src"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ul>
<li>Compatibility with Rider 2023.3</li>
<li>Azure Functions: Update file templates (<a href="https://github.com/JetBrains/azure-tools-for-intellij/issues/730">#730</a>)</li>
<li>Azure Functions: Inlay Hints with Timer Trigger Cron description (<a href="https://github.com/JetBrains/azure-tools-for-intellij/issues/740">#740</a>)</li>
</ul>
<h4>Fixed bugs:</h4>
<ul>
Expand Down