From 3d5eef5d0aa8068f94a6c990e91315aa343749aa Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 20 Oct 2024 16:46:24 +0200 Subject: [PATCH] experimental hover effect (#406) --- src/Sdk/AzurePipelines/AutoCompleteHelper.cs | 36 +++++ .../AzurePipelines/PipelinesDescriptions.cs | 38 +++++ src/Sdk/AzurePipelines/descriptions.json | 114 +++++++++++++++ src/Sdk/Sdk.csproj | 3 + .../ext-core/Interop.cs | 2 + .../ext-core/Program.cs | 24 ++++ src/azure-pipelines-vscode-ext/index.js | 134 +++++++++++------- src/azure-pipelines-vscode-ext/package.json | 6 + 8 files changed, 309 insertions(+), 48 deletions(-) create mode 100644 src/Sdk/AzurePipelines/PipelinesDescriptions.cs create mode 100644 src/Sdk/AzurePipelines/descriptions.json diff --git a/src/Sdk/AzurePipelines/AutoCompleteHelper.cs b/src/Sdk/AzurePipelines/AutoCompleteHelper.cs index cdd7ec9c05d..f317cd468ae 100644 --- a/src/Sdk/AzurePipelines/AutoCompleteHelper.cs +++ b/src/Sdk/AzurePipelines/AutoCompleteHelper.cs @@ -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 = "||", diff --git a/src/Sdk/AzurePipelines/PipelinesDescriptions.cs b/src/Sdk/AzurePipelines/PipelinesDescriptions.cs new file mode 100644 index 00000000000..14e52aca277 --- /dev/null +++ b/src/Sdk/AzurePipelines/PipelinesDescriptions.cs @@ -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 ToOrdinalIgnoreCaseDictionary(IEnumerable> source) { + var ret = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(var kv in source) { + ret[kv.Key] = kv.Value; + } + return ret; + } + + public string Description { get; set; } + + public Dictionary Versions { get; set; } + public static Dictionary> 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>>(json).Select(kv => new KeyValuePair>(kv.Key, ToOrdinalIgnoreCaseDictionary(kv.Value)))); + } + } + +} \ No newline at end of file diff --git a/src/Sdk/AzurePipelines/descriptions.json b/src/Sdk/AzurePipelines/descriptions.json new file mode 100644 index 00000000000..957c453d899 --- /dev/null +++ b/src/Sdk/AzurePipelines/descriptions.json @@ -0,0 +1,114 @@ +{ + "root": { + "parameters": { + "description": "Contains parameters." + }, + "pipeline": { + "description": "Contains informations about this pipeline run" + }, + "dependencies": { + "description": "Contains output and status informations of dependend stages or jobs in same stage." + }, + "stageDependencies": { + "description": "Contains output and status informations of jobs in dependend stages." + }, + "variables": { + "description": "Contains variables set at the repository, organization, or environment levels. For more information, see [`vars` context](https://docs.github.com/actions/learn-github-actions/contexts#vars-context)." + } + }, + "functions": { + "always": { + "description": "* Always evaluates to `True` (even when canceled). Note: A critical failure may still prevent a task from running. For example, if getting sources failed." + }, + "canceled": { + "description": "* Evaluates to `True` if the pipeline was canceled." + }, + "failed": { + "description": "* For a step, equivalent to `eq(variables['Agent.JobStatus'], 'Failed')`.\n* For a job:\n * With no arguments, evaluates to `True` only if any previous job in the dependency graph failed.\n * With job names as arguments, evaluates to `True` only if any of those jobs failed." + }, + "succeeded": { + "description": "* For a step, equivalent to `in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')`\n* Use with `dependsOn` when working with jobs and you want to evaluate whether a previous job was successful. Jobs are designed to run in parallel while stages run sequentially. \n* For a job:\n * With no arguments, evaluates to `True` only if all previous jobs in the dependency graph succeeded or partially succeeded.\n * With job names as arguments, evaluates to `True` if all of those jobs succeeded or partially succeeded.\n * Evaluates to `False` if the pipeline is canceled." + }, + "succeededOrFailed": { + "description": "* For a step, equivalent to `in(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues', 'Failed')`\n* For a job:\n * With no arguments, evaluates to `True` regardless of whether any jobs in the dependency graph succeeded or failed.\n * With job names as arguments, evaluates to `True` whether any of those jobs succeeded or failed.\n * You may want to use `not(canceled())` instead when there are previous skipped jobs in the dependency graph. \n\n > This is like `always()`, except it will evaluate `False` when the pipeline is canceled." + }, + "and": { + "description": "* Evaluates to `True` if all parameters are `True`\n* Min parameters: 2. Max parameters: N\n* Casts parameters to Boolean for evaluation\n* Short-circuits after first `False`\n* Example: `and(eq(variables.letters, 'ABC'), eq(variables.numbers, 123))`" + }, + "coalesce": { + "description": "* Evaluates the parameters in order (left to right), and returns the first value that doesn't equal null or empty-string.\n* No value is returned if the parameter values all are null or empty strings.\n* Min parameters: 2. Max parameters: N\n* Example: `coalesce(variables.couldBeNull, variables.couldAlsoBeNull, 'literal so it always works')`" + }, + "contains": { + "description": "* Evaluates `True` if left parameter String contains right parameter\n* Min parameters: 2. Max parameters: 2\n* Casts parameters to String for evaluation\n* Performs ordinal ignore-case comparison\n* Example: `contains('ABCDE', 'BCD')` (returns True)" + }, + "containsValue": { + "description": "* Evaluates `True` if the left parameter is an array, and any item equals the right parameter. Also evaluates `True` if the left parameter is an object, and the value of any property equals the right parameter.\n* Min parameters: 2. Max parameters: 2\n* If the left parameter is an array, convert each item to match the type of the right parameter. If the left parameter is an object, convert the value of each property to match the type of the right parameter. The equality comparison for each specific item evaluates `False` if the conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Short-circuits after the first match\n\n> [!NOTE]\n> There is no literal syntax in a YAML pipeline for specifying an array.\n> This function is of limited use in general pipelines.\n> It's intended for use in the [pipeline decorator context](../../extend/develop/pipeline-decorator-context.md) with system-provided arrays such as the list of steps.\n\nYou can use the `containsValue` expression to find a matching value in an object. Here's an example that demonstrates looking in list of source branches for a match for `Build.SourceBranch`. \n\n```yaml\nparameters:\n- name: branchOptions\n displayName: Source branch options\n type: object\n default:\n - refs/heads/main\n - refs/heads/test\n\njobs:\n - job: A1 \n steps:\n - ${{ each value in parameters.branchOptions }}:\n - script: echo ${{ value }}\n\n - job: B1 \n condition: ${{ containsValue(parameters.branchOptions, variables['Build.SourceBranch']) }}\n steps:\n - script: echo \"Matching branch found\"\n```" + }, + "convertToJson": { + "description": "* Take a complex object and outputs it as JSON.\n* Min parameters: 1. Max parameters: 1.\n\n```yaml\nparameters:\n - name: listOfValues\n type: object\n default:\n this_is:\n a_complex: object\n with:\n - one\n - two\n \nsteps:\n- script: |\n echo \"${MY_JSON}\"\n env:\n MY_JSON: ${{ convertToJson(parameters.listOfValues) }}\n```\n\nScript output:\n\n```json\n{\n \"this_is\": {\n \"a_complex\": \"object\",\n \"with\": [\n \"one\",\n \"two\"\n ]\n }\n}\n```" + }, + "counter": { + "description": "* This function can only be used in an expression that defines a variable. It can't be used as part of a condition for a step, job, or stage.\n* Evaluates a number that is incremented with each run of a pipeline.\n* Parameters: 2. `prefix` and `seed`.\n* Prefix is a string expression. A separate value of counter is tracked for each unique value of prefix. The `prefix` should use UTF-16 characters.\n* Seed is the starting value of the counter\n\nYou can create a counter that is automatically incremented by one in each execution of your pipeline. When you define a counter, you provide a `prefix` and a `seed`. Here's an example that demonstrates this. \n\n```yaml\nvariables:\n major: 1\n # define minor as a counter with the prefix as variable major, and seed as 100.\n minor: $[counter(variables['major'], 100)]\n\nsteps:\n- bash: echo $(minor)\n```\n\nThe value of `minor` in the above example in the first run of the pipeline is 100. In the second run it is 101, provided the value of `major` is still 1.\n\nIf you edit the YAML file, and update the value of the variable `major` to be 2, then in the next run of the pipeline, the value of `minor` will be 100. Subsequent runs increment the counter to 101, 102, 103, ...\n\nLater, if you edit the YAML file, and set the value of `major` back to 1, then the value of the counter resumes where it left off for that prefix. In this example, it resumes at 102.\n\nHere's another example of setting a variable to act as a counter that starts at 100, gets incremented by 1 for every run, and gets reset to 100 every day.\n\n> [!NOTE]\n> `pipeline.startTime` is not available outside of expressions. `pipeline.startTime`\n> formats `system.pipelineStartTime` into a date and time object so that it is available to work with expressions.\n> The default time zone for `pipeline.startTime` is UTC. You can [change the time zone](../../organizations/accounts/change-time-zone.md) for your organization.\n\n\n```yaml\njobs:\n- job:\n variables:\n a: $[counter(format('{0:yyyyMMdd}', pipeline.startTime), 100)]\n steps:\n - bash: echo $(a)\n```\n\nHere's an example of having a counter that maintains a separate value for PRs and CI runs.\n\n```yaml\nvariables:\n patch: $[counter(variables['build.reason'], 0)]\n```\n\nCounters are scoped to a pipeline. In other words, its value is incremented for each run of that pipeline. There are no project-scoped counters." + }, + "endsWith": { + "description": "* Evaluates `True` if left parameter String ends with right parameter\n* Min parameters: 2. Max parameters: 2\n* Casts parameters to String for evaluation\n* Performs ordinal ignore-case comparison\n* Example: `endsWith('ABCDE', 'DE')` (returns True)" + }, + "eq": { + "description": "* Evaluates `True` if parameters are equal\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Returns `False` if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `eq(variables.letters, 'ABC')`" + }, + "format": { + "description": "* Evaluates the trailing parameters and inserts them into the leading parameter string\n* Min parameters: 1. Max parameters: N\n* Example: `format('Hello {0} {1}', 'John', 'Doe')`\n* Uses [.NET custom date and time format specifiers](/dotnet/standard/base-types/custom-date-and-time-format-strings) for date formatting (`yyyy`, `yy`, `MM`, `M`, `dd`, `d`, `HH`, `H`, `m`, `mm`, `ss`, `s`, `f`, `ff`, `ffff`, `K`)\n* Example: `format('{0:yyyyMMdd}', pipeline.startTime)`. In this case `pipeline.startTime` is a special date time object variable.\n* Escape by doubling braces. For example: `format('literal left brace {{ and literal right brace }}')`" + }, + "ge": { + "description": "* Evaluates `True` if left parameter is greater than or equal to the right parameter\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Errors if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `ge(5, 5)` (returns True)" + }, + "gt": { + "description": "* Evaluates `True` if left parameter is greater than the right parameter\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Errors if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `gt(5, 2)` (returns True)" + }, + "in": { + "description": "* Evaluates `True` if left parameter is equal to any right parameter\n* Min parameters: 1. Max parameters: N\n* Converts right parameters to match type of left parameter. Equality comparison evaluates `False` if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Short-circuits after first match\n* Example: `in('B', 'A', 'B', 'C')` (returns True)" + }, + "join": { + "description": "* Concatenates all elements in the right parameter array, separated by the left parameter string.\n* Min parameters: 2. Max parameters: 2\n* Each element in the array is converted to a string. Complex objects are converted to empty string.\n* If the right parameter isn't an array, the result is the right parameter converted to a string.\n\nIn this example, a semicolon gets added between each item in the array. The parameter type is an object.\n\n```yaml\nparameters:\n- name: myArray\n type: object\n default:\n - FOO\n - BAR\n - ZOO\n\nvariables:\n A: ${{ join(';',parameters.myArray) }}\n\nsteps:\n - script: echo $A # outputs FOO;BAR;ZOO\n```" + }, + "le": { + "description": "* Evaluates `True` if left parameter is less than or equal to the right parameter\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Errors if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `le(2, 2)` (returns True)" + }, + "length": { + "description": "* Returns the length of a string or an array, either one that comes from the system or that comes from a parameter\n* Min parameters: 1. Max parameters 1\n* Example: `length('fabrikam')` returns 8" + }, + "lower": { + "description": "* Converts a string or variable value to all lowercase characters\n* Min parameters: 1. Max parameters 1\n* Returns the lowercase equivalent of a string\n* Example: `lower('FOO')` returns `foo`" + }, + "lt": { + "description": "* Evaluates `True` if left parameter is less than the right parameter\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Errors if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `lt(2, 5)` (returns True)" + }, + "ne": { + "description": "* Evaluates `True` if parameters are not equal\n* Min parameters: 2. Max parameters: 2\n* Converts right parameter to match type of left parameter. Returns `True` if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Example: `ne(1, 2)` (returns True)" + }, + "not": { + "description": "* Evaluates `True` if parameter is `False`\n* Min parameters: 1. Max parameters: 1\n* Converts value to Boolean for evaluation\n* Example: `not(eq(1, 2))` (returns True)" + }, + "notIn": { + "description": "* Evaluates `True` if left parameter isn't equal to any right parameter\n* Min parameters: 1. Max parameters: N\n* Converts right parameters to match type of left parameter. Equality comparison evaluates `False` if conversion fails.\n* Ordinal ignore-case comparison for Strings\n* Short-circuits after first match\n* Example: `notIn('D', 'A', 'B', 'C')` (returns True)" + }, + "or": { + "description": "* Evaluates `True` if any parameter is `True`\n* Min parameters: 2. Max parameters: N\n* Casts parameters to Boolean for evaluation\n* Short-circuits after first `True`\n* Example: `or(eq(1, 1), eq(2, 3))` (returns True, short-circuits)" + }, + "replace": { + "description": "* Returns a new string in which all instances of a string in the current instance are replaced with another string\n* Min parameters: 3. Max parameters: 3\n* `replace(a, b, c)`: returns a, with all instances of b replaced by c\n* Example: `replace('https://www.tinfoilsecurity.com/saml/consume','https://www.tinfoilsecurity.com','http://server')` (returns `http://server/saml/consume`)" + }, + "split": { + "description": "* Splits a string into substrings based on the specified delimiting characters \n* Min parameters: 2. Max parameters: 2\n* The first parameter is the string to split\n* The second parameter is the delimiting characters\n* Returns an array of substrings. The array includes empty strings when the delimiting characters appear consecutively or at the end of the string\n* Example: \n ```yml\n variables:\n - name: environments\n value: prod1,prod2 \n steps: \n - ${{ each env in split(variables.environments, ',')}}:\n - script: ./deploy.sh --environment ${{ env }}\n ```\n* Example of using split() with replace():\n ```yml\n parameters:\n - name: resourceIds\n type: object\n default:\n - /subscriptions/mysubscription/resourceGroups/myResourceGroup/providers/Microsoft.Network/loadBalancers/kubernetes-internal\n - /subscriptions/mysubscription02/resourceGroups/myResourceGroup02/providers/Microsoft.Network/loadBalancers/kubernetes\n - name: environments\n type: object\n default: \n - prod1\n - prod2\n\n trigger:\n - main\n \n steps:\n - ${{ each env in parameters.environments }}:\n - ${{ each resourceId in parameters.resourceIds }}:\n - script: echo ${{ replace(split(resourceId, '/')[8], '-', '_') }}_${{ env }}\n ```" + }, + "startsWith": { + "description": "* Evaluates `True` if left parameter string starts with right parameter\n* Min parameters: 2. Max parameters: 2\n* Casts parameters to String for evaluation\n* Performs ordinal ignore-case comparison\n* Example: `startsWith('ABCDE', 'AB')` (returns True)" + }, + "upper": { + "description": "* Converts a string or variable value to all uppercase characters\n* Min parameters: 1. Max parameters 1\n* Returns the uppercase equivalent of a string\n* Example: `upper('bah')` returns `BAH`" + }, + "xor": { + "description": "* Evaluates `True` if exactly one parameter is `True`\n* Min parameters: 2. Max parameters: 2\n* Casts parameters to Boolean for evaluation\n* Example: `xor(True, False)` (returns True)" + } + } +} \ No newline at end of file diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj index f43e78974d3..482e9d3d969 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -37,6 +37,9 @@ azurepiplines.json + + pipelines.descriptions.json + descriptions.json diff --git a/src/azure-pipelines-vscode-ext/ext-core/Interop.cs b/src/azure-pipelines-vscode-ext/ext-core/Interop.cs index b47f3be9ad6..df8a990ba51 100644 --- a/src/azure-pipelines-vscode-ext/ext-core/Interop.cs +++ b/src/azure-pipelines-vscode-ext/ext-core/Interop.cs @@ -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); } \ No newline at end of file diff --git a/src/azure-pipelines-vscode-ext/ext-core/Program.cs b/src/azure-pipelines-vscode-ext/ext-core/Program.cs index b4b9328f04d..ce778c51e5e 100644 --- a/src/azure-pipelines-vscode-ext/ext-core/Program.cs +++ b/src/azure-pipelines-vscode-ext/ext-core/Program.cs @@ -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); @@ -170,6 +171,8 @@ public static async Task ParseCurrentPipeline(JSObject handle, string currentFil var schema = AzureDevops.LoadSchema(); List 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]); @@ -177,6 +180,27 @@ public static async Task ParseCurrentPipeline(JSObject handle, string currentFil } + 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] public static string YAMLToJson(string content) { diff --git a/src/azure-pipelines-vscode-ext/index.js b/src/azure-pipelines-vscode-ext/index.js index 6830de937cf..a70c71d0bc3 100644 --- a/src/azure-pipelines-vscode-ext/index.js +++ b/src/azure-pipelines-vscode-ext/index.js @@ -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"); @@ -392,6 +395,7 @@ function activate(context) { if(pos) { autocompletelist.autocompletelist = handle.autocompletelist + autocompletelist.hover = handle.hover } if(handle.enableSemTokens) { autocompletelist.semTokens = handle.semTokens @@ -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 { diff --git a/src/azure-pipelines-vscode-ext/package.json b/src/azure-pipelines-vscode-ext/package.json index 613b2b926d5..f121921b7c2 100644 --- a/src/azure-pipelines-vscode-ext/package.json +++ b/src/azure-pipelines-vscode-ext/package.json @@ -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" } } }