Skip to content

Commit

Permalink
feat: improved completion provider (#997)
Browse files Browse the repository at this point in the history
Closes #41

### Summary of Changes

Various improvements to the completion provider:

* Proper icons
* Documentation
* Mark as deprecated
* Fix suggestions for member accesses
* Fix suggestions for member types
* Filter keywords
  • Loading branch information
lars-reimann authored Apr 5, 2024
1 parent df89291 commit 61e776b
Show file tree
Hide file tree
Showing 8 changed files with 586 additions and 17 deletions.
6 changes: 3 additions & 3 deletions packages/safe-ds-lang/src/language/helpers/fileExtensions.ts
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
115 changes: 115 additions & 0 deletions packages/safe-ds-lang/src/language/lsp/safe-ds-completion-provider.ts
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

0 comments on commit 61e776b

Please sign in to comment.