Skip to content

Commit 230d0b4

Browse files
Add Hover for output section fields
1 parent 4364e5c commit 230d0b4

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export const outputSectionFieldDocsMap = getOutputSectionFieldDocsMap();
2+
3+
function getOutputSectionFieldDocsMap(): Map<string, string> {
4+
const outputSectionFieldDocsMap = new Map<string, string>();
5+
6+
outputSectionFieldDocsMap.set(
7+
'Description',
8+
[
9+
'**Description (optional)**',
10+
'\n',
11+
'---',
12+
'A `String` type that describes the output value. ',
13+
"The value for the description declaration must be a literal string that's between 0 and 1024 bytes in length. ",
14+
"You can't use a parameter or function to specify the description. ",
15+
'\n',
16+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)',
17+
].join('\n'),
18+
);
19+
20+
outputSectionFieldDocsMap.set(
21+
'Value',
22+
[
23+
'**Value (required)**',
24+
'\n',
25+
'---',
26+
'The value of the property returned by the [describe-stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/service_code_examples.html#describe-stacks-sdk) command. ',
27+
'The value of an output can include literals, parameter references, pseudo parameters, a mapping value, or intrinsic functions. ',
28+
'\n',
29+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)',
30+
].join('\n'),
31+
);
32+
33+
outputSectionFieldDocsMap.set(
34+
'Export',
35+
[
36+
'**Export (optional)**',
37+
'\n',
38+
'---',
39+
'The name of the resource output to be exported for a cross-stack reference.',
40+
'\n',
41+
'You can use intrinsic functions to customize the Name value of an export. ',
42+
'\n',
43+
'For more information, see [Get exported outputs from a deployed CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html). ',
44+
'\n',
45+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)',
46+
].join('\n'),
47+
);
48+
49+
return outputSectionFieldDocsMap;
50+
}

src/hover/HoverRouter.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { HoverProvider } from './HoverProvider';
1313
import { IntrinsicFunctionArgumentHoverProvider } from './IntrinsicFunctionArgumentHoverProvider';
1414
import { IntrinsicFunctionHoverProvider } from './IntrinsicFunctionHoverProvider';
1515
import { MappingHoverProvider } from './MappingHoverProvider';
16+
import { OutputSectionFieldHoverProvider } from './OutputSectionFieldHoverProvider';
1617
import { ParameterAttributeHoverProvider } from './ParameterAttributeHoverProvider';
1718
import { ParameterHoverProvider } from './ParameterHoverProvider';
1819
import { PseudoParameterHoverProvider } from './PseudoParameterHoverProvider';
@@ -101,6 +102,11 @@ export class HoverRouter implements Configurable, Closeable {
101102
if (doc) {
102103
return doc;
103104
}
105+
} else if (context.section === TopLevelSection.Outputs && this.isOutputAttribute(context)) {
106+
const doc = this.hoverProviderMap.get(HoverType.OutputSectionField)?.getInformation(context);
107+
if (doc) {
108+
return doc;
109+
}
104110
}
105111

