Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved completion provider #997

Merged
merged 16 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ export type SdSFileExtension = typeof PIPELINE_FILE_EXTENSION | typeof STUB_FILE
/**
* Returns whether the object is contained in a pipeline file.
*/
export const isInPipelineFile = (node: AstNode) => isPipelineFile(AstUtils.getDocument(node));
export const isInPipelineFile = (node: AstNode | undefined) => node && isPipelineFile(AstUtils.getDocument(node));

/**
* Returns whether the object is contained in a stub file.
*/
export const isInStubFile = (node: AstNode) => isStubFile(AstUtils.getDocument(node));
export const isInStubFile = (node: AstNode | undefined) => node && isStubFile(AstUtils.getDocument(node));

/**
* Returns whether the object is contained in a test file.
*/
export const isInTestFile = (node: AstNode) => isTestFile(AstUtils.getDocument(node));
export const isInTestFile = (node: AstNode | undefined) => node && isTestFile(AstUtils.getDocument(node));

/**
* Returns whether the resource represents a pipeline file.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { CompletionContext, CompletionValueItem, DefaultCompletionProvider } from 'langium/lsp';
import { AstNode, AstNodeDescription, ReferenceInfo, Stream } from 'langium';
import { SafeDsServices } from '../safe-ds-module.js';
import { CompletionItemTag, MarkupContent } from 'vscode-languageserver';
import { createMarkupContent } from '../documentation/safe-ds-comment-provider.js';
import { SafeDsDocumentationProvider } from '../documentation/safe-ds-documentation-provider.js';
import type { SafeDsAnnotations } from '../builtins/safe-ds-annotations.js';
import { isSdsAnnotatedObject, isSdsModule, isSdsNamedType, isSdsReference } from '../generated/ast.js';
import { getPackageName } from '../helpers/nodeProperties.js';
import { isInPipelineFile, isInStubFile } from '../helpers/fileExtensions.js';

export class SafeDsCompletionProvider extends DefaultCompletionProvider {
private readonly builtinAnnotations: SafeDsAnnotations;
private readonly documentationProvider: SafeDsDocumentationProvider;

readonly completionOptions = {
triggerCharacters: ['.', '@'],
};

constructor(service: SafeDsServices) {
super(service);

this.builtinAnnotations = service.builtins.Annotations;
this.documentationProvider = service.documentation.DocumentationProvider;
}

protected override getReferenceCandidates(
refInfo: ReferenceInfo,
context: CompletionContext,
): Stream<AstNodeDescription> {
this.fixReferenceInfo(refInfo);
return super.getReferenceCandidates(refInfo, context);
}

private fixReferenceInfo(refInfo: ReferenceInfo): void {
if (isSdsNamedType(refInfo.container) && refInfo.container.$containerProperty === 'declaration') {
const syntheticNode = refInfo.container.$container as AstNode;
if (isSdsNamedType(syntheticNode) && syntheticNode.$containerProperty === 'member') {
refInfo.container = {
...refInfo.container,
$container: syntheticNode.$container,
$containerProperty: 'member',
};
} else {
refInfo.container = {
...refInfo.container,
$containerProperty: 'member',
};
}
} else if (isSdsReference(refInfo.container) && refInfo.container.$containerProperty === 'member') {
const syntheticNode = refInfo.container.$container as AstNode;
if (isSdsReference(syntheticNode) && syntheticNode.$containerProperty === 'member') {
refInfo.container = {
...refInfo.container,
$container: syntheticNode.$container,
$containerProperty: 'member',
};
}
}
}

protected override createReferenceCompletionItem(nodeDescription: AstNodeDescription): CompletionValueItem {
const node = nodeDescription.node;

return {
nodeDescription,
documentation: this.getDocumentation(node),
kind: this.nodeKindProvider.getCompletionItemKind(nodeDescription),
tags: this.getTags(node),
sortText: '0',
};
}

private getDocumentation(node: AstNode | undefined): MarkupContent | undefined {
if (!node) {
/* c8 ignore next 2 */
return undefined;
}

const documentation = this.documentationProvider.getDescription(node);
return createMarkupContent(documentation);
}

private getTags(node: AstNode | undefined): CompletionItemTag[] | undefined {
if (isSdsAnnotatedObject(node) && this.builtinAnnotations.callsDeprecated(node)) {
return [CompletionItemTag.Deprecated];
} else {
return undefined;
}
}

private illegalKeywordsInPipelineFile = new Set(['annotation', 'class', 'enum', 'fun', 'schema']);
private illegalKeywordsInStubFile = new Set(['pipeline', 'internal', 'private', 'segment']);

protected override filterKeyword(context: CompletionContext, keyword: Keyword): boolean {
// Filter out keywords that do not contain any word character
if (!/\p{L}/u.test(keyword.value)) {
return false;
}

if ((!context.node || isSdsModule(context.node)) && !getPackageName(context.node)) {
return keyword.value === 'package';
} else if (isSdsModule(context.node) && isInPipelineFile(context.node)) {
return !this.illegalKeywordsInPipelineFile.has(keyword.value);
} else if (isSdsModule(context.node) && isInStubFile(context.node)) {
return !this.illegalKeywordsInStubFile.has(keyword.value);
} else {
return true;
}
}
}

