Skip to content

Commit 08d3a92

Browse files
committed
feat: add speed optimizations like async and lazyload
1 parent de31b9a commit 08d3a92

File tree

1 file changed

+77
-48
lines changed

1 file changed

+77
-48
lines changed

src/extension.ts

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as fs from 'node:fs';
1+
import * as fs from 'node:fs/promises';
22
import * as path from 'node:path';
33
import * as vscode from 'vscode';
44
import { CompletionItem, CompletionItemKind, type Disposable, Hover, MarkdownString } from 'vscode';
@@ -15,22 +15,34 @@ interface IamActionData {
1515
}
1616

1717
class IamActionMappings {
18-
private iamActionsMap: Map<string, IamActionData> = new Map();
19-
private servicePrefixMap: Map<string, string[]> = new Map();
18+
private iamActionsMap: Map<string, IamActionData> | null = null;
19+
private servicePrefixMap: Map<string, Set<string>> | null = null;
20+
private loadingPromise: Promise<void> | null = null;
2021

21-
constructor() {
22-
this.loadIamActionsMap();
22+
private async ensureDataLoaded(): Promise<void> {
23+
if (!this.loadingPromise) {
24+
this.loadingPromise = this.loadIamActionsMap();
25+
}
26+
await this.loadingPromise;
2327
}
2428

25-
private loadIamActionsMap() {
29+
private async loadIamActionsMap(): Promise<void> {
2630
const filePath = path.join(__dirname, '..', 'snippets', 'iam-actions.json');
2731
try {
28-
const rawData = fs.readFileSync(filePath, 'utf8');
32+
const rawData = await fs.readFile(filePath, 'utf8');
2933
const jsonData = JSON.parse(rawData);
3034

35+
this.iamActionsMap = new Map();
36+
this.servicePrefixMap = new Map();
37+
3138
for (const service in jsonData) {
3239
const servicePrefix = jsonData[service].service_prefix;
33-
this.servicePrefixMap.set(servicePrefix, []);
40+
let servicePrefixSet = this.servicePrefixMap.get(servicePrefix);
41+
42+
if (!servicePrefixSet) {
43+
servicePrefixSet = new Set<string>();
44+
this.servicePrefixMap.set(servicePrefix, servicePrefixSet);
45+
}
3446

3547
for (const action in jsonData[service].actions) {
3648
const actionData = jsonData[service].actions[action];
@@ -39,12 +51,7 @@ class IamActionMappings {
3951
condition_keys: actionData.condition_keys || [],
4052
resource_types: actionData.resource_types || [],
4153
});
42-
const servicePrefixActions = this.servicePrefixMap.get(servicePrefix);
43-
if (servicePrefixActions) {
44-
servicePrefixActions.push(actionData.action_name);
45-
} else {
46-
this.servicePrefixMap.set(servicePrefix, [actionData.action_name]);
47-
}
54+
servicePrefixSet.add(actionData.action_name);
4855
}
4956
}
5057

@@ -56,19 +63,38 @@ class IamActionMappings {
5663
}
5764
}
5865

59-
public getIamActionData(action: string): IamActionData | undefined {
60-
return this.iamActionsMap.get(action);
66+
public async getIamActionData(action: string): Promise<IamActionData | undefined> {
67+
await this.ensureDataLoaded();
68+
if (this.iamActionsMap) {
69+
return this.iamActionsMap.get(action);
70+
}
71+
return undefined;
6172
}
6273

63-
public getMatchingActions(wildcardAction: string): string[] {
74+
public async getMatchingActions(wildcardAction: string): Promise<string[]> {
75+
await this.ensureDataLoaded();
76+
77+
if (!this.servicePrefixMap) {
78+
return [];
79+
}
80+
6481
const [servicePrefix, actionPattern] = wildcardAction.split(':');
6582
const regex = new RegExp(`^${actionPattern.replace('*', '.*')}$`);
6683

67-
const serviceActions = this.servicePrefixMap.get(servicePrefix) || [];
68-
return serviceActions.filter((action) => regex.test(action.split(':')[1]));
84+
const serviceActions = this.servicePrefixMap.get(servicePrefix) || new Set<string>();
85+
return Array.from(serviceActions).filter((action) => {
86+
const parts = action.split(':');
87+
return parts.length > 1 && regex.test(parts[1]);
88+
});
6989
}
7090

71-
public getAllIamActions(): string[] {
91+
public async getAllIamActions(): Promise<string[]> {
92+
await this.ensureDataLoaded();
93+
94+
if (!this.iamActionsMap) {
95+
return [];
96+
}
97+
7298
return Array.from(this.iamActionsMap.keys());
7399
}
74100
}
@@ -83,13 +109,13 @@ export function activate(context: vscode.ExtensionContext) {
83109
// Register completion provider
84110
disposable.push(
85111
vscode.languages.registerCompletionItemProvider(['yaml', 'yml', 'json'], {
86-
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
87-
if (isBelowActionKey(document, position)) {
112+
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
113+
if (await isBelowActionKey(document, position)) {
88114
outputChannel.appendLine(`Providing completion items at position: ${position.line}:${position.character}`);
89-
return iamActionMappings
90-
.getAllIamActions()
91-
.map((action) => {
92-
const actionData = iamActionMappings.getIamActionData(action);
115+
const allActions = await iamActionMappings.getAllIamActions();
116+
return Promise.all(
117+
allActions.map(async (action) => {
118+
const actionData = await iamActionMappings.getIamActionData(action);
93119
if (actionData) {
94120
const item = new CompletionItem(action, CompletionItemKind.Value);
95121
item.detail = `IAM Action: ${action.split(':')[1]} (${actionData.access_level})`;
@@ -117,8 +143,8 @@ export function activate(context: vscode.ExtensionContext) {
117143
return item;
118144
}
119145
return null;
120-
})
121-
.filter((item): item is CompletionItem => item !== null);
146+
}),
147+
).then((items) => items.filter((item): item is CompletionItem => item !== null));
122148
}
123149
return undefined;
124150
},
@@ -128,7 +154,7 @@ export function activate(context: vscode.ExtensionContext) {
128154
// Register hover provider
129155
disposable.push(
130156
vscode.languages.registerHoverProvider(['yaml', 'yml', 'json'], {
131-
provideHover(document: vscode.TextDocument, position: vscode.Position) {
157+
async provideHover(document: vscode.TextDocument, position: vscode.Position) {
132158
const actionRegex = /[a-zA-Z0-9]+:[a-zA-Z0-9*]+/;
133159
const range = document.getWordRangeAtPosition(position, actionRegex);
134160

@@ -137,7 +163,7 @@ export function activate(context: vscode.ExtensionContext) {
137163
outputChannel.appendLine(`Providing hover for: ${word}`);
138164

139165
if (word.includes('*')) {
140-
const matchingActions = iamActionMappings.getMatchingActions(word);
166+
const matchingActions = await iamActionMappings.getMatchingActions(word);
141167
if (matchingActions.length > 0) {
142168
const content = new MarkdownString();
143169
content.isTrusted = true;
@@ -146,7 +172,7 @@ export function activate(context: vscode.ExtensionContext) {
146172
content.appendMarkdown('|:-------|:------------|\n');
147173

148174
for (const action of matchingActions) {
149-
const actionData = iamActionMappings.getIamActionData(action);
175+
const actionData = await iamActionMappings.getIamActionData(action);
150176
if (actionData) {
151177
const description = actionData.description.replace(/\n/g, ' ');
152178
const actionColumn = `[${action}](${actionData.url}) (${actionData.access_level})`;
@@ -156,7 +182,7 @@ export function activate(context: vscode.ExtensionContext) {
156182
return new Hover(content);
157183
}
158184
} else {
159-
const actionData = iamActionMappings.getIamActionData(word);
185+
const actionData = await iamActionMappings.getIamActionData(word);
160186
if (actionData) {
161187
const content = new MarkdownString();
162188
content.isTrusted = true;
@@ -205,32 +231,35 @@ export function deactivate() {
205231
outputChannel.appendLine('IAM Action Snippets extension deactivated');
206232
}
207233

208-
function isBelowActionKey(document: vscode.TextDocument, position: vscode.Position): boolean {
234+
async function isBelowActionKey(document: vscode.TextDocument, position: vscode.Position): Promise<boolean> {
209235
const maxLinesUp = 10;
210236
const startLine = Math.max(0, position.line - maxLinesUp);
237+
const text = document.getText(new vscode.Range(startLine, 0, position.line, position.character));
211238

212-
for (let i = position.line; i >= startLine; i--) {
213-
const line = document.lineAt(i).text.trim().toLowerCase();
214-
215-
if (document.languageId === 'json') {
216-
if (line.includes('"action":') && line.includes('[')) {
217-
return true;
218-
}
219-
if (line.includes(']')) break;
220-
} else if (document.languageId === 'yaml' || document.languageId === 'yml') {
239+
if (document.languageId === 'json') {
240+
return /"action":\s*\[/.test(text) && !/\]/.test(text.split('"action":')[1]);
241+
}
242+
if (document.languageId === 'yaml' || document.languageId === 'yml') {
243+
const lines = text.split('\n').reverse();
244+
for (const line of lines) {
245+
const trimmedLine = line.trim().toLowerCase();
221246
if (
222-
line.startsWith('action:') ||
223-
line.startsWith('- action:') ||
224-
line.startsWith('notaction:') ||
225-
line.startsWith('- notaction:')
247+
trimmedLine.startsWith('action:') ||
248+
trimmedLine.startsWith('- action:') ||
249+
trimmedLine.startsWith('notaction:') ||
250+
trimmedLine.startsWith('- notaction:')
226251
) {
227252
return true;
228253
}
229-
if (line !== '' && !line.startsWith('-') && !line.startsWith('- ') && !line.startsWith('#')) {
254+
if (
255+
trimmedLine !== '' &&
256+
!trimmedLine.startsWith('-') &&
257+
!trimmedLine.startsWith('- ') &&
258+
!trimmedLine.startsWith('#')
259+
) {
230260
break;
231261
}
232262
}
233263
}
234-
235264
return false;
236265
}

0 commit comments

Comments
 (0)