Merge branch 'main' into elbrenn/duplicate-inputs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,7 +38,7 @@
|
||||
"prettier-fix": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.67",
|
||||
"@github/actions-languageservice": "^0.1.72",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {ContextProviderConfig} from "@github/actions-languageservice";
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {isMapping, isString} from "@github/actions-workflow-parser";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {getSecrets} from "./context-providers/secrets";
|
||||
import {getStepsContext} from "./context-providers/steps";
|
||||
@@ -27,7 +28,23 @@ export function contextProviders(
|
||||
) => {
|
||||
switch (name) {
|
||||
case "secrets": {
|
||||
const secrets = await getSecrets(octokit, cache, repo.owner, repo.name);
|
||||
let environmentName: string | undefined;
|
||||
if (workflowContext?.job?.environment) {
|
||||
if (isString(workflowContext.job.environment)) {
|
||||
environmentName = workflowContext.job.environment.value;
|
||||
} else if (isMapping(workflowContext.job.environment)) {
|
||||
for (const x of workflowContext.job.environment) {
|
||||
if (isString(x.key) && x.key.value === "name") {
|
||||
if (isString(x.value)) {
|
||||
environmentName = x.value.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const secrets = await getSecrets(octokit, cache, repo, environmentName);
|
||||
|
||||
defaultContext = defaultContext || new data.Dictionary();
|
||||
secrets.forEach(secret => defaultContext!.add(secret.value, new data.StringData("***")));
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import {StringData} from "@github/actions-expressions/data/string";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {RepositoryContext} from "../initializationOptions";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
export async function getSecrets(
|
||||
octokit: Octokit,
|
||||
cache: TTLCache,
|
||||
owner: string,
|
||||
name: string
|
||||
repo: RepositoryContext,
|
||||
environmentName?: string
|
||||
): Promise<StringData[]> {
|
||||
const repoSecrets = await cache.get(`${owner}/${name}/secrets`, undefined, () => fetchSecrets(octokit, owner, name));
|
||||
const secrets: StringData[] = [];
|
||||
|
||||
return repoSecrets;
|
||||
// Repo secrets
|
||||
const repoSecrets = await cache.get(`${repo.owner}/${repo.name}/secrets`, undefined, () =>
|
||||
fetchSecrets(octokit, repo.owner, repo.name)
|
||||
);
|
||||
|
||||
secrets.push(...repoSecrets);
|
||||
|
||||
// Environment secrets
|
||||
if (environmentName) {
|
||||
const envSecrets = await cache.get(
|
||||
`${repo.owner}/${repo.name}/secrets/environment/${environmentName}`,
|
||||
undefined,
|
||||
() => fetchEnvironmentSecrets(octokit, repo.id, environmentName)
|
||||
);
|
||||
|
||||
secrets.push(...envSecrets);
|
||||
}
|
||||
|
||||
return secrets.sort();
|
||||
}
|
||||
|
||||
async function fetchSecrets(octokit: Octokit, owner: string, name: string): Promise<StringData[]> {
|
||||
@@ -27,3 +46,22 @@ async function fetchSecrets(octokit: Octokit, owner: string, name: string): Prom
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchEnvironmentSecrets(
|
||||
octokit: Octokit,
|
||||
repositoryId: number,
|
||||
environmentName: string
|
||||
): Promise<StringData[]> {
|
||||
try {
|
||||
const response = await octokit.actions.listEnvironmentSecrets({
|
||||
repository_id: repositoryId,
|
||||
environment_name: environmentName
|
||||
});
|
||||
|
||||
return response.data.secrets.map(secret => new StringData(secret.name));
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve environment secrets: ", e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export async function getActionInputValues(
|
||||
return {
|
||||
label: inputName,
|
||||
description: input.description,
|
||||
insertText: `${inputName}: `,
|
||||
deprecated: input.deprecationMessage !== undefined
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {TextEdit} from "vscode-languageserver-types";
|
||||
import {MarkupContent, TextEdit} from "vscode-languageserver-types";
|
||||
import {complete} from "./complete";
|
||||
import {WorkflowContext} from "./context/workflow-context";
|
||||
import {registerLogger} from "./log";
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
]);
|
||||
|
||||
// Includes detail when available. Using continue-on-error as a sample here.
|
||||
expect(result.map(x => x.detail)).toContain(
|
||||
expect(result.map(x => (x.documentation as MarkupContent)?.value)).toContain(
|
||||
"Prevents a job from failing when a step fails. Set to true to allow a job to pass when this step fails."
|
||||
);
|
||||
});
|
||||
@@ -337,7 +337,7 @@ o|
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
const onResult = result.find(x => x.label === "on");
|
||||
expect(onResult).not.toBeUndefined();
|
||||
expect(onResult?.detail).toEqual(
|
||||
expect((onResult!.documentation as MarkupContent).value).toEqual(
|
||||
"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
|
||||
);
|
||||
});
|
||||
@@ -348,7 +348,7 @@ o|
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
const dispatchResult = result.find(x => x.label === "workflow_dispatch");
|
||||
expect(dispatchResult).not.toBeUndefined();
|
||||
expect(dispatchResult?.detail).toEqual(
|
||||
expect((dispatchResult!.documentation as MarkupContent).value).toEqual(
|
||||
"You can now create workflows that are manually triggered with the new workflow_dispatch event. You will then see a 'Run workflow' button on the Actions tab, enabling you to easily trigger a run."
|
||||
);
|
||||
});
|
||||
@@ -373,4 +373,51 @@ jobs:
|
||||
end: {line: 6, character: 17}
|
||||
});
|
||||
});
|
||||
|
||||
describe("completes with indentation", () => {
|
||||
it("default indentation", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
step|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Sequence
|
||||
expect(result.filter(x => x.label === "steps").map(x => x.textEdit?.newText)).toEqual(["steps:\n - "]);
|
||||
|
||||
// Mapping
|
||||
expect(result.filter(x => x.label === "env").map(x => x.textEdit?.newText)).toEqual(["env:\n "]);
|
||||
|
||||
// Value
|
||||
expect(result.filter(x => x.label === "timeout-minutes").map(x => x.textEdit?.newText)).toEqual([
|
||||
"timeout-minutes: "
|
||||
]);
|
||||
|
||||
// One-of
|
||||
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
|
||||
});
|
||||
|
||||
it("custom indentation", async () => {
|
||||
// Use 3 spaces to indent
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
step|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Sequence
|
||||
expect(result.filter(x => x.label === "steps").map(x => x.textEdit?.newText)).toEqual(["steps:\n - "]);
|
||||
|
||||
// Mapping
|
||||
expect(result.filter(x => x.label === "env").map(x => x.textEdit?.newText)).toEqual(["env:\n "]);
|
||||
|
||||
// Value
|
||||
expect(result.filter(x => x.label === "timeout-minutes").map(x => x.textEdit?.newText)).toEqual([
|
||||
"timeout-minutes: "
|
||||
]);
|
||||
|
||||
// One-of
|
||||
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {getContext, Mode} from "./context-providers/default";
|
||||
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {findToken} from "./utils/find-token";
|
||||
import {guessIndentation} from "./utils/indentation-guesser";
|
||||
import {mapRange} from "./utils/range";
|
||||
import {transform} from "./utils/transform";
|
||||
import {Value, ValueProviderConfig} from "./value-providers/config";
|
||||
@@ -94,18 +95,26 @@ export async function complete(
|
||||
}
|
||||
}
|
||||
|
||||
const values = await getValues(token, keyToken, parent, valueProviderConfig, workflowContext);
|
||||
const indentation = guessIndentation(newDoc, 2, true); // Use 2 spaces as default and most common for YAML
|
||||
const indentString = " ".repeat(indentation.tabSize);
|
||||
|
||||
const values = await getValues(token, keyToken, parent, valueProviderConfig, workflowContext, indentString);
|
||||
let replaceRange: Range | undefined;
|
||||
if (token?.range) {
|
||||
replaceRange = mapRange(token.range);
|
||||
}
|
||||
|
||||
return values.map(value => {
|
||||
const newText = value.insertText || value.label;
|
||||
|
||||
const item: CompletionItem = {
|
||||
label: value.label,
|
||||
detail: value.description,
|
||||
documentation: value.description && {
|
||||
kind: "markdown",
|
||||
value: value.description
|
||||
},
|
||||
tags: value.deprecated ? [CompletionItemTag.Deprecated] : undefined,
|
||||
textEdit: replaceRange ? TextEdit.replace(replaceRange, value.label) : undefined
|
||||
textEdit: replaceRange ? TextEdit.replace(replaceRange, newText) : TextEdit.insert(position, newText)
|
||||
};
|
||||
|
||||
return item;
|
||||
@@ -117,7 +126,8 @@ async function getValues(
|
||||
keyToken: TemplateToken | null,
|
||||
parent: TemplateToken | null,
|
||||
valueProviderConfig: ValueProviderConfig | undefined,
|
||||
workflowContext: WorkflowContext
|
||||
workflowContext: WorkflowContext,
|
||||
indentation: string
|
||||
): Promise<Value[]> {
|
||||
if (!parent) {
|
||||
return [];
|
||||
@@ -150,7 +160,7 @@ async function getValues(
|
||||
return [];
|
||||
}
|
||||
|
||||
const values = definitionValues(def);
|
||||
const values = definitionValues(def, indentation);
|
||||
return filterAndSortCompletionOptions(values, existingValues);
|
||||
}
|
||||
|
||||
@@ -205,6 +215,10 @@ function mapExpressionCompletionItem(item: ExpressionCompletionItem, charAfterPo
|
||||
}
|
||||
return {
|
||||
label: item.label,
|
||||
documentation: item.description && {
|
||||
kind: "markdown",
|
||||
value: item.description
|
||||
},
|
||||
insertText: insertText,
|
||||
kind: item.function ? CompletionItemKind.Function : CompletionItemKind.Variable
|
||||
};
|
||||
|
||||
@@ -47,12 +47,18 @@ export async function getContext(
|
||||
|
||||
function getDefaultContext(name: string, workflowContext: WorkflowContext, mode: Mode): ContextValue | undefined {
|
||||
switch (name) {
|
||||
case "env":
|
||||
return getEnvContext(workflowContext);
|
||||
|
||||
case "github":
|
||||
return getGithubContext(workflowContext);
|
||||
|
||||
case "inputs":
|
||||
return getInputsContext(workflowContext);
|
||||
|
||||
case "job":
|
||||
return getJobContext(workflowContext);
|
||||
|
||||
case "matrix":
|
||||
return getMatrixContext(workflowContext, mode);
|
||||
|
||||
@@ -76,12 +82,6 @@ function getDefaultContext(name: string, workflowContext: WorkflowContext, mode:
|
||||
|
||||
case "strategy":
|
||||
return getStrategyContext(workflowContext);
|
||||
|
||||
case "job":
|
||||
return getJobContext(workflowContext);
|
||||
|
||||
case "env":
|
||||
return getEnvContext(workflowContext);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {hover} from "./hover";
|
||||
import {getPositionFromCursor} from "./test-utils/cursor-position";
|
||||
|
||||
@@ -25,8 +24,33 @@ jobs:
|
||||
expect(result?.contents).toEqual("Runs your workflow when you push a commit or tag.");
|
||||
});
|
||||
|
||||
it("on a parameter with a description", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
co|ntinue-on-error: false`;
|
||||
const result = await hover(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual(
|
||||
"Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails.\n\n" +
|
||||
"**Context:** github, inputs, vars, needs, strategy, matrix"
|
||||
);
|
||||
});
|
||||
|
||||
it("on a parameter with its own type", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
pe|rmissions: read-all`;
|
||||
const result = await hover(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual(
|
||||
"You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, so that you only allow the minimum required access."
|
||||
);
|
||||
});
|
||||
|
||||
it("on a value in a sequence", async () => {
|
||||
const input = `on: [pull_request,
|
||||
const input = `on: [pull_request,
|
||||
pu|sh]
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copied from https://github.com/microsoft/vscode/blob/c3b617f9b058e01cbc5cf00ec3813a7047326503/src/vs/editor/common/model/indentationGuesser.ts
|
||||
// And adapted to work with languageserver-textdocument types
|
||||
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
|
||||
enum CharCode {
|
||||
Space = 32,
|
||||
Tab = 9,
|
||||
Comma = 44
|
||||
}
|
||||
|
||||
function getLineContent(doc: TextDocument, lineNumber: number): string {
|
||||
return doc.getText({
|
||||
start: {
|
||||
line: lineNumber - 1,
|
||||
character: 0
|
||||
},
|
||||
end: {
|
||||
line: lineNumber - 1,
|
||||
character: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getLineCharCode(doc: TextDocument, lineNumber: number, index: number): number {
|
||||
return doc
|
||||
.getText({
|
||||
start: {
|
||||
line: lineNumber - 1,
|
||||
character: index
|
||||
},
|
||||
end: {
|
||||
line: lineNumber - 1,
|
||||
character: index + 1
|
||||
}
|
||||
})
|
||||
.charCodeAt(0);
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
class SpacesDiffResult {
|
||||
public spacesDiff: number = 0;
|
||||
public looksLikeAlignment: boolean = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the diff in spaces between two line's indentation.
|
||||
*/
|
||||
function spacesDiff(a: string, aLength: number, b: string, bLength: number, result: SpacesDiffResult): void {
|
||||
result.spacesDiff = 0;
|
||||
result.looksLikeAlignment = false;
|
||||
|
||||
// This can go both ways (e.g.):
|
||||
// - a: "\t"
|
||||
// - b: "\t "
|
||||
// => This should count 1 tab and 4 spaces
|
||||
|
||||
let i: number;
|
||||
|
||||
for (i = 0; i < aLength && i < bLength; i++) {
|
||||
const aCharCode = a.charCodeAt(i);
|
||||
const bCharCode = b.charCodeAt(i);
|
||||
|
||||
if (aCharCode !== bCharCode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let aSpacesCnt = 0,
|
||||
aTabsCount = 0;
|
||||
for (let j = i; j < aLength; j++) {
|
||||
const aCharCode = a.charCodeAt(j);
|
||||
if (aCharCode === CharCode.Space) {
|
||||
aSpacesCnt++;
|
||||
} else {
|
||||
aTabsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
let bSpacesCnt = 0,
|
||||
bTabsCount = 0;
|
||||
for (let j = i; j < bLength; j++) {
|
||||
const bCharCode = b.charCodeAt(j);
|
||||
if (bCharCode === CharCode.Space) {
|
||||
bSpacesCnt++;
|
||||
} else {
|
||||
bTabsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (aSpacesCnt > 0 && aTabsCount > 0) {
|
||||
return;
|
||||
}
|
||||
if (bSpacesCnt > 0 && bTabsCount > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabsDiff = Math.abs(aTabsCount - bTabsCount);
|
||||
const spacesDiff = Math.abs(aSpacesCnt - bSpacesCnt);
|
||||
|
||||
if (tabsDiff === 0) {
|
||||
// check if the indentation difference might be caused by alignment reasons
|
||||
// sometime folks like to align their code, but this should not be used as a hint
|
||||
result.spacesDiff = spacesDiff;
|
||||
|
||||
if (spacesDiff > 0 && 0 <= bSpacesCnt - 1 && bSpacesCnt - 1 < a.length && bSpacesCnt < b.length) {
|
||||
if (b.charCodeAt(bSpacesCnt) !== CharCode.Space && a.charCodeAt(bSpacesCnt - 1) === CharCode.Space) {
|
||||
if (a.charCodeAt(a.length - 1) === CharCode.Comma) {
|
||||
// This looks like an alignment desire: e.g.
|
||||
// const a = b + c,
|
||||
// d = b - c;
|
||||
result.looksLikeAlignment = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (spacesDiff % tabsDiff === 0) {
|
||||
result.spacesDiff = spacesDiff / tabsDiff;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result for a guessIndentation
|
||||
*/
|
||||
export interface IGuessedIndentation {
|
||||
/**
|
||||
* If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent?
|
||||
*/
|
||||
tabSize: number;
|
||||
/**
|
||||
* Is indentation based on spaces?
|
||||
*/
|
||||
insertSpaces: boolean;
|
||||
}
|
||||
|
||||
export function guessIndentation(
|
||||
source: TextDocument,
|
||||
defaultTabSize: number,
|
||||
defaultInsertSpaces: boolean
|
||||
): IGuessedIndentation {
|
||||
// Look at most at the first 10k lines
|
||||
const linesCount = Math.min(source.lineCount, 10000);
|
||||
|
||||
let linesIndentedWithTabsCount = 0; // number of lines that contain at least one tab in indentation
|
||||
let linesIndentedWithSpacesCount = 0; // number of lines that contain only spaces in indentation
|
||||
|
||||
let previousLineText = ""; // content of latest line that contained non-whitespace chars
|
||||
let previousLineIndentation = 0; // index at which latest line contained the first non-whitespace char
|
||||
|
||||
const ALLOWED_TAB_SIZE_GUESSES = [2, 4, 6, 8, 3, 5, 7]; // prefer even guesses for `tabSize`, limit to [2, 8].
|
||||
const MAX_ALLOWED_TAB_SIZE_GUESS = 8; // max(ALLOWED_TAB_SIZE_GUESSES) = 8
|
||||
|
||||
const spacesDiffCount = [0, 0, 0, 0, 0, 0, 0, 0, 0]; // `tabSize` scores
|
||||
const tmp = new SpacesDiffResult();
|
||||
|
||||
for (let lineNumber = 1; lineNumber <= linesCount; lineNumber++) {
|
||||
const currentLineText = getLineContent(source, lineNumber);
|
||||
const currentLineLength = currentLineText.length;
|
||||
|
||||
// if the text buffer is chunk based, so long lines are cons-string, v8 will flattern the string when we check charCode.
|
||||
// checking charCode on chunks directly is cheaper.
|
||||
const useCurrentLineText = currentLineLength <= 65536;
|
||||
|
||||
let currentLineHasContent = false; // does `currentLineText` contain non-whitespace chars
|
||||
let currentLineIndentation = 0; // index at which `currentLineText` contains the first non-whitespace char
|
||||
let currentLineSpacesCount = 0; // count of spaces found in `currentLineText` indentation
|
||||
let currentLineTabsCount = 0; // count of tabs found in `currentLineText` indentation
|
||||
for (let j = 0, lenJ = currentLineLength; j < lenJ; j++) {
|
||||
const charCode = useCurrentLineText ? currentLineText.charCodeAt(j) : getLineCharCode(source, lineNumber, j);
|
||||
|
||||
if (charCode === CharCode.Tab) {
|
||||
currentLineTabsCount++;
|
||||
} else if (charCode === CharCode.Space) {
|
||||
currentLineSpacesCount++;
|
||||
} else {
|
||||
// Hit non whitespace character on this line
|
||||
currentLineHasContent = true;
|
||||
currentLineIndentation = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore empty or only whitespace lines
|
||||
if (!currentLineHasContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentLineTabsCount > 0) {
|
||||
linesIndentedWithTabsCount++;
|
||||
} else if (currentLineSpacesCount > 1) {
|
||||
linesIndentedWithSpacesCount++;
|
||||
}
|
||||
|
||||
spacesDiff(previousLineText, previousLineIndentation, currentLineText, currentLineIndentation, tmp);
|
||||
|
||||
if (tmp.looksLikeAlignment) {
|
||||
// if defaultInsertSpaces === true && the spaces count == tabSize, we may want to count it as valid indentation
|
||||
//
|
||||
// - item1
|
||||
// - item2
|
||||
//
|
||||
// otherwise skip this line entirely
|
||||
//
|
||||
// const a = 1,
|
||||
// b = 2;
|
||||
|
||||
if (!(defaultInsertSpaces && defaultTabSize === tmp.spacesDiff)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const currentSpacesDiff = tmp.spacesDiff;
|
||||
if (currentSpacesDiff <= MAX_ALLOWED_TAB_SIZE_GUESS) {
|
||||
spacesDiffCount[currentSpacesDiff]++;
|
||||
}
|
||||
|
||||
previousLineText = currentLineText;
|
||||
previousLineIndentation = currentLineIndentation;
|
||||
}
|
||||
|
||||
let insertSpaces = defaultInsertSpaces;
|
||||
if (linesIndentedWithTabsCount !== linesIndentedWithSpacesCount) {
|
||||
insertSpaces = linesIndentedWithTabsCount < linesIndentedWithSpacesCount;
|
||||
}
|
||||
|
||||
let tabSize = defaultTabSize;
|
||||
|
||||
// Guess tabSize only if inserting spaces...
|
||||
if (insertSpaces) {
|
||||
let tabSizeScore = insertSpaces ? 0 : 0.1 * linesCount;
|
||||
|
||||
// console.log("score threshold: " + tabSizeScore);
|
||||
|
||||
ALLOWED_TAB_SIZE_GUESSES.forEach(possibleTabSize => {
|
||||
const possibleTabSizeScore = spacesDiffCount[possibleTabSize];
|
||||
if (possibleTabSizeScore > tabSizeScore) {
|
||||
tabSizeScore = possibleTabSizeScore;
|
||||
tabSize = possibleTabSize;
|
||||
}
|
||||
});
|
||||
|
||||
// Let a tabSize of 2 win even if it is not the maximum
|
||||
// (only in case 4 was guessed)
|
||||
if (
|
||||
tabSize === 4 &&
|
||||
spacesDiffCount[4] > 0 &&
|
||||
spacesDiffCount[2] > 0 &&
|
||||
spacesDiffCount[2] >= spacesDiffCount[4] / 2
|
||||
) {
|
||||
tabSize = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('--------------------------');
|
||||
// console.log('linesIndentedWithTabsCount: ' + linesIndentedWithTabsCount + ', linesIndentedWithSpacesCount: ' + linesIndentedWithSpacesCount);
|
||||
// console.log('spacesDiffCount: ' + spacesDiffCount);
|
||||
// console.log('tabSize: ' + tabSize + ', tabSizeScore: ' + tabSizeScore);
|
||||
|
||||
return {
|
||||
insertSpaces: insertSpaces,
|
||||
tabSize: tabSize
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export interface Value {
|
||||
/** Label of this value */
|
||||
label: string;
|
||||
|
||||
/** Optional description to show when auto-completing or hovering */
|
||||
description?: string;
|
||||
|
||||
/** Whether this value is deprecated */
|
||||
deprecated?: boolean;
|
||||
|
||||
/** Alternative insert text, if not given `label` will be used */
|
||||
insertText?: string;
|
||||
}
|
||||
|
||||
export enum ValueProviderKind {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {BooleanDefinition} from "@github/actions-workflow-parser/templates/schema/boolean-definition";
|
||||
import {Definition} from "@github/actions-workflow-parser/templates/schema/definition";
|
||||
import {DefinitionType} from "@github/actions-workflow-parser/templates/schema/definition-type";
|
||||
import {MappingDefinition} from "@github/actions-workflow-parser/templates/schema/mapping-definition";
|
||||
import {OneOfDefinition} from "@github/actions-workflow-parser/templates/schema/one-of-definition";
|
||||
import {SequenceDefinition} from "@github/actions-workflow-parser/templates/schema/sequence-definition";
|
||||
@@ -8,15 +9,15 @@ import {getWorkflowSchema} from "@github/actions-workflow-parser/workflows/workf
|
||||
import {Value} from "./config";
|
||||
import {stringsToValues} from "./strings-to-values";
|
||||
|
||||
export function definitionValues(def: Definition): Value[] {
|
||||
export function definitionValues(def: Definition, indentation: string): Value[] {
|
||||
const schema = getWorkflowSchema();
|
||||
|
||||
if (def instanceof MappingDefinition) {
|
||||
return mappingValues(def, schema.definitions);
|
||||
return mappingValues(def, schema.definitions, indentation);
|
||||
}
|
||||
|
||||
if (def instanceof OneOfDefinition) {
|
||||
return oneOfValues(def, schema.definitions);
|
||||
return oneOfValues(def, schema.definitions, indentation);
|
||||
}
|
||||
|
||||
if (def instanceof BooleanDefinition) {
|
||||
@@ -35,30 +36,64 @@ export function definitionValues(def: Definition): Value[] {
|
||||
if (def instanceof SequenceDefinition) {
|
||||
const itemDef = schema.getDefinition(def.itemType);
|
||||
if (itemDef) {
|
||||
return definitionValues(itemDef);
|
||||
return definitionValues(itemDef, indentation);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function mappingValues(mappingDefinition: MappingDefinition, definitions: {[key: string]: Definition}): Value[] {
|
||||
function mappingValues(
|
||||
mappingDefinition: MappingDefinition,
|
||||
definitions: {[key: string]: Definition},
|
||||
indentation: string
|
||||
): Value[] {
|
||||
const properties: Value[] = [];
|
||||
for (const [key, value] of Object.entries(mappingDefinition.properties)) {
|
||||
let insertText: string | undefined;
|
||||
|
||||
let description: string | undefined;
|
||||
if (value.type) {
|
||||
const typeDef = definitions[value.type];
|
||||
description = typeDef?.description;
|
||||
|
||||
if (typeDef) {
|
||||
switch (typeDef.definitionType) {
|
||||
case DefinitionType.Sequence:
|
||||
insertText = `${key}:\n${indentation}- `;
|
||||
break;
|
||||
|
||||
case DefinitionType.Mapping:
|
||||
insertText = `${key}:\n${indentation}`;
|
||||
break;
|
||||
|
||||
case DefinitionType.OneOf:
|
||||
// No special insertText in this case
|
||||
break;
|
||||
|
||||
default:
|
||||
insertText = `${key}: `;
|
||||
}
|
||||
}
|
||||
}
|
||||
properties.push({label: key, description: description});
|
||||
|
||||
properties.push({
|
||||
label: key,
|
||||
description,
|
||||
insertText
|
||||
});
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
function oneOfValues(oneOfDefinition: OneOfDefinition, definitions: {[key: string]: Definition}): Value[] {
|
||||
function oneOfValues(
|
||||
oneOfDefinition: OneOfDefinition,
|
||||
definitions: {[key: string]: Definition},
|
||||
indentation: string
|
||||
): Value[] {
|
||||
const values: Value[] = [];
|
||||
for (const key of oneOfDefinition.oneOf) {
|
||||
values.push(...definitionValues(definitions[key]));
|
||||
values.push(...definitionValues(definitions[key], indentation));
|
||||
}
|
||||
return distinctValues(values);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.67",
|
||||
"@github/actions-languageserver": "^0.1.72",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.67"
|
||||
"version": "0.1.72"
|
||||
}
|
||||
|
||||
Generated
+13
-13
@@ -16,10 +16,10 @@
|
||||
},
|
||||
"actions-languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.67",
|
||||
"@github/actions-languageservice": "^0.1.72",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"actions-languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
@@ -63,10 +63,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.67",
|
||||
"version": "0.1.72",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.67",
|
||||
"@github/actions-languageserver": "^0.1.72",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -702,9 +702,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@github/actions-workflow-parser": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.39/ba3e9e02e489175e0f284f0aebd9bde92ad4e935",
|
||||
"integrity": "sha512-Iw8OkCUxdgFvXpJkg6dAIEpGQrS09o3TC1I0looCJ1lHMBDBD7aVXasE8zJzlQUTdgRTGMiptNMgZS2MnGC76Q==",
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.42/e511c44db93c2c8b522e48ca54fe733063902dd3",
|
||||
"integrity": "sha512-Qd4vmYjNdFxHBer0P3RympLnBqDCigRqrxvMFRFEfLhUQ1oeBHmmvF6sCEgQ+JtKFlwVPvOoJLmfN/r8o5NEBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
@@ -13533,7 +13533,7 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:actions-languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.67",
|
||||
"@github/actions-languageservice": "^0.1.72",
|
||||
"@github/actions-workflow-parser": "*",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -13565,9 +13565,9 @@
|
||||
}
|
||||
},
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.39/ba3e9e02e489175e0f284f0aebd9bde92ad4e935",
|
||||
"integrity": "sha512-Iw8OkCUxdgFvXpJkg6dAIEpGQrS09o3TC1I0looCJ1lHMBDBD7aVXasE8zJzlQUTdgRTGMiptNMgZS2MnGC76Q==",
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.42/e511c44db93c2c8b522e48ca54fe733063902dd3",
|
||||
"integrity": "sha512-Qd4vmYjNdFxHBer0P3RympLnBqDCigRqrxvMFRFEfLhUQ1oeBHmmvF6sCEgQ+JtKFlwVPvOoJLmfN/r8o5NEBQ==",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "*",
|
||||
"yaml": "^2.0.0-8"
|
||||
@@ -16320,7 +16320,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.67",
|
||||
"@github/actions-languageserver": "^0.1.72",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
|
||||
Reference in New Issue
Block a user