-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #679 ### Summary of Changes Show inlay hints for * the type of block lambda results, placeholders, yields, * the corresponding parameter for positional arguments.
- Loading branch information
1 parent
275ad5e
commit f23fa29
Showing
4 changed files
with
240 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { AbstractInlayHintProvider, AstNode, InlayHintAcceptor } from 'langium'; | ||
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js'; | ||
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js'; | ||
import { SafeDsServices } from '../safe-ds-module.js'; | ||
import { isSdsArgument, isSdsBlockLambdaResult, isSdsPlaceholder, isSdsYield } from '../generated/ast.js'; | ||
import { isPositionalArgument } from '../helpers/nodeProperties.js'; | ||
import { InlayHintKind } from 'vscode-languageserver'; | ||
|
||
export class SafeDsInlayHintProvider extends AbstractInlayHintProvider { | ||
private readonly nodeMapper: SafeDsNodeMapper; | ||
private readonly typeComputer: SafeDsTypeComputer; | ||
|
||
constructor(services: SafeDsServices) { | ||
super(); | ||
|
||
this.nodeMapper = services.helpers.NodeMapper; | ||
this.typeComputer = services.types.TypeComputer; | ||
} | ||
|
||
override computeInlayHint(node: AstNode, acceptor: InlayHintAcceptor) { | ||
const cstNode = node.$cstNode; | ||
/* c8 ignore start */ | ||
if (!cstNode) { | ||
return; | ||
} | ||
/* c8 ignore stop */ | ||
|
||
if (isSdsArgument(node) && isPositionalArgument(node)) { | ||
const parameter = this.nodeMapper.argumentToParameter(node); | ||
if (parameter) { | ||
acceptor({ | ||
position: cstNode.range.start, | ||
label: `${parameter.name} = `, | ||
kind: InlayHintKind.Parameter, | ||
}); | ||
} | ||
} else if (isSdsBlockLambdaResult(node) || isSdsPlaceholder(node) || isSdsYield(node)) { | ||
const type = this.typeComputer.computeType(node); | ||
acceptor({ | ||
position: cstNode.range.end, | ||
label: `: ${type}`, | ||
kind: InlayHintKind.Type, | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
import { clearDocuments, parseHelper } from 'langium/test'; | ||
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; | ||
import { Position } from 'vscode-languageserver'; | ||
import { NodeFileSystem } from 'langium/node'; | ||
import { findTestChecks } from '../../helpers/testChecks.js'; | ||
import { URI } from 'langium'; | ||
|
||
const services = createSafeDsServices(NodeFileSystem).SafeDs; | ||
const inlayHintProvider = services.lsp.InlayHintProvider!; | ||
const workspaceManager = services.shared.workspace.WorkspaceManager; | ||
const parse = parseHelper(services); | ||
|
||
describe('SafeDsInlayHintProvider', async () => { | ||
beforeEach(async () => { | ||
// Load the builtin library | ||
await workspaceManager.initializeWorkspace([]); | ||
}); | ||
|
||
afterEach(async () => { | ||
await clearDocuments(services); | ||
}); | ||
|
||
const testCases: InlayHintProviderTest[] = [ | ||
{ | ||
testName: 'resolved positional argument', | ||
code: ` | ||
fun f(p: Int) | ||
pipeline myPipeline { | ||
// $TEST$ before "p = " | ||
f(»«1); | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'unresolved positional argument', | ||
code: ` | ||
fun f() | ||
pipeline myPipeline { | ||
f(1); | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'named argument', | ||
code: ` | ||
fun f(p: Int) | ||
pipeline myPipeline { | ||
f(p = 1); | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'block lambda result', | ||
code: ` | ||
pipeline myPipeline { | ||
() { | ||
// $TEST$ after ": literal<1>" | ||
yield r»« = 1; | ||
}; | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'placeholder', | ||
code: ` | ||
pipeline myPipeline { | ||
// $TEST$ after ": literal<1>" | ||
val x»« = 1; | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'wildcard', | ||
code: ` | ||
pipeline myPipeline { | ||
_ = 1; | ||
} | ||
`, | ||
}, | ||
{ | ||
testName: 'yield', | ||
code: ` | ||
segment s() -> r: Int { | ||
// $TEST$ after ": literal<1>" | ||
yield r»« = 1; | ||
} | ||
`, | ||
}, | ||
]; | ||
|
||
it.each(testCases)('should assign the correct inlay hints ($testName)', async ({ code }) => { | ||
const actualInlayHints = await getActualInlayHints(code); | ||
const expectedInlayHints = getExpectedInlayHints(code); | ||
|
||
expect(actualInlayHints).toStrictEqual(expectedInlayHints); | ||
}); | ||
}); | ||
|
||
const getActualInlayHints = async (code: string): Promise<SimpleInlayHint[] | undefined> => { | ||
const document = await parse(code); | ||
const inlayHints = await inlayHintProvider.getInlayHints(document, { | ||
range: document.parseResult.value.$cstNode!.range, | ||
textDocument: { uri: document.textDocument.uri }, | ||
}); | ||
|
||
return inlayHints?.map((hint) => { | ||
if (typeof hint.label === 'string') { | ||
return { | ||
label: hint.label, | ||
position: hint.position, | ||
}; | ||
} else { | ||
return { | ||
label: hint.label.join(''), | ||
position: hint.position, | ||
}; | ||
} | ||
}); | ||
}; | ||
|
||
const getExpectedInlayHints = (code: string): SimpleInlayHint[] => { | ||
const testChecks = findTestChecks(code, URI.file('file:///test.sdstest'), { failIfFewerRangesThanComments: true }); | ||
if (testChecks.isErr) { | ||
throw new Error(testChecks.error.message); | ||
} | ||
|
||
return testChecks.value.map((check) => { | ||
const range = check.location!.range; | ||
|
||
const afterMatch = /after "(?<label>[^"]*)"/gu.exec(check.comment); | ||
if (afterMatch) { | ||
return { | ||
label: afterMatch.groups!.label, | ||
position: { | ||
line: range.start.line, | ||
character: range.start.character - 1, | ||
}, | ||
}; | ||
} | ||
|
||
const beforeMatch = /before "(?<label>[^"]*)"/gu.exec(check.comment); | ||
if (beforeMatch) { | ||
return { | ||
label: beforeMatch.groups!.label, | ||
position: { | ||
line: range.end.line, | ||
character: range.end.character + 1, | ||
}, | ||
}; | ||
} | ||
|
||
throw new Error('Incorrect test comment format'); | ||
}); | ||
}; | ||
|
||
/** | ||
* A description of a test case for the inlay hint provider. | ||
*/ | ||
interface InlayHintProviderTest { | ||
/** | ||
* A short description of the test case. | ||
*/ | ||
testName: string; | ||
|
||
/** | ||
* The code to parse. | ||
*/ | ||
code: string; | ||
} | ||
|
||
/** | ||
* A simple inlay hint with some information removed. | ||
*/ | ||
interface SimpleInlayHint { | ||
/** | ||
* The text of the inlay hint. | ||
*/ | ||
label: string; | ||
|
||
/** | ||
* The position of the inlay hint. | ||
*/ | ||
position: Position; | ||
} |