198 lines
5.1 KiB
TypeScript
198 lines
5.1 KiB
TypeScript
import {isString} from "@github/actions-workflow-parser";
|
|
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index";
|
|
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
|
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
|
import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types";
|
|
import {Position} from "vscode-languageserver-textdocument";
|
|
|
|
export function findInnerToken(pos: Position, root?: TemplateToken) {
|
|
const {token} = findToken(pos, root);
|
|
return token;
|
|
}
|
|
|
|
export type TokenResult = {
|
|
parent: TemplateToken | null;
|
|
keyToken: TemplateToken | null;
|
|
token: TemplateToken | null;
|
|
|
|
path: TemplateToken[];
|
|
};
|
|
|
|
/**
|
|
* Find a token at the given position in the document.
|
|
*
|
|
* If the position is within
|
|
* - the key of a mapping, parent will be the mapping, keyToken will be null, and token will be the key.
|
|
* - the value of a mapping, parent will be the mapping, keyToken will be the key for the value, and token will be the value
|
|
* - a sequence item, parent will be the sequence, keyToken will be null, and token will be the item
|
|
*
|
|
* @param pos Position within the document for which to find a token
|
|
* @param root Root node
|
|
* @returns Token result
|
|
*/
|
|
export function findToken(pos: Position, root?: TemplateToken): TokenResult {
|
|
if (!root) {
|
|
return {
|
|
token: null,
|
|
keyToken: null,
|
|
parent: null,
|
|
path: []
|
|
};
|
|
}
|
|
|
|
let lastMatching: TokenResult | null = null;
|
|
|
|
const s: TokenResult[] = [
|
|
{
|
|
token: root,
|
|
keyToken: null,
|
|
parent: null,
|
|
path: []
|
|
}
|
|
];
|
|
|
|
while (s.length > 0) {
|
|
const result = s.shift()!;
|
|
const {parent, token, keyToken, path} = result;
|
|
if (!token) {
|
|
break;
|
|
}
|
|
|
|
if (!posInToken(pos, token)) {
|
|
continue;
|
|
}
|
|
|
|
// Pos is in token, remember this token
|
|
lastMatching = result;
|
|
|
|
// Position is in token, enqueue children if there are any
|
|
switch (token.templateTokenType) {
|
|
case TokenType.Mapping:
|
|
const mappingToken = token as MappingToken;
|
|
for (const {key, value} of mappingToken) {
|
|
// If the position is within the key, immediately return it as the token.
|
|
if (posInToken(pos, key)) {
|
|
return {
|
|
parent: mappingToken,
|
|
keyToken: null,
|
|
token: key,
|
|
path: [...path, mappingToken]
|
|
};
|
|
}
|
|
|
|
// If pos, key, and value are on the same line, and value is an empty node (null, empty string) return early
|
|
// we cannot reliably check the position in that empty node
|
|
if (onSameLine(pos, key, value) && emptyNode(value)) {
|
|
return {
|
|
parent: mappingToken,
|
|
keyToken: key,
|
|
token: value,
|
|
path: [...path, mappingToken]
|
|
};
|
|
}
|
|
|
|
s.push({
|
|
parent: mappingToken,
|
|
keyToken: key,
|
|
token: value,
|
|
path: [...path, mappingToken, key]
|
|
});
|
|
}
|
|
continue;
|
|
|
|
case TokenType.Sequence:
|
|
const sequenceToken = token as SequenceToken;
|
|
for (const token of sequenceToken) {
|
|
s.push({
|
|
parent: sequenceToken,
|
|
keyToken: null,
|
|
token: token,
|
|
path: [...path, sequenceToken]
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
return {
|
|
token,
|
|
keyToken,
|
|
parent,
|
|
path
|
|
};
|
|
}
|
|
|
|
// Did not find a matching token, return the last matching token as parent
|
|
return {
|
|
token: null,
|
|
parent: lastMatching?.token ?? null,
|
|
keyToken: null,
|
|
path: lastMatching?.token ? [...lastMatching.path, lastMatching.token] : []
|
|
};
|
|
}
|
|
|
|
function posInToken(pos: Position, token: TemplateToken): boolean {
|
|
if (!token.range) {
|
|
return false;
|
|
}
|
|
const r = token.range;
|
|
|
|
// TokenRange is one-based, Position is zero-based
|
|
const tokenLine = pos.line + 1;
|
|
const tokenChar = pos.character + 1;
|
|
|
|
// Check lines
|
|
if (r.start.line > tokenLine || tokenLine > r.end.line) {
|
|
return false;
|
|
}
|
|
|
|
// Position is within the token lines. Check character/column if pos line matches
|
|
// start or end
|
|
if (
|
|
(r.start.line === tokenLine && tokenChar < r.start.column) ||
|
|
(r.end.line === tokenLine && tokenChar > r.end.column)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function onSameLine(pos: Position, key: TemplateToken, value: TemplateToken): boolean {
|
|
if (!value.range) {
|
|
return false;
|
|
}
|
|
|
|
if (!key.range) {
|
|
return false;
|
|
}
|
|
|
|
if (value.range.start.line !== value.range.end.line) {
|
|
// Token occupies multiple lines, can't be an empty node
|
|
return false;
|
|
}
|
|
|
|
// TokenRange is one-based, Position is zero-based
|
|
const posLine = pos.line + 1;
|
|
if (posLine != value.range.start.line) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function emptyNode(token: TemplateToken | null): boolean {
|
|
if (!token) {
|
|
return false;
|
|
}
|
|
|
|
if (token.templateTokenType === TokenType.Null) {
|
|
return true;
|
|
}
|
|
|
|
if (isString(token)) {
|
|
return token.value === "";
|
|
}
|
|
|
|
return false;
|
|
}
|