Skip to content

Commit 0a0afbd

Browse files
committed
Add impl
1 parent 2583043 commit 0a0afbd

File tree

5 files changed

+228
-22
lines changed

5 files changed

+228
-22
lines changed

src/Contract.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const preInsertedScript =
2+
`export default async ({ context, core, exec, github, glob, io, require }: import('github-script').AsyncFunctionArguments) => {
3+
` as const;
4+
export const postInsertedScript = `}` as const;
5+
6+
export const triggerTextYamlToTs = "#```typescript" as const;
7+
export const triggerTextTsToYaml = "#```" as const;

src/LanguageService.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as fs from "fs";
2+
import path from "path";
3+
import * as ts from "typescript";
4+
5+
const scriptSnapshots: { [fileName: string]: ts.IScriptSnapshot } = {};
6+
const scriptVersions: { [fileName: string]: string } = {};
7+
8+
const extensionRootDir = path.join(__dirname, "..");
9+
10+
export function createLanguageService() {
11+
const compilerOptions: ts.CompilerOptions = {
12+
target: ts.ScriptTarget.ESNext,
13+
module: ts.ModuleKind.ESNext,
14+
lib: ["esnext"],
15+
typeRoots: [
16+
path.join(extensionRootDir, "node_modules", "@types"),
17+
path.join(extensionRootDir, "node_modules"),
18+
],
19+
};
20+
21+
const host: ts.LanguageServiceHost = {
22+
getScriptFileNames: () => Object.keys(scriptSnapshots),
23+
getScriptVersion: (fileName) => scriptVersions[fileName],
24+
getScriptSnapshot: (fileName) => {
25+
if (scriptSnapshots[fileName]) {
26+
return scriptSnapshots[fileName];
27+
}
28+
if (fs.existsSync(fileName)) {
29+
const content = fs.readFileSync(fileName, "utf8");
30+
const snapshot = ts.ScriptSnapshot.fromString(content);
31+
scriptSnapshots[fileName] = snapshot;
32+
scriptVersions[fileName] = "1";
33+
return snapshot;
34+
}
35+
return undefined;
36+
},
37+
getCurrentDirectory: () => process.cwd(),
38+
getCompilationSettings: () => compilerOptions,
39+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
40+
readFile: (fileName) => {
41+
if (scriptSnapshots[fileName]) {
42+
return scriptSnapshots[fileName].getText(
43+
0,
44+
scriptSnapshots[fileName].getLength()
45+
);
46+
}
47+
return fs.readFileSync(fileName, "utf8");
48+
},
49+
fileExists: (fileName) => {
50+
return !!scriptSnapshots[fileName] || fs.existsSync(fileName);
51+
},
52+
};
53+
54+
return ts.createLanguageService(host);
55+
}
56+
57+
export function updateScript(fileName: string, content: string) {
58+
scriptSnapshots[fileName] = ts.ScriptSnapshot.fromString(content);
59+
scriptVersions[fileName] = (
60+
parseInt(scriptVersions[fileName] || "0", 10) + 1
61+
).toString();
62+
}

src/Logger.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as vscode from "vscode";
2+
3+
export const logger = vscode.window.createOutputChannel(
4+
"Workflow Script Highlighter",
5+
{ log: true }
6+
);

src/Platform.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (C) 2018 TypeFox and others.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*/
7+
8+
import * as ts from "typescript";
9+
import * as vscode from "vscode";
10+
11+
// https://github.com/typescript-language-server/typescript-language-server/blob/184c60de3308621380469d6632bdff2e10f672fd/src/completion.ts#L271
12+
export function asCompletionItemKind(
13+
kind: ts.ScriptElementKind
14+
): vscode.CompletionItemKind {
15+
switch (kind) {
16+
case ts.ScriptElementKind.primitiveType:
17+
case ts.ScriptElementKind.keyword:
18+
return vscode.CompletionItemKind.Keyword;
19+
case ts.ScriptElementKind.constElement:
20+
case ts.ScriptElementKind.letElement:
21+
case ts.ScriptElementKind.variableElement:
22+
case ts.ScriptElementKind.localVariableElement:
23+
case ts.ScriptElementKind.alias:
24+
case ts.ScriptElementKind.parameterElement:
25+
return vscode.CompletionItemKind.Variable;
26+
case ts.ScriptElementKind.memberVariableElement:
27+
case ts.ScriptElementKind.memberGetAccessorElement:
28+
case ts.ScriptElementKind.memberSetAccessorElement:
29+
return vscode.CompletionItemKind.Field;
30+
case ts.ScriptElementKind.functionElement:
31+
case ts.ScriptElementKind.localFunctionElement:
32+
return vscode.CompletionItemKind.Function;
33+
case ts.ScriptElementKind.memberFunctionElement:
34+
case ts.ScriptElementKind.constructSignatureElement:
35+
case ts.ScriptElementKind.callSignatureElement:
36+
case ts.ScriptElementKind.indexSignatureElement:
37+
return vscode.CompletionItemKind.Method;
38+
case ts.ScriptElementKind.enumElement:
39+
return vscode.CompletionItemKind.Enum;
40+
case ts.ScriptElementKind.enumMemberElement:
41+
return vscode.CompletionItemKind.EnumMember;
42+
case ts.ScriptElementKind.moduleElement:
43+
case ts.ScriptElementKind.externalModuleName:
44+
return vscode.CompletionItemKind.Module;
45+
case ts.ScriptElementKind.classElement:
46+
case ts.ScriptElementKind.typeElement:
47+
return vscode.CompletionItemKind.Class;
48+
case ts.ScriptElementKind.interfaceElement:
49+
return vscode.CompletionItemKind.Interface;
50+
case ts.ScriptElementKind.warning:
51+
return vscode.CompletionItemKind.Text;
52+
case ts.ScriptElementKind.scriptElement:
53+
return vscode.CompletionItemKind.File;
54+
case ts.ScriptElementKind.directory:
55+
return vscode.CompletionItemKind.Folder;
56+
case ts.ScriptElementKind.string:
57+
return vscode.CompletionItemKind.Constant;
58+
}
59+
return vscode.CompletionItemKind.Property;
60+
}

