Skip to content

Commit

Permalink
experimental hover effect (#406)
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherHX authored Oct 20, 2024
1 parent 2915ead commit 3d5eef5
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 48 deletions.
36 changes: 36 additions & 0 deletions src/Sdk/AzurePipelines/AutoCompleteHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,42 @@ Returns the uppercase equivalent of a string
Value = "Not Equals Operator"
}
};
yield return new CompletionItem {
Label = new CompletionItemLabel {
Label = "<=",
},
Kind = 24,
Documentation = new MarkdownString {
Value = "Less Equals Operator"
}
};
yield return new CompletionItem {
Label = new CompletionItemLabel {
Label = ">=",
},
Kind = 24,
Documentation = new MarkdownString {
Value = "Greater Equals Operator"
}
};
yield return new CompletionItem {
Label = new CompletionItemLabel {
Label = "<",
},
Kind = 24,
Documentation = new MarkdownString {
Value = "Less Operator"
}
};
yield return new CompletionItem {
Label = new CompletionItemLabel {
Label = ">",
},
Kind = 24,
Documentation = new MarkdownString {
Value = "Greater Operator"
}
};
yield return new CompletionItem {
Label = new CompletionItemLabel {
Label = "||",
Expand Down
38 changes: 38 additions & 0 deletions src/Sdk/AzurePipelines/PipelinesDescriptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

using Newtonsoft.Json;

namespace Sdk.Actions {

public class PipelinesDescriptions
{

public static Dictionary<string, TSource> ToOrdinalIgnoreCaseDictionary<TSource>(IEnumerable<KeyValuePair<string, TSource>> source) {
var ret = new Dictionary<string, TSource>(StringComparer.OrdinalIgnoreCase);
foreach(var kv in source) {
ret[kv.Key] = kv.Value;
}
return ret;
}

public string Description { get; set; }

public Dictionary<string, string> Versions { get; set; }
public static Dictionary<string, Dictionary<string, PipelinesDescriptions>> LoadDescriptions() {
var assembly = Assembly.GetExecutingAssembly();
var json = default(String);
using (var stream = assembly.GetManifestResourceStream("pipelines.descriptions.json"))
using (var streamReader = new StreamReader(stream))
{
json = streamReader.ReadToEnd();
}

return ToOrdinalIgnoreCaseDictionary(JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, PipelinesDescriptions>>>(json).Select(kv => new KeyValuePair<string, Dictionary<string, PipelinesDescriptions>>(kv.Key, ToOrdinalIgnoreCaseDictionary(kv.Value))));
}
}

}
114 changes: 114 additions & 0 deletions src/Sdk/AzurePipelines/descriptions.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/Sdk/Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<EmbeddedResource Include="AzurePipelines\azurepiplines.json">
<LogicalName>azurepiplines.json</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="AzurePipelines\descriptions.json">
<LogicalName>pipelines.descriptions.json</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Actions\descriptions.json">
<LogicalName>descriptions.json</LogicalName>
</EmbeddedResource>
Expand Down
2 changes: 2 additions & 0 deletions src/azure-pipelines-vscode-ext/ext-core/Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public static partial class Interop {
[JSImport("semTokens", "extension.js")]
internal static partial Task SemTokens(JSObject handle, int[] data);

[JSImport("hoverResult", "extension.js")]
internal static partial Task HoverResult(JSObject handle, string jsonRange, string conten);
}
24 changes: 24 additions & 0 deletions src/azure-pipelines-vscode-ext/ext-core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using GitHub.DistributedTask.ObjectTemplating.Schema;
using System.Linq;
using System.Text.RegularExpressions;
using Sdk.Actions;

while (true) {
await Interop.Sleep(10 * 60 * 1000);
Expand Down Expand Up @@ -170,13 +171,36 @@ public static async Task ParseCurrentPipeline(JSObject handle, string currentFil
var schema = AzureDevops.LoadSchema();
List<CompletionItem> list = AutoCompletetionHelper.CollectCompletions(column, row, context, schema);
await Interop.AutoCompleteList(handle, JsonConvert.SerializeObject(list));
var (pos, doc) = GetHoverResult(context, row, column);
await Interop.HoverResult(handle, JsonConvert.SerializeObject(pos), doc);
}
if(check && context.SemTokens?.Count > 0) {
await Interop.SemTokens(handle, [.. context.SemTokens]);
}

}

private static (Runner.Server.Azure.Devops.Range, string) GetHoverResult(Context context, int row, int column) {
var last = context.AutoCompleteMatches?.LastOrDefault();
if(last?.Tokens?.Any() == true) {
var tkn = last.Tokens.LastOrDefault(t => t.Index <= last.Index);
if(tkn == null || tkn.Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.String) {
return (null, null);
}

var i = last.Tokens.IndexOf(tkn);

var desc = PipelinesDescriptions.LoadDescriptions();

return (new Runner.Server.Azure.Devops.Range { Start = new Position { Line = row - 1, Character = column - 1 - (last.Index - tkn.Index) }, End = new Position { Line = row - 1, Character = column - 1 - (last.Index - tkn.Index) + tkn.RawValue.Length } }, i > 2 && last.Tokens[i - 2].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.NamedValue && last.Tokens[i - 1].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.Dereference && new [] { "github", "runner", "strategy" }.Contains(last.Tokens[i - 2].RawValue.ToLower()) && desc[last.Tokens[i - 2].RawValue].TryGetValue(tkn.RawValue, out var d)
|| i > 4 && last.Tokens[i - 4].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.NamedValue && last.Tokens[i - 3].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.Dereference
&& last.Tokens[i - 2].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.PropertyName && last.Tokens[i - 1].Kind == GitHub.DistributedTask.Expressions2.Tokens.TokenKind.Dereference && new [] { "steps", "jobs", "needs" }.Contains(last.Tokens[i - 4].RawValue.ToLower()) && desc[last.Tokens[i - 4].RawValue].TryGetValue(tkn.RawValue, out d)
|| desc["root"].TryGetValue(tkn.RawValue, out d)
|| desc["functions"].TryGetValue(tkn.RawValue, out d) ? d.Description : tkn.RawValue);
}
return (new Runner.Server.Azure.Devops.Range { Start = last.Token.PreWhiteSpace != null ? new Position { Line = (int)last.Token.PreWhiteSpace.Line - 1, Character = (int)last.Token.PreWhiteSpace.Character - 1 } : new Position { Line = last.Token.Line.Value - 1, Character = last.Token.Column.Value - 1 }, End = new Position { Line = (int)last.Token.PostWhiteSpace.Line - 1, Character = (int)last.Token.PostWhiteSpace.Character - 1 } }, last.Definitions.FirstOrDefault()?.Description ?? "???");
}

[MethodImpl(MethodImplOptions.NoInlining)]
[JSExport]

Check warning on line 205 in src/azure-pipelines-vscode-ext/ext-core/Program.cs

View workflow job for this annotation

GitHub Actions / deploy

This call site is reachable on all platforms. 'JSExportAttribute' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 205 in src/azure-pipelines-vscode-ext/ext-core/Program.cs

View workflow job for this annotation

GitHub Actions / deploy

This call site is reachable on all platforms. 'JSExportAttribute' is only supported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
public static string YAMLToJson(string content) {
Expand Down
134 changes: 86 additions & 48 deletions src/azure-pipelines-vscode-ext/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ function activate(context) {
if(handle.enableSemTokens){
handle.semTokens = completions;
}
},
hoverResult: async (handle, range, content) => {
handle.hover = { range: JSON.parse(range), content };
}
});
logchannel.appendLine("Starting extension main to keep dotnet alive");
Expand Down Expand Up @@ -392,6 +395,7 @@ function activate(context) {

if(pos) {
autocompletelist.autocompletelist = handle.autocompletelist
autocompletelist.hover = handle.hover
}
if(handle.enableSemTokens) {
autocompletelist.semTokens = handle.semTokens
Expand Down Expand Up @@ -533,72 +537,106 @@ function activate(context) {
}
return schema;
}

var registerSemanticHighlighting = () => vscode.languages.registerDocumentSemanticTokensProvider({
language: "yaml"
}, {
provideDocumentSemanticTokens: async (doc, token) => {
var data = {enableSemTokens: true};
await expandAzurePipeline(false, null, null, null, () => {
}, null, () => {
}, null, null, null, true, true, null, null, data);
var semTokens = data.semTokens || new Uint32Array();
return new vscode.SemanticTokens(semTokens);
}
}, new vscode.SemanticTokensLegend(["variable","parameter","function","property","constant","punctuation","string"], ["readonly","defaultLibrary","numeric"]));
var registerAutoCompletionYaml = (lang) => vscode.languages.registerCompletionItemProvider({
language: lang ?? "yaml"
}, {
provideCompletionItems: async (doc, pos, token, context) => {
var data = {autocompletelist: []};
await expandAzurePipeline(false, null, null, null, () => {
}, null, () => {
}, null, null, null, true, true, null, pos, data);
for(var item of data.autocompletelist) {
if(item.insertText && item.insertText.value) {
item.insertText = new vscode.SnippetString(item.insertText.value)
}
if(item.documentation) {
item.documentation = new vscode.MarkdownString(item.documentation.value, item.supportThemeIcons)
}
}
return data.autocompletelist
}
});

var semHightl = null;
var autoCompleteYaml = null;
var autoCompleteAdo = null;
var semHightlSettingChanged = () => {
var semHighlight = null;
var autoComplete = null;
var hover = null;
var semHighlightSettingChanged = () => {
if(vscode.workspace.getConfiguration("azure-pipelines-vscode-ext").get("enable-semantic-highlighting")) {
semHightl = registerSemanticHighlighting();
context.subscriptions.push(semHightl);
semHighlight = vscode.languages.registerDocumentSemanticTokensProvider([
{
language: "yaml"
},
{
language: "azure-pipelines"
}
], {
provideDocumentSemanticTokens: async (doc, token) => {
var data = {enableSemTokens: true};
await expandAzurePipeline(false, null, null, null, () => {
}, null, () => {
}, null, null, null, true, true, null, null, data);
var semTokens = data.semTokens || new Uint32Array();
return new vscode.SemanticTokens(semTokens);
}
}, new vscode.SemanticTokensLegend(["variable","parameter","function","property","constant","punctuation","string"], ["readonly","defaultLibrary","numeric"]));
context.subscriptions.push(semHighlight);
} else {
semHightl?.dispose();
semHighlight?.dispose();
}
};
var autoCompleteSettingChanged = () => {
if(vscode.workspace.getConfiguration("azure-pipelines-vscode-ext").get("enable-auto-complete")) {
autoCompleteYaml = registerAutoCompletionYaml();
autoCompleteAdo = registerAutoCompletionYaml("azure-pipelines");
context.subscriptions.push(autoCompleteYaml);
context.subscriptions.push(autoCompleteAdo);
autoComplete = vscode.languages.registerCompletionItemProvider([
{
language: "yaml"
},
{
language: "azure-pipelines"
}
], {
provideCompletionItems: async (doc, pos, token, context) => {
var data = {autocompletelist: []};
await expandAzurePipeline(false, null, null, null, () => {
}, null, () => {
}, null, null, null, true, true, null, pos, data);
for(var item of data.autocompletelist) {
if(item.insertText && item.insertText.value) {
item.insertText = new vscode.SnippetString(item.insertText.value)
}
if(item.documentation) {
item.documentation = new vscode.MarkdownString(item.documentation.value, item.supportThemeIcons)
}
}
return data.autocompletelist
}
});
context.subscriptions.push(autoComplete);
} else {
autoComplete?.dispose();
}
};
var hoverSettingChanged = () => {
if(vscode.workspace.getConfiguration("azure-pipelines-vscode-ext").get("enable-hover")) {
hover = vscode.languages.registerHoverProvider([
{
language: "yaml"
},
{
language: "azure-pipelines"
}
], {
provideHover: async (doc, pos, token) => {
var data = {autocompletelist: []};
await expandAzurePipeline(false, null, null, null, () => {
}, null, () => {
}, null, null, null, true, true, null, pos, data);
if(data.hover && data.hover.range && data.hover.content) {
return new vscode.Hover(new vscode.MarkdownString(data.hover.content, true), data.hover.range)
}
return null;
}
});
context.subscriptions.push(hover);
} else {
autoCompleteYaml?.dispose();
autoCompleteAdo?.dispose();
hover?.dispose();
}
};
vscode.workspace.onDidChangeConfiguration(conf => {
if(conf.affectsConfiguration("azure-pipelines-vscode-ext.enable-semantic-highlighting")) {
semHightlSettingChanged();
semHighlightSettingChanged();
}
if(conf.affectsConfiguration("azure-pipelines-vscode-ext.enable-auto-complete")) {
autoCompleteSettingChanged();
}
if(conf.affectsConfiguration("azure-pipelines-vscode-ext.enable-hover")) {
hoverSettingChanged();
}
})
semHightlSettingChanged();
semHighlightSettingChanged();
autoCompleteSettingChanged();

hoverSettingChanged();

context.subscriptions.push(vscode.commands.registerCommand(statusbar.command.command, async (file, collection, obj) => {
var getSchema = () => {
try {
Expand Down
6 changes: 6 additions & 0 deletions src/azure-pipelines-vscode-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@
"default": false,
"scope": "window",
"description": "Colorize Pipeline Expressions via Semantic Highlighting"
},
"azure-pipelines-vscode-ext.enable-hover": {
"type": "boolean",
"default": false,
"scope": "window",
"description": "Enable currently slow hover content in this web extension"
}
}
}
Expand Down

0 comments on commit 3d5eef5

Please sign in to comment.