Skip to content

Commit 7b38a36

Browse files
Hugo-HacheHugo Hache
andauthored
Add anchor preview in hover (#1150)
* Add anchor preview in hover * Enable shouldHoverAnchor by default * Add MAX_MERGE_RECURSION_LEVEL when hovering anchor * Display properties in declaration order --------- Co-authored-by: Hugo Hache <hugo.hache@sorare.com>
1 parent 5268a03 commit 7b38a36

File tree

4 files changed

+145
-11
lines changed

4 files changed

+145
-11
lines changed

src/languageserver/handlers/settingsHandlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export class SettingsHandler {
7171
if (Object.prototype.hasOwnProperty.call(settings.yaml, 'hover')) {
7272
this.yamlSettings.yamlShouldHover = settings.yaml.hover;
7373
}
74+
if (Object.prototype.hasOwnProperty.call(settings.yaml, 'hoverAnchor')) {
75+
this.yamlSettings.yamlShouldHoverAnchor = settings.yaml.hoverAnchor;
76+
}
7477
if (Object.prototype.hasOwnProperty.call(settings.yaml, 'completion')) {
7578
this.yamlSettings.yamlShouldCompletion = settings.yaml.completion;
7679
}
@@ -265,6 +268,7 @@ export class SettingsHandler {
265268
let languageSettings: LanguageSettings = {
266269
validate: this.yamlSettings.yamlShouldValidate,
267270
hover: this.yamlSettings.yamlShouldHover,
271+
hoverAnchor: this.yamlSettings.yamlShouldHoverAnchor,
268272
completion: this.yamlSettings.yamlShouldCompletion,
269273
schemas: [],
270274
customTags: this.yamlSettings.customTags,

src/languageservice/services/yamlHover.ts

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,27 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

7+
import * as l10n from '@vscode/l10n';
8+
import * as path from 'path';
9+
import { TextDocument } from 'vscode-languageserver-textdocument';
710
import { Hover, MarkupContent, MarkupKind, Position, Range } from 'vscode-languageserver-types';
8-
import { matchOffsetToDocument } from '../utils/arrUtils';
9-
import { LanguageSettings } from '../yamlLanguageService';
10-
import { YAMLSchemaService } from './yamlSchemaService';
11+
import { URI } from 'vscode-uri';
12+
import { isAlias, isMap, isSeq, Node, stringify as stringifyYAML } from 'yaml';
13+
import { ASTNode, ObjectASTNode, PropertyASTNode } from '../jsonASTTypes';
14+
import { JSONSchema } from '../jsonSchema';
1115
import { setKubernetesParserOption } from '../parser/isKubernetes';
12-
import { TextDocument } from 'vscode-languageserver-textdocument';
16+
import { getNodeValue, IApplicableSchema } from '../parser/jsonParser07';
1317
import { yamlDocumentsCache } from '../parser/yaml-documents';
1418
import { SingleYAMLDocument } from '../parser/yamlParser07';
15-
import { getNodeValue, IApplicableSchema } from '../parser/jsonParser07';
16-
import { JSONSchema } from '../jsonSchema';
17-
import { URI } from 'vscode-uri';
18-
import * as path from 'path';
19-
import * as l10n from '@vscode/l10n';
2019
import { Telemetry } from '../telemetry';
21-
import { ASTNode } from 'vscode-json-languageservice';
22-
import { stringify as stringifyYAML } from 'yaml';
20+
import { matchOffsetToDocument } from '../utils/arrUtils';
2321
import { toYamlStringScalar } from '../utils/yamlScalar';
22+
import { LanguageSettings } from '../yamlLanguageService';
23+
import { YAMLSchemaService } from './yamlSchemaService';
2424

2525
export class YAMLHover {
2626
private shouldHover: boolean;
27+
private shouldHoverAnchor: boolean;
2728
private indentation: string;
2829
private schemaService: YAMLSchemaService;
2930

@@ -38,6 +39,7 @@ export class YAMLHover {
3839
configure(languageSettings: LanguageSettings): void {
3940
if (languageSettings) {
4041
this.shouldHover = languageSettings.hover;
42+
this.shouldHoverAnchor = languageSettings.hoverAnchor;
4143
this.indentation = languageSettings.indentation;
4244
}
4345
}
@@ -103,6 +105,14 @@ export class YAMLHover {
103105
return result;
104106
};
105107

108+
if (this.shouldHoverAnchor && node.type === 'property' && node.valueNode) {
109+
if (node.valueNode.type === 'object') {
110+
const resolved = this.resolveMergeKeys(node.valueNode as ObjectASTNode, doc);
111+
const contents = '```yaml\n' + stringifyYAML(resolved, null, 2) + '\n```';
112+
return Promise.resolve(createHover(contents));
113+
}
114+
}
115+
106116
const removePipe = (value: string): string => {
107117
return value.replace(/\s\|\|\s*$/, '');
108118
};
@@ -207,6 +217,123 @@ export class YAMLHover {
207217
});
208218
}
209219

220+
/**
221+
* Resolves merge keys (<<) and anchors recursively in an object node
222+
* @param node The object AST node to resolve
223+
* @param doc The YAML document for resolving anchors
224+
* @param currentRecursionLevel Current recursion level (default: 0)
225+
* @returns A plain JavaScript object with all merges resolved
226+
*/
227+
private resolveMergeKeys(node: ObjectASTNode, doc: SingleYAMLDocument, currentRecursionLevel = 0): Record<string, unknown> {
228+
const result: Record<string, unknown> = {};
229+
const unprocessedProperties: PropertyASTNode[] = [...node.properties];
230+
231+
while (unprocessedProperties.length > 0) {
232+
const propertyNode = unprocessedProperties.shift();
233+
const key = propertyNode.keyNode.value;
234+
235+
if (key === '<<' && propertyNode.valueNode) {
236+
// Handle merge key
237+
const mergeValue = this.resolveMergeValue(propertyNode.valueNode, doc, currentRecursionLevel + 1);
238+
if (mergeValue && typeof mergeValue === 'object' && !Array.isArray(mergeValue)) {
239+
// Merge properties from the resolved value
240+
const mergeKeys = Object.keys(mergeValue);
241+
for (const mergeKey of mergeKeys) {
242+
result[mergeKey] = mergeValue[mergeKey];
243+
}
244+
}
245+
} else {
246+
// Regular property
247+
result[key] = this.astNodeToValue(propertyNode.valueNode, doc, currentRecursionLevel);
248+
}
249+
}
250+
251+
return result;
252+
}
253+
254+
/**
255+
* Resolves a merge value (which might be an alias) and recursively resolves its merge keys
256+
* @param node The AST node that might be an alias or object
257+
* @param doc The YAML document for resolving anchors
258+
* @param currentRecursionLevel Current recursion level
259+
* @returns The resolved value
260+
*/
261+
private resolveMergeValue(node: ASTNode, doc: SingleYAMLDocument, currentRecursionLevel: number): unknown {
262+
const MAX_MERGE_RECURSION_LEVEL = 10;
263+
264+
// Check if we've exceeded max recursion level
265+
if (currentRecursionLevel >= MAX_MERGE_RECURSION_LEVEL) {
266+
return { '<<': node.parent.internalNode['value'] + ' (recursion limit reached)' };
267+
}
268+
269+
// If it's an object node, resolve its merge keys
270+
if (node.type === 'object') {
271+
return this.resolveMergeKeys(node as ObjectASTNode, doc, currentRecursionLevel);
272+
}
273+
274+
// Otherwise, convert to value
275+
return this.astNodeToValue(node, doc, currentRecursionLevel);
276+
}
277+
278+
/**
279+
* Converts an AST node to a plain JavaScript value
280+
* @param node The AST node to convert
281+
* @param doc The YAML document for resolving anchors
282+
* @param currentRecursionLevel Current recursion level
283+
* @returns The converted value
284+
*/
285+
private astNodeToValue(node: ASTNode | undefined, doc: SingleYAMLDocument, currentRecursionLevel: number): unknown {
286+
if (!node) {
287+
return null;
288+
}
289+
290+
switch (node.type) {
291+
case 'object': {
292+
return this.resolveMergeKeys(node as ObjectASTNode, doc, currentRecursionLevel);
293+
}
294+
case 'array': {
295+
return node.children.map((child) => this.astNodeToValue(child, doc, currentRecursionLevel));
296+
}
297+
case 'string':
298+
case 'number':
299+
case 'boolean':
300+
case 'null': {
301+
return node.value;
302+
}
303+
default: {
304+
return this.nodeToValue(node.internalNode as Node);
305+
}
306+
}
307+
}
308+
309+
/**
310+
* Converts a YAML Node to a plain JavaScript value
311+
* @param node The YAML node to convert
312+
* @returns The converted value
313+
*/
314+
private nodeToValue(node: Node): unknown {
315+
if (isAlias(node)) {
316+
return node.source;
317+
}
318+
if (isMap(node)) {
319+
const result: Record<string, unknown> = {};
320+
for (const pair of node.items) {
321+
if (pair.key && pair.value) {
322+
const key = this.nodeToValue(pair.key as Node);
323+
const value = this.nodeToValue(pair.value as Node);
324+
if (typeof key === 'string') {
325+
result[key] = value;
326+
}
327+
}
328+
}
329+
return result;
330+
}
331+
if (isSeq(node)) {
332+
return node.items.map((item) => this.nodeToValue(item as Node));
333+
}
334+
return (node as { value?: unknown }).value;
335+
}
336+
210337
// copied from https://github.com/microsoft/vscode-json-languageservice/blob/2ea5ad3d2ffbbe40dea11cfe764a502becf113ce/src/services/jsonHover.ts#L112
211338
private toMarkdown(plain: string | undefined): string | undefined {
212339
if (plain) {

src/languageservice/yamlLanguageService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface SchemasSettings {
7979
export interface LanguageSettings {
8080
validate?: boolean; //Setting for whether we want to validate the schema
8181
hover?: boolean; //Setting for whether we want to have hover results
82+
hoverAnchor?: boolean; //Setting for whether we want to have hover anchor results
8283
completion?: boolean; //Setting for whether we want to have completion results
8384
format?: boolean; //Setting for whether we want to have the formatter or not
8485
isKubernetes?: boolean; //If true then its validating against kubernetes

src/yamlSettings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface Settings {
1414
schemas: JSONSchemaSettings[];
1515
validate: boolean;
1616
hover: boolean;
17+
hoverAnchor: boolean;
1718
completion: boolean;
1819
customTags: Array<string>;
1920
schemaStore: {
@@ -77,6 +78,7 @@ export class SettingsState {
7778
enable: true,
7879
} as CustomFormatterOptions;
7980
yamlShouldHover = true;
81+
yamlShouldHoverAnchor = true;
8082
yamlShouldCompletion = true;
8183
schemaStoreSettings = [];
8284
customTags = [];

0 commit comments

Comments
 (0)