aero-vscode 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscodeignore +13 -0
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/aero-vscode-0.0.1.vsix +0 -0
- package/dist/extension.js +19 -0
- package/images/logo.png +0 -0
- package/package.json +98 -0
- package/src/__tests__/analyzer.test.ts +202 -0
- package/src/__tests__/diagnostics.test.ts +964 -0
- package/src/__tests__/providers.test.ts +292 -0
- package/src/__tests__/utils.test.ts +120 -0
- package/src/analyzer.ts +914 -0
- package/src/completionProvider.ts +328 -0
- package/src/constants.ts +35 -0
- package/src/definitionProvider.ts +371 -0
- package/src/diagnostics.ts +732 -0
- package/src/extension.ts +74 -0
- package/src/hoverProvider.ts +134 -0
- package/src/pathResolver.ts +171 -0
- package/src/positionAt.ts +509 -0
- package/src/scope.ts +116 -0
- package/src/utils.ts +56 -0
- package/syntaxes/aero-attributes.json +54 -0
- package/syntaxes/aero-expressions.json +26 -0
- package/syntaxes/aero-globals.json +22 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for VS Code language providers: AeroCompletionProvider, AeroHoverProvider,
|
|
3
|
+
* AeroDefinitionProvider. Mocks vscode (Range, Position, workspace, languages, etc.),
|
|
4
|
+
* pathResolver (getResolver), and scope (isAeroDocument, getScopeMode). Asserts completion
|
|
5
|
+
* items, hover null for non-Aero docs, and definition locations for content globals and components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
9
|
+
|
|
10
|
+
const mockSet = vi.fn()
|
|
11
|
+
const mockCollection = {
|
|
12
|
+
set: mockSet,
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
dispose: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
vi.mock('vscode', () => {
|
|
18
|
+
return {
|
|
19
|
+
Range: class {
|
|
20
|
+
start: any
|
|
21
|
+
end: any
|
|
22
|
+
constructor(start: any, end: any) {
|
|
23
|
+
this.start = start
|
|
24
|
+
this.end = end
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
Position: class {
|
|
28
|
+
line: any
|
|
29
|
+
character: any
|
|
30
|
+
constructor(line: any, character: any) {
|
|
31
|
+
this.line = line
|
|
32
|
+
this.character = character
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
Diagnostic: class {
|
|
36
|
+
range: any
|
|
37
|
+
message: any
|
|
38
|
+
severity: any
|
|
39
|
+
tags: any[]
|
|
40
|
+
constructor(range: any, message: any, severity: any) {
|
|
41
|
+
this.range = range
|
|
42
|
+
this.message = message
|
|
43
|
+
this.severity = severity
|
|
44
|
+
this.tags = []
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
|
|
48
|
+
DiagnosticTag: { Unnecessary: 1 },
|
|
49
|
+
workspace: {
|
|
50
|
+
onDidOpenTextDocument: vi.fn(),
|
|
51
|
+
onDidSaveTextDocument: vi.fn(),
|
|
52
|
+
onDidChangeTextDocument: vi.fn(),
|
|
53
|
+
onDidCloseTextDocument: vi.fn(),
|
|
54
|
+
textDocuments: [],
|
|
55
|
+
getWorkspaceFolder: vi.fn(() => ({ uri: { fsPath: '/workspace' } })),
|
|
56
|
+
getConfiguration: () => ({ get: () => 'always' }),
|
|
57
|
+
},
|
|
58
|
+
languages: {
|
|
59
|
+
createDiagnosticCollection: () => mockCollection,
|
|
60
|
+
registerDefinitionProvider: vi.fn(),
|
|
61
|
+
registerCompletionItemProvider: vi.fn(),
|
|
62
|
+
registerHoverProvider: vi.fn(),
|
|
63
|
+
},
|
|
64
|
+
Uri: {
|
|
65
|
+
parse: (s: string) => ({ toString: () => s, fsPath: s, scheme: 'file' }),
|
|
66
|
+
file: (s: string) => ({ fsPath: s, scheme: 'file' }),
|
|
67
|
+
},
|
|
68
|
+
CompletionItem: class {
|
|
69
|
+
label: string
|
|
70
|
+
kind: number
|
|
71
|
+
constructor(label: string, kind: number) {
|
|
72
|
+
this.label = label
|
|
73
|
+
this.kind = kind
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
CompletionItemKind: {
|
|
77
|
+
Property: 6,
|
|
78
|
+
Keyword: 13,
|
|
79
|
+
Class: 6,
|
|
80
|
+
Struct: 22,
|
|
81
|
+
Folder: 17,
|
|
82
|
+
File: 1,
|
|
83
|
+
Module: 8,
|
|
84
|
+
Variable: 5,
|
|
85
|
+
},
|
|
86
|
+
SnippetString: class {
|
|
87
|
+
value: string
|
|
88
|
+
constructor(value: string) {
|
|
89
|
+
this.value = value
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
MarkdownString: class {
|
|
93
|
+
value: string
|
|
94
|
+
constructor(value?: string) {
|
|
95
|
+
this.value = value || ''
|
|
96
|
+
}
|
|
97
|
+
appendMarkdown(val: string) {
|
|
98
|
+
this.value += val
|
|
99
|
+
}
|
|
100
|
+
appendCodeblock(val: string, lang?: string) {
|
|
101
|
+
this.value += '```' + (lang || '') + '\n' + val + '\n```\n'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
vi.mock('../pathResolver', () => ({
|
|
108
|
+
getResolver: vi.fn(() => ({
|
|
109
|
+
root: '/workspace',
|
|
110
|
+
resolve: vi.fn((specifier: string) => {
|
|
111
|
+
if (specifier.startsWith('@components/')) {
|
|
112
|
+
return '/workspace/client/components/' + specifier.replace('@components/', '') + '.html'
|
|
113
|
+
}
|
|
114
|
+
if (specifier.startsWith('@layouts/')) {
|
|
115
|
+
return '/workspace/client/layouts/' + specifier.replace('@layouts/', '') + '.html'
|
|
116
|
+
}
|
|
117
|
+
if (specifier.startsWith('@content/')) {
|
|
118
|
+
return '/workspace/client/content/' + specifier.replace('@content/', '') + '.ts'
|
|
119
|
+
}
|
|
120
|
+
return '/workspace/' + specifier
|
|
121
|
+
}),
|
|
122
|
+
})),
|
|
123
|
+
clearResolverCache: vi.fn(),
|
|
124
|
+
}))
|
|
125
|
+
|
|
126
|
+
vi.mock('../scope', () => ({
|
|
127
|
+
isAeroDocument: vi.fn(() => true),
|
|
128
|
+
getScopeMode: vi.fn(() => 'auto'),
|
|
129
|
+
clearScopeCache: vi.fn(),
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
import { AeroCompletionProvider } from '../completionProvider'
|
|
133
|
+
import { AeroHoverProvider } from '../hoverProvider'
|
|
134
|
+
import { AeroDefinitionProvider } from '../definitionProvider'
|
|
135
|
+
|
|
136
|
+
/** Completions: after < (tags), inside tag (attrs), inside { } (content globals), import paths. */
|
|
137
|
+
describe('AeroCompletionProvider', () => {
|
|
138
|
+
let provider: AeroCompletionProvider
|
|
139
|
+
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
vi.clearAllMocks()
|
|
142
|
+
provider = new AeroCompletionProvider()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should provide component tag completions after <', async () => {
|
|
146
|
+
const doc = {
|
|
147
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
148
|
+
getText: () => '<',
|
|
149
|
+
lineAt: (line: number) => ({ text: '<' }),
|
|
150
|
+
} as any
|
|
151
|
+
|
|
152
|
+
const position = { line: 0, character: 1 } as any
|
|
153
|
+
const context = { triggerCharacter: '<', isIncomplete: false } as any
|
|
154
|
+
|
|
155
|
+
const result = provider.provideCompletionItems(doc, position, {} as any, context)
|
|
156
|
+
|
|
157
|
+
expect(result).not.toBeNull()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should provide attribute completions inside a tag', async () => {
|
|
161
|
+
const doc = {
|
|
162
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
163
|
+
getText: () => '<div ',
|
|
164
|
+
lineAt: (line: number) => ({ text: '<div ' }),
|
|
165
|
+
} as any
|
|
166
|
+
|
|
167
|
+
const position = { line: 0, character: 5 } as any
|
|
168
|
+
const context = {} as any
|
|
169
|
+
|
|
170
|
+
const result = provider.provideCompletionItems(doc, position, {} as any, context)
|
|
171
|
+
|
|
172
|
+
expect(result).not.toBeNull()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should provide content global completions inside expression', async () => {
|
|
176
|
+
const doc = {
|
|
177
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
178
|
+
getText: () => '<div>{',
|
|
179
|
+
lineAt: (line: number) => ({ text: '<div>{|' }),
|
|
180
|
+
} as any
|
|
181
|
+
|
|
182
|
+
const position = { line: 0, character: 6 } as any
|
|
183
|
+
const context = {} as any
|
|
184
|
+
|
|
185
|
+
const result = provider.provideCompletionItems(doc, position, {} as any, context)
|
|
186
|
+
|
|
187
|
+
expect(result).not.toBeNull()
|
|
188
|
+
const items = result as any[]
|
|
189
|
+
const labels = items.map((i: any) => i.label)
|
|
190
|
+
expect(labels).toContain('site')
|
|
191
|
+
expect(labels).toContain('theme')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should provide import path completions', async () => {
|
|
195
|
+
const doc = {
|
|
196
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
197
|
+
getText: () => "import foo from '@components",
|
|
198
|
+
lineAt: (line: number) => ({ text: "import foo from '@components" }),
|
|
199
|
+
} as any
|
|
200
|
+
|
|
201
|
+
const position = { line: 0, character: 17 } as any // after '@components'
|
|
202
|
+
|
|
203
|
+
const result = provider.provideCompletionItems(doc, position, {} as any, {} as any)
|
|
204
|
+
|
|
205
|
+
expect(result).not.toBeNull()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
/** Hover: returns null when isAeroDocument is false; otherwise hover content for symbols. */
|
|
210
|
+
describe('AeroHoverProvider', () => {
|
|
211
|
+
let provider: AeroHoverProvider
|
|
212
|
+
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
vi.clearAllMocks()
|
|
215
|
+
provider = new AeroHoverProvider()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should return null for non-Aero documents', async () => {
|
|
219
|
+
const { isAeroDocument } = await import('../scope')
|
|
220
|
+
;(isAeroDocument as any).mockReturnValueOnce(false)
|
|
221
|
+
|
|
222
|
+
const doc = {
|
|
223
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
224
|
+
getText: () => '<div></div>',
|
|
225
|
+
lineAt: (line: number) => ({ text: '<div></div>' }),
|
|
226
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
227
|
+
} as any
|
|
228
|
+
|
|
229
|
+
const position = { line: 0, character: 2 } as any
|
|
230
|
+
const result = await provider.provideHover(doc, position, {} as any)
|
|
231
|
+
|
|
232
|
+
expect(result).toBeNull()
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
/** Go-to-definition: null for non-Aero docs; content global (site) and component tags resolve to file locations. */
|
|
237
|
+
describe('AeroDefinitionProvider', () => {
|
|
238
|
+
let provider: AeroDefinitionProvider
|
|
239
|
+
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
vi.clearAllMocks()
|
|
242
|
+
provider = new AeroDefinitionProvider()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should return null for non-Aero documents', async () => {
|
|
246
|
+
const { isAeroDocument } = await import('../scope')
|
|
247
|
+
;(isAeroDocument as any).mockReturnValueOnce(false)
|
|
248
|
+
|
|
249
|
+
const doc = {
|
|
250
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
251
|
+
getText: () => '<div></div>',
|
|
252
|
+
lineAt: (line: number) => ({ text: '<div></div>' }),
|
|
253
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
254
|
+
} as any
|
|
255
|
+
|
|
256
|
+
const position = { line: 0, character: 2 } as any
|
|
257
|
+
const result = await provider.provideDefinition(doc, position, {} as any)
|
|
258
|
+
|
|
259
|
+
expect(result).toBeNull()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should provide definition for content global', async () => {
|
|
263
|
+
const doc = {
|
|
264
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
265
|
+
getText: () => '<div>{site.title}</div>',
|
|
266
|
+
lineAt: (line: number) => ({ text: '<div>{site.title}</div>' }),
|
|
267
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
268
|
+
offsetAt: (pos: any) => pos.character,
|
|
269
|
+
} as any
|
|
270
|
+
|
|
271
|
+
const position = { line: 0, character: 7 } as any // on 'site'
|
|
272
|
+
const result = await provider.provideDefinition(doc, position, {} as any)
|
|
273
|
+
|
|
274
|
+
expect(result).not.toBeNull()
|
|
275
|
+
expect(result?.length).toBeGreaterThan(0)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should provide definition for component tag', async () => {
|
|
279
|
+
const doc = {
|
|
280
|
+
uri: { toString: () => 'file:///test.html', fsPath: '/test.html' },
|
|
281
|
+
getText: () => '<header-component></header-component>',
|
|
282
|
+
lineAt: (line: number) => ({ text: '<header-component></header-component>' }),
|
|
283
|
+
positionAt: (offset: number) => ({ line: 0, character: offset }),
|
|
284
|
+
} as any
|
|
285
|
+
|
|
286
|
+
const position = { line: 0, character: 1 } as any // on 'header'
|
|
287
|
+
const result = await provider.provideDefinition(doc, position, {} as any)
|
|
288
|
+
|
|
289
|
+
expect(result).not.toBeNull()
|
|
290
|
+
expect(result?.length).toBeGreaterThan(0)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for scope.ts (isAeroDocument, getScopeMode) and pathResolver.ts (getResolver,
|
|
3
|
+
* clearResolverCache). Mocks vscode workspace/Uri and node:fs so workspace detection and
|
|
4
|
+
* config file presence are under test control.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
8
|
+
|
|
9
|
+
vi.mock('vscode', () => {
|
|
10
|
+
return {
|
|
11
|
+
workspace: {
|
|
12
|
+
getWorkspaceFolder: vi.fn((uri: any) => {
|
|
13
|
+
if (uri?.fsPath?.startsWith('/workspace')) {
|
|
14
|
+
return { uri: { fsPath: '/workspace' } }
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}),
|
|
18
|
+
getConfiguration: vi.fn(() => ({ get: vi.fn(() => 'auto') })),
|
|
19
|
+
},
|
|
20
|
+
Uri: {
|
|
21
|
+
file: (s: string) => ({ fsPath: s, scheme: 'file' }),
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
vi.mock('node:fs', () => ({
|
|
27
|
+
existsSync: vi.fn((path: string) => {
|
|
28
|
+
if (
|
|
29
|
+
path.includes('vite.config.ts') ||
|
|
30
|
+
path.includes('package.json') ||
|
|
31
|
+
path.includes('tsconfig.json')
|
|
32
|
+
) {
|
|
33
|
+
if (path.includes('aero') || path.includes('workspace')) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}),
|
|
39
|
+
readFileSync: vi.fn((path: string) => {
|
|
40
|
+
if (path.includes('vite.config.ts')) {
|
|
41
|
+
return "import { aero } from '@aerobuilt/core'"
|
|
42
|
+
}
|
|
43
|
+
if (path.includes('package.json')) {
|
|
44
|
+
return '{ "name": "test-project" }'
|
|
45
|
+
}
|
|
46
|
+
return '{}'
|
|
47
|
+
}),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
describe('scope', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** Aero documents: file scheme + HTML languageId; untitled and non-HTML are excluded. */
|
|
56
|
+
describe('isAeroDocument', () => {
|
|
57
|
+
it('should return false for non-HTML documents', async () => {
|
|
58
|
+
const { isAeroDocument } = await import('../scope')
|
|
59
|
+
|
|
60
|
+
const doc = {
|
|
61
|
+
languageId: 'javascript',
|
|
62
|
+
uri: { scheme: 'file', fsPath: '/test.js' },
|
|
63
|
+
} as any
|
|
64
|
+
|
|
65
|
+
expect(isAeroDocument(doc)).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return false for non-file URIs', async () => {
|
|
69
|
+
const { isAeroDocument } = await import('../scope')
|
|
70
|
+
|
|
71
|
+
const doc = {
|
|
72
|
+
languageId: 'html',
|
|
73
|
+
uri: { scheme: 'untitled', fsPath: '/test.html' },
|
|
74
|
+
} as any
|
|
75
|
+
|
|
76
|
+
expect(isAeroDocument(doc)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/** Scope mode from workspace config (e.g. 'auto', 'always'); mock returns 'auto'. */
|
|
81
|
+
describe('getScopeMode', () => {
|
|
82
|
+
it('should return default mode as auto', async () => {
|
|
83
|
+
const { getScopeMode } = await import('../scope')
|
|
84
|
+
|
|
85
|
+
expect(getScopeMode()).toBe('auto')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('pathResolver', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.clearAllMocks()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/** Resolver is built from document's workspace folder; root and resolve() are used by providers. */
|
|
96
|
+
describe('getResolver', () => {
|
|
97
|
+
it('should return a resolver object with root and resolve method', async () => {
|
|
98
|
+
const { getResolver, clearResolverCache } = await import('../pathResolver')
|
|
99
|
+
clearResolverCache()
|
|
100
|
+
|
|
101
|
+
const doc = {
|
|
102
|
+
uri: { fsPath: '/workspace/test.html' },
|
|
103
|
+
} as any
|
|
104
|
+
|
|
105
|
+
const resolver = getResolver(doc)
|
|
106
|
+
|
|
107
|
+
expect(resolver).toBeDefined()
|
|
108
|
+
expect(resolver!.root).toBe('/workspace')
|
|
109
|
+
expect(typeof resolver!.resolve).toBe('function')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('clearResolverCache', () => {
|
|
114
|
+
it('should be exported function', async () => {
|
|
115
|
+
const { clearResolverCache } = await import('../pathResolver')
|
|
116
|
+
|
|
117
|
+
expect(typeof clearResolverCache).toBe('function')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|