blob: f18e26e4f0ec6e7ae7f1da802b1abfedd727c113 [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as vscode from 'vscode';
export function activate(_context: vscode.ExtensionContext): void {
vscode.languages.registerDocumentSymbolProvider(
{language: 'upstart'},
new UpstartSymbolProvider(),
{label: 'ChromiumIDE Upstart'}
);
}
class UpstartSymbolProvider implements vscode.DocumentSymbolProvider {
provideDocumentSymbols(
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.DocumentSymbol[] {
const symbols: vscode.DocumentSymbol[] = [];
for (const extractor of extractors) {
let match: RegExpExecArray | null;
while ((match = extractor.regex.exec(document.getText())) !== null) {
const symbol = new vscode.DocumentSymbol(
extractor.name(match),
extractor.detail(match),
extractor.symbolKind,
extractor.range(match, document),
extractor.selectionRange(match, document)
);
symbols.push(symbol);
}
}
return symbols;
}
}
interface StanzaExtractor {
regex: RegExp;
symbolKind: vscode.SymbolKind;
/** For example, 'script' or variable name. */
name(match: RegExpExecArray): string;
/** Modifier, for example, 'post-exec'. */
detail(match: RegExpExecArray): string;
/** Full range described by a symbol, for example, everything in 'script (...) end script' */
range(match: RegExpExecArray, document: vscode.TextDocument): vscode.Range;
/** Smaller range where a symbol is located. */
selectionRange(
match: RegExpExecArray,
document: vscode.TextDocument
): vscode.Range;
}
function groupRange(
match: RegExpExecArray,
document: vscode.TextDocument,
group: number,
offset?: number
): vscode.Range {
return new vscode.Range(
document.positionAt(match.index + (offset ?? 0)),
document.positionAt(match.index + (offset ?? 0) + match[group].length)
);
}
const extractors: StanzaExtractor[] = [
// env
{
// The regex does not allow leading spaces (they would match
// a different 'env' inside script) and multiple spaces between 'env' and
// the variable name (all platform2 conf files have one space).
// This behavior matches how our current syntax highlighting works.
regex: /^env ([A-Za-z0-9_]+)(=.*)?/gm,
symbolKind: vscode.SymbolKind.Variable,
name(match: RegExpExecArray) {
return match[1];
},
detail(_match: RegExpExecArray) {
return '';
},
range(match: RegExpExecArray, document: vscode.TextDocument) {
// entire line, from env to the assigned value
return groupRange(match, document, 0);
},
selectionRange(match: RegExpExecArray, document: vscode.TextDocument) {
// just the variable name
return groupRange(match, document, 1, 4);
},
},
// script
{
// We need .*? instead of .* to avoid greedy matches,
// which would merge all scripts into one.
regex:
/^((pre-start |post-start |pre-stop |post-stop )?script).*?\nend script/gms,
symbolKind: vscode.SymbolKind.Function,
name(_match: RegExpExecArray) {
return 'script';
},
detail(match: RegExpExecArray) {
return match[2] ? match[2].trim() : '';
},
range(match: RegExpExecArray, document: vscode.TextDocument) {
return groupRange(match, document, 0);
},
selectionRange(match: RegExpExecArray, document: vscode.TextDocument) {
return groupRange(match, document, 1);
},
},
// exec
{
regex: /^((pre-start |post-start |pre-stop |post-stop )?exec).*?[^\\]$/gms,
symbolKind: vscode.SymbolKind.Function,
name(_match: RegExpExecArray) {
return 'exec';
},
detail(match: RegExpExecArray) {
return match[2] ? match[2].trim() : '';
},
range(match: RegExpExecArray, document: vscode.TextDocument) {
return groupRange(match, document, 0);
},
selectionRange(match: RegExpExecArray, document: vscode.TextDocument) {
return groupRange(match, document, 1);
},
},
];
export const TEST_ONLY = {UpstartSymbolProvider};