export interface Keyword {
value: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class SafeDsNodeInfoProvider {
* Returns the detail string for the given node. This can be used, for example, to provide document symbols or call
* hierarchies.
*/
getDetails(node: AstNode): string | undefined {
getDetails(node: AstNode | undefined): string | undefined {
if (isSdsAttribute(node)) {
return `: ${this.typeComputer.computeType(node)}`;
} else if (isSdsFunction(node) || isSdsSegment(node)) {
Expand All @@ -32,7 +32,7 @@ export class SafeDsNodeInfoProvider {
* Returns the tags for the given node. This can be used, for example, to provide document symbols or call
* hierarchies.
*/
getTags(node: AstNode): SymbolTag[] | undefined {
getTags(node: AstNode | undefined): SymbolTag[] | undefined {
if (isSdsAnnotatedObject(node) && this.builtinAnnotations.callsDeprecated(node)) {
return [SymbolTag.Deprecated];
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ export class SafeDsNodeKindProvider implements NodeKindProvider {
return SymbolKind.Method;
}

/* c8 ignore start */
const type = this.getNodeType(nodeOrDescription);
switch (type) {
case SdsAnnotation:
return SymbolKind.Interface;
case SdsAttribute:
return SymbolKind.Property;
/* c8 ignore next 2 */
case SdsBlockLambdaResult:
return SymbolKind.Variable;
case SdsClass:
Expand All @@ -48,36 +48,71 @@ export class SafeDsNodeKindProvider implements NodeKindProvider {
return SymbolKind.Function;
case SdsModule:
return SymbolKind.Package;
/* c8 ignore next 2 */
case SdsParameter:
return SymbolKind.Variable;
case SdsPipeline:
return SymbolKind.Function;
/* c8 ignore next 2 */
case SdsPlaceholder:
return SymbolKind.Variable;
/* c8 ignore next 2 */
case SdsResult:
return SymbolKind.Variable;
case SdsSchema:
return SymbolKind.Struct;
case SdsSegment:
return SymbolKind.Function;
/* c8 ignore next 2 */
case SdsTypeParameter:
return SymbolKind.TypeParameter;
/* c8 ignore next 2 */
default:
return SymbolKind.Null;
}
/* c8 ignore stop */
}

/* c8 ignore start */
getCompletionItemKind(_nodeOrDescription: AstNode | AstNodeDescription) {
return CompletionItemKind.Reference;
}
getCompletionItemKind(nodeOrDescription: AstNode | AstNodeDescription): CompletionItemKind {
// The WorkspaceSymbolProvider only passes descriptions, where the node might be undefined
const node = this.getNode(nodeOrDescription);
if (isSdsFunction(node) && AstUtils.hasContainerOfType(node, isSdsClass)) {
return CompletionItemKind.Method;
}

/* c8 ignore stop */
/* c8 ignore start */
const type = this.getNodeType(nodeOrDescription);
switch (type) {
case SdsAnnotation:
return CompletionItemKind.Interface;
case SdsAttribute:
return CompletionItemKind.Property;
case SdsBlockLambdaResult:
return CompletionItemKind.Variable;
case SdsClass:
return CompletionItemKind.Class;
case SdsEnum:
return CompletionItemKind.Enum;
case SdsEnumVariant:
return CompletionItemKind.EnumMember;
case SdsFunction:
return CompletionItemKind.Function;
case SdsModule:
return CompletionItemKind.Module;
case SdsParameter:
return CompletionItemKind.Variable;
case SdsPipeline:
return CompletionItemKind.Function;
case SdsPlaceholder:
return CompletionItemKind.Variable;
case SdsResult:
return CompletionItemKind.Variable;
case SdsSchema:
return CompletionItemKind.Struct;
case SdsSegment:
return CompletionItemKind.Function;
case SdsTypeParameter:
return CompletionItemKind.TypeParameter;
default:
return CompletionItemKind.Reference;
}
/* c8 ignore stop */
}

private getNode(nodeOrDescription: AstNode | AstNodeDescription): AstNode | undefined {
if (isAstNode(nodeOrDescription)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/safe-ds-lang/src/language/safe-ds-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { SafeDsRenameProvider } from './lsp/safe-ds-rename-provider.js';
import { SafeDsRunner } from './runner/safe-ds-runner.js';
import { SafeDsTypeFactory } from './typing/safe-ds-type-factory.js';
import { SafeDsMarkdownGenerator } from './generation/safe-ds-markdown-generator.js';
import { SafeDsCompletionProvider } from './lsp/safe-ds-completion-provider.js';

/**
* Declaration of custom services - add your own service classes here.
Expand Down Expand Up @@ -129,6 +130,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
},
lsp: {
CallHierarchyProvider: (services) => new SafeDsCallHierarchyProvider(services),
CompletionProvider: (services) => new SafeDsCompletionProvider(services),
DocumentSymbolProvider: (services) => new SafeDsDocumentSymbolProvider(services),
Formatter: () => new SafeDsFormatter(),
InlayHintProvider: (services) => new SafeDsInlayHintProvider(services),
Expand Down
Loading