Skip to content

Commit f23fa29

Browse files
authored
feat: inlay hint provider (#683)
Closes #679 ### Summary of Changes Show inlay hints for * the type of block lambda results, placeholders, yields, * the corresponding parameter for positional arguments.
1 parent 275ad5e commit f23fa29

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed

src/language/helpers/nodeProperties.ts

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export const isNamedArgument = (node: SdsArgument): boolean => {
8181
return Boolean(node.parameter);
8282
};
8383

84+
export const isPositionalArgument = (node: SdsArgument): boolean => {
85+
return !node.parameter;
86+
};
87+
8488
export const isNamedTypeArgument = (node: SdsTypeArgument): boolean => {
8589
return Boolean(node.typeParameter);
8690
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { AbstractInlayHintProvider, AstNode, InlayHintAcceptor } from 'langium';
2+
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
3+
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
4+
import { SafeDsServices } from '../safe-ds-module.js';
5+
import { isSdsArgument, isSdsBlockLambdaResult, isSdsPlaceholder, isSdsYield } from '../generated/ast.js';
6+
import { isPositionalArgument } from '../helpers/nodeProperties.js';
7+
import { InlayHintKind } from 'vscode-languageserver';
8+
9+
export class SafeDsInlayHintProvider extends AbstractInlayHintProvider {
10+
private readonly nodeMapper: SafeDsNodeMapper;
11+
private readonly typeComputer: SafeDsTypeComputer;
12+
13+
constructor(services: SafeDsServices) {
14+
super();
15+
16+
this.nodeMapper = services.helpers.NodeMapper;
17+
this.typeComputer = services.types.TypeComputer;
18+
}
19+
20+
override computeInlayHint(node: AstNode, acceptor: InlayHintAcceptor) {
21+
const cstNode = node.$cstNode;
22+
/* c8 ignore start */
23+
if (!cstNode) {
24+
return;
25+
}
26+
/* c8 ignore stop */
27+
28+
if (isSdsArgument(node) && isPositionalArgument(node)) {
29+
const parameter = this.nodeMapper.argumentToParameter(node);
30+
if (parameter) {
31+
acceptor({
32+
position: cstNode.range.start,
33+
label: `${parameter.name} = `,
34+
kind: InlayHintKind.Parameter,
35+
});
36+
}
37+
} else if (isSdsBlockLambdaResult(node) || isSdsPlaceholder(node) || isSdsYield(node)) {
38+
const type = this.typeComputer.computeType(node);
39+
acceptor({
40+
position: cstNode.range.end,
41+
label: `: ${type}`,
42+
kind: InlayHintKind.Type,
43+
});
44+
}
45+
}
46+
}

src/language/safe-ds-module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { SafeDsNodeKindProvider } from './lsp/safe-ds-node-kind-provider.js';
3030
import { SafeDsDocumentSymbolProvider } from './lsp/safe-ds-document-symbol-provider.js';
3131
import { SafeDsDocumentBuilder } from './workspace/safe-ds-document-builder.js';
3232
import { SafeDsEnums } from './builtins/safe-ds-enums.js';
33+
import { SafeDsInlayHintProvider } from './lsp/safe-ds-inlay-hint-provider.js';
3334

3435
/**
3536
* Declaration of custom services - add your own service classes here.
@@ -83,6 +84,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
8384
lsp: {
8485
DocumentSymbolProvider: (services) => new SafeDsDocumentSymbolProvider(services),
8586
Formatter: () => new SafeDsFormatter(),
87+
InlayHintProvider: (services) => new SafeDsInlayHintProvider(services),
8688
SemanticTokenProvider: (services) => new SafeDsSemanticTokenProvider(services),
8789
},
8890
parser: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { clearDocuments, parseHelper } from 'langium/test';
3+
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
4+
import { Position } from 'vscode-languageserver';
5+
import { NodeFileSystem } from 'langium/node';
6+
import { findTestChecks } from '../../helpers/testChecks.js';
7+
import { URI } from 'langium';
8+
9+
const services = createSafeDsServices(NodeFileSystem).SafeDs;
10+
const inlayHintProvider = services.lsp.InlayHintProvider!;
11+
const workspaceManager = services.shared.workspace.WorkspaceManager;
12+
const parse = parseHelper(services);
13+
14+
describe('SafeDsInlayHintProvider', async () => {
15+
beforeEach(async () => {
16+
// Load the builtin library
17+
await workspaceManager.initializeWorkspace([]);
18+
});
19+
20+
afterEach(async () => {
21+
await clearDocuments(services);
22+
});
23+
24+
const testCases: InlayHintProviderTest[] = [
25+
{
26+
testName: 'resolved positional argument',
27+
code: `
28+
fun f(p: Int)
29+
30+
pipeline myPipeline {
31+
// $TEST$ before "p = "
32+
f(»«1);
33+
}
34+
`,
35+
},
36+
{
37+
testName: 'unresolved positional argument',
38+
code: `
39+
fun f()
40+
41+
pipeline myPipeline {
42+
f(1);
43+
}
44+
`,
45+
},
46+
{
47+
testName: 'named argument',
48+
code: `
49+
fun f(p: Int)
50+
51+
pipeline myPipeline {
52+
f(p = 1);
53+
}
54+
`,
55+
},
56+
{
57+
testName: 'block lambda result',
58+
code: `
59+
pipeline myPipeline {
60+
() {
61+
// $TEST$ after ": literal<1>"
62+
yield r»« = 1;
63+
};
64+
}
65+
`,
66+
},
67+
{
68+
testName: 'placeholder',
69+
code: `
70+
pipeline myPipeline {
71+
// $TEST$ after ": literal<1>"
72+
val x»« = 1;
73+
}
74+
`,
75+
},
76+
{
77+
testName: 'wildcard',
78+
code: `
79+
pipeline myPipeline {
80+
_ = 1;
81+
}
82+
`,
83+
},
84+
{
85+
testName: 'yield',
86+
code: `
87+
segment s() -> r: Int {
88+
// $TEST$ after ": literal<1>"
89+
yield r»« = 1;
90+
}
91+
`,
92+
},
93+
];
94+
95+
it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => {
96+
const actualInlayHints = await getActualInlayHints(code);
97+
const expectedInlayHints = getExpectedInlayHints(code);
98+
99+
expect(actualInlayHints).toStrictEqual(expectedInlayHints);
100+
});
101+
});
102+
103+
const getActualInlayHints = async (code: string): Promise<SimpleInlayHint[] | undefined> => {
104+
const document = await parse(code);
105+
const inlayHints = await inlayHintProvider.getInlayHints(document, {
106+
range: document.parseResult.value.$cstNode!.range,
107+
textDocument: { uri: document.textDocument.uri },
108+
});
109+
110+
return inlayHints?.map((hint) => {
111+
if (typeof hint.label === 'string') {
112+
return {
113+
label: hint.label,
114+
position: hint.position,
115+
};
116+
} else {
117+
return {
118+
label: hint.label.join(''),
119+
position: hint.position,
120+
};
121+
}
122+
});
123+
};
124+
125+
const getExpectedInlayHints = (code: string): SimpleInlayHint[] => {
126+
const testChecks = findTestChecks(code, URI.file('file:///test.sdstest'), { failIfFewerRangesThanComments: true });
127+
if (testChecks.isErr) {
128+
throw new Error(testChecks.error.message);
129+
}
130+
131+
return testChecks.value.map((check) => {
132+
const range = check.location!.range;
133+
134+
const afterMatch = /after "(?<label>[^"]*)"/gu.exec(check.comment);
135+
if (afterMatch) {
136+
return {
137+
label: afterMatch.groups!.label,
138+
position: {
139+
line: range.start.line,
140+
character: range.start.character - 1,
141+
},
142+
};
143+
}
144+
145+
const beforeMatch = /before "(?<label>[^"]*)"/gu.exec(check.comment);
146+
if (beforeMatch) {
147+
return {
148+
label: beforeMatch.groups!.label,
149+
position: {
150+
line: range.end.line,
151+
character: range.end.character + 1,
152+
},
153+
};
154+
}
155+
156+
throw new Error('Incorrect test comment format');
157+
});
158+
};
159+
160+
/**
161+
* A description of a test case for the inlay hint provider.
162+
*/
163+
interface InlayHintProviderTest {
164+
/**
165+
* A short description of the test case.
166+
*/
167+
testName: string;
168+
169+
/**
170+
* The code to parse.
171+
*/
172+
code: string;
173+
}
174+
175+
/**
176+
* A simple inlay hint with some information removed.
177+
*/
178+
interface SimpleInlayHint {
179+
/**
180+
* The text of the inlay hint.
181+
*/
182+
label: string;
183+
184+
/**
185+
* The position of the inlay hint.
186+
*/
187+
position: Position;
188+
}

0 commit comments

Comments
 (0)