src/extension.ts

Lines changed: 93 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,101 @@
1-
// The module 'vscode' contains the VS Code extensibility API
2-
// Import the module and reference it with the alias vscode in your code below
1+
import * as ts from "typescript";
32
import * as vscode from "vscode";
3+
import {
4+
postInsertedScript,
5+
preInsertedScript,
6+
triggerTextTsToYaml,
7+
triggerTextYamlToTs,
8+
} from "./Contract";
9+
import { logger } from "./Logger";
10+
import { createLanguageService, updateScript } from "./LanguageService";
11+
import { asCompletionItemKind } from "./Platform";
412

5-
// This method is called when your extension is activated
6-
// Your extension is activated the very first time the command is executed
7-
export function activate(context: vscode.ExtensionContext) {
8-
// Use the console to output diagnostic information (console.log) and errors (console.error)
9-
// This line of code will only be executed once when your extension is activated
10-
console.log(
11-
'Congratulations, your extension "workflow-script-highlighter" is now active!'
13+
// For debugging
14+
logger.hide();
15+
16+
let languageService: ts.LanguageService;
17+
18+
function extractTsCodeBlock(
19+
text: string,
20+
offset: number
21+
): { content: string; offset: number } | null {
22+
const tsBlockStart = text.lastIndexOf(triggerTextYamlToTs, offset);
23+
const tsBlockEnd = text.indexOf(
24+
triggerTextTsToYaml,
25+
tsBlockStart + triggerTextYamlToTs.length
1226
);
1327

14-
// The command has been defined in the package.json file
15-
// Now provide the implementation of the command with registerCommand
16-
// The commandId parameter must match the command field in package.json
17-
const disposable = vscode.commands.registerCommand(
18-
"workflow-script-highlighter.helloWorld",
19-
() => {
20-
// The code you place here will be executed every time your command is executed
21-
// Display a message box to the user
22-
vscode.window.showInformationMessage("Hello World from !");
23-
}
28+
if (tsBlockStart === -1 || tsBlockEnd === -1 || tsBlockEnd < offset) {
29+
return null;
30+
}
31+
32+
const content = text.substring(
33+
tsBlockStart + triggerTextYamlToTs.length,
34+
tsBlockEnd - postInsertedScript.length
2435
);
36+
const tsOffset = offset - (tsBlockStart + triggerTextYamlToTs.length);
37+
38+
return { content, offset: tsOffset };
39+
}
40+
41+
export function activate(context: vscode.ExtensionContext) {
42+
languageService = createLanguageService();
43+
44+
const completionItemProvider: vscode.CompletionItemProvider = {
45+
provideCompletionItems(
46+
document: vscode.TextDocument,
47+
position: vscode.Position
48+
) {
49+
const text = document.getText();
50+
const offset = document.offsetAt(position);
2551

26-
context.subscriptions.push(disposable);
52+
const tsCodeBlock = extractTsCodeBlock(text, offset);
53+
if (!tsCodeBlock) {
54+
return [];
55+
}
56+
57+
const scriptFileName = document.uri.fsPath + ".ts";
58+
const augmentedContent = `${preInsertedScript}${tsCodeBlock.content}${postInsertedScript}`;
59+
updateScript(scriptFileName, augmentedContent);
60+
61+
const updatedPosition = tsCodeBlock.offset + preInsertedScript.length;
62+
63+
const completions = languageService.getCompletionsAtPosition(
64+
scriptFileName,
65+
updatedPosition,
66+
{}
67+
);
68+
if (!completions) {
69+
return [];
70+
}
71+
72+
// TODO: More powerful completion
73+
const mapped = completions.entries.map((entry) => {
74+
const item = new vscode.CompletionItem(
75+
entry.name,
76+
asCompletionItemKind(entry.kind)
77+
);
78+
item.sortText = entry.sortText;
79+
80+
return item;
81+
});
82+
83+
return mapped;
84+
},
85+
};
86+
87+
const githubActionsWorkflowProvider =
88+
vscode.languages.registerCompletionItemProvider(
89+
{ language: "github-actions-workflow", scheme: "file" },
90+
completionItemProvider,
91+
"."
92+
);
93+
94+
context.subscriptions.push(githubActionsWorkflowProvider);
2795
}
2896

29-
// This method is called when your extension is deactivated
30-
export function deactivate() {}
97+
export function deactivate() {
98+
if (languageService) {
99+
languageService.dispose();
100+
}
101+
}

0 commit comments

Comments
 (0)