106112
return this.getTopLevelReference(context);
@@ -112,6 +118,7 @@ export class HoverRouter implements Configurable, Closeable {
112118
hoverProviderMap.set(HoverType.ResourceSection, new ResourceSectionHoverProvider(schemaRetriever));
113119
hoverProviderMap.set(HoverType.Parameter, new ParameterHoverProvider());
114120
hoverProviderMap.set(HoverType.ParameterAttribute, new ParameterAttributeHoverProvider());
121+
hoverProviderMap.set(HoverType.OutputSectionField, new OutputSectionFieldHoverProvider());
115122
hoverProviderMap.set(HoverType.PseudoParameter, new PseudoParameterHoverProvider());
116123
hoverProviderMap.set(HoverType.Condition, new ConditionHoverProvider());
117124
hoverProviderMap.set(HoverType.Mapping, new MappingHoverProvider());
@@ -128,6 +135,14 @@ export class HoverRouter implements Configurable, Closeable {
128135
return ParameterAttributeHoverProvider.isParameterAttribute(context.text);
129136
}
130137

138+
private isOutputAttribute(context: Context): boolean {
139+
if (context.section !== TopLevelSection.Outputs) {
140+
return false;
141+
}
142+
143+
return OutputSectionFieldHoverProvider.isOutputSectionField(context.text);
144+
}
145+
131146
private getTopLevelReference(context: ContextWithRelatedEntities): string | undefined {
132147
for (const section of context.relatedEntities.values()) {
133148
const relatedContext = section.get(context.text);
@@ -170,6 +185,7 @@ enum HoverType {
170185
IntrinsicFunctionArgument,
171186
Parameter,
172187
ParameterAttribute,
188+
OutputSectionField,
173189
PseudoParameter,
174190
Condition,
175191
Mapping,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { outputSectionFieldDocsMap } from '../artifacts/OutputSectionFieldDocs';
2+
import { Context } from '../context/Context';
3+
import { HoverProvider } from './HoverProvider';
4+
5+
export class OutputSectionFieldHoverProvider implements HoverProvider {
6+
private static readonly OUTPUT_SECTION_FIELDS = new Set(['Description', 'Value', 'Export']);
7+
8+
getInformation(context: Context): string | undefined {
9+
const attributeName = context.text;
10+
11+
if (!OutputSectionFieldHoverProvider.OUTPUT_SECTION_FIELDS.has(attributeName)) {
12+
return undefined;
13+
}
14+
15+
return outputSectionFieldDocsMap.get(attributeName);
16+
}
17+
18+
static isOutputSectionField(text: string): boolean {
19+
return OutputSectionFieldHoverProvider.OUTPUT_SECTION_FIELDS.has(text);
20+
}
21+
}

tst/e2e/hover/Hover.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22
import { intrinsicFunctionsDocsMap } from '../../../src/artifacts/IntrinsicFunctionsDocs';
3+
import { outputSectionFieldDocsMap } from '../../../src/artifacts/OutputSectionFieldDocs';
34
import { parameterAttributeDocsMap } from '../../../src/artifacts/ParameterAttributeDocs';
45
import { pseudoParameterDocsMap } from '../../../src/artifacts/PseudoParameterDocs';
56
import { resourceAttributeDocsMap } from '../../../src/artifacts/ResourceAttributeDocs';
@@ -1474,6 +1475,30 @@ Outputs:`,
14741475
.build(),
14751476
},
14761477
},
1478+
{
1479+
action: 'type',
1480+
content: ``,
1481+
position: { line: 408, character: 18 },
1482+
description: 'Test Hover for Description output section field',
1483+
verification: {
1484+
position: { line: 407, character: 8 },
1485+
expectation: HoverExpectationBuilder.create()
1486+
.expectContent(outputSectionFieldDocsMap.get('Description'))
1487+
.build(),
1488+
},
1489+
},
1490+
{
1491+
action: 'type',
1492+
content: ``,
1493+
position: { line: 408, character: 18 },
1494+
description: 'Test Hover for Value output section field',
1495+
verification: {
1496+
position: { line: 408, character: 7 },
1497+
expectation: HoverExpectationBuilder.create()
1498+
.expectContent(outputSectionFieldDocsMap.get('Value'))
1499+
.build(),
1500+
},
1501+
},
14771502
{
14781503
action: 'type',
14791504
content: ` VPC.CidrBlock
@@ -1492,6 +1517,18 @@ Outputs:`,
14921517
.build(),
14931518
},
14941519
},
1520+
{
1521+
action: 'type',
1522+
content: ``,
1523+
position: { line: 414, character: 18 },
1524+
description: 'Test Hover for Export output section field',
1525+
verification: {
1526+
position: { line: 409, character: 7 },
1527+
expectation: HoverExpectationBuilder.create()
1528+
.expectContent(outputSectionFieldDocsMap.get('Export'))
1529+
.build(),
1530+
},
1531+
},
14951532
{
14961533
action: 'type',
14971534
content: ` Database.Endpoint.Address
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { TopLevelSection } from '../../../src/context/ContextType';
3+
import { OutputSectionFieldHoverProvider } from '../../../src/hover/OutputSectionFieldHoverProvider';
4+
import { createMockContext } from '../../utils/MockContext';
5+
6+
describe('OutputSectionFieldHoverProvider', () => {
7+
const outputSectionFieldHoverProvider = new OutputSectionFieldHoverProvider();
8+
9+
describe('Output Section Field Hover', () => {
10+
it('should return Description documentation when hovering on Description attribute', () => {
11+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
12+
text: 'Description',
13+
propertyPath: ['Outputs', 'MyOutput', 'Description'],
14+
});
15+
16+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
17+
18+
expect(result).toContain('**Description (optional)**');
19+
expect(result).toContain('A `String` type that describes the output value');
20+
expect(result).toContain('between 0 and 1024 bytes in length');
21+
expect(result).toContain("You can't use a parameter or function to specify the description");
22+
});
23+
24+
it('should return Value documentation when hovering on Value attribute', () => {
25+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
26+
text: 'Value',
27+
propertyPath: ['Outputs', 'MyOutput', 'Value'],
28+
});
29+
30+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
31+
32+
expect(result).toContain('**Value (required)**');
33+
expect(result).toContain('The value of the property returned by the [describe-stacks]');
34+
expect(result).toContain(
35+
'The value of an output can include literals, parameter references, pseudo parameters, a mapping value, or intrinsic functions',
36+
);
37+
});
38+
39+
it('should return Export documentation when hovering on Export attribute', () => {
40+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
41+
text: 'Export',
42+
propertyPath: ['Outputs', 'MyOutput', 'Export'],
43+
});
44+
45+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
46+
47+
expect(result).toContain('**Export (optional)**');
48+
expect(result).toContain('The name of the resource output to be exported for a cross-stack reference');
49+
expect(result).toContain('You can use intrinsic functions to customize the Name value of an export');
50+
expect(result).toContain('Get exported outputs from a deployed CloudFormation stack');
51+
});
52+
53+
it('should return undefined for non-output attributes', () => {
54+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
55+
text: 'InvalidAttribute',
56+
propertyPath: ['Outputs', 'MyOutput', 'InvalidAttribute'],
57+
});
58+
59+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
60+
61+
expect(result).toBeUndefined();
62+
});
63+
64+
it('should return undefined for empty text', () => {
65+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
66+
text: '',
67+
propertyPath: ['Outputs', 'MyOutput'],
68+
});
69+
70+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
71+
72+
expect(result).toBeUndefined();
73+
});
74+
75+
it('should return undefined for parameter attributes (case sensitivity)', () => {
76+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
77+
text: 'Type',
78+
propertyPath: ['Outputs', 'MyOutput', 'Type'],
79+
});
80+
81+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
82+
83+
expect(result).toBeUndefined();
84+
});
85+
86+
it('should return undefined for resource attributes', () => {
87+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
88+
text: 'DependsOn',
89+
propertyPath: ['Outputs', 'MyOutput', 'DependsOn'],
90+
});
91+
92+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
93+
94+
expect(result).toBeUndefined();
95+
});
96+
97+
it('should be case sensitive for attribute names', () => {
98+
const mockContext = createMockContext(TopLevelSection.Outputs, 'MyOutput', {
99+
text: 'description',
100+
propertyPath: ['Outputs', 'MyOutput', 'description'],
101+
});
102+
103+
const result = outputSectionFieldHoverProvider.getInformation(mockContext);
104+
105+
expect(result).toBeUndefined();
106+
});
107+
});
108+
109+
describe('isOutputSectionField static method', () => {
110+
it('should return true for valid output attributes', () => {
111+
const validAttributes = ['Description', 'Value', 'Export'];
112+
113+
for (const attribute of validAttributes) {
114+
expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(true);
115+
}
116+
});
117+
118+
it('should return false for invalid output attributes', () => {
119+
const invalidAttributes = [
120+
'InvalidAttribute',
121+
'Type',
122+
'Default',
123+
'Properties',
124+
'Condition',
125+
'DependsOn',
126+
'description',
127+
'value',
128+
'export',
129+
'',
130+
'Name', // This is a property of Export, not a top-level output attribute
131+
];
132+
133+
for (const attribute of invalidAttributes) {
134+
expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false);
135+
}
136+
});
137+
138+
it('should return false for parameter attributes', () => {
139+
const parameterAttributes = [
140+
'Type',
141+
'Default',
142+
'AllowedValues',
143+
'AllowedPattern',
144+
'MinLength',
145+
'MaxLength',
146+
'MinValue',
147+
'MaxValue',
148+
'NoEcho',
149+
'ConstraintDescription',
150+
];
151+
152+
for (const attribute of parameterAttributes) {
153+
expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false);
154+
}
155+
});
156+
157+
it('should return false for resource attributes', () => {
158+
const resourceAttributes = [
159+
'CreationPolicy',
160+
'DeletionPolicy',
161+
'UpdatePolicy',
162+
'UpdateReplacePolicy',
163+
'Condition',
164+
'DependsOn',
165+
'Metadata',
166+
];
167+
168+
for (const attribute of resourceAttributes) {
169+
expect(OutputSectionFieldHoverProvider.isOutputSectionField(attribute)).toBe(false);
170+
}
171+
});
172+
});
173+
});

0 commit comments

Comments
 (0)