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

fix: improve props completion for namespaced component #2207

Merged
merged 2 commits into from
Nov 21, 2023
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
@@ -1,5 +1,5 @@
import ts from 'typescript';
import { flatten, isNotNullOrUndefined } from '../../utils';
import { isNotNullOrUndefined } from '../../utils';
import { findContainingNode } from './features/utils';

export type ComponentPartInfo = Array<{ name: string; type: string; doc?: string }>;
Expand All @@ -8,7 +8,6 @@ export interface ComponentInfoProvider {
getEvents(): ComponentPartInfo;
getSlotLets(slot?: string): ComponentPartInfo;
getProps(): ComponentPartInfo;
getProp(propName: string): ts.CompletionEntry[];
}

export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
Expand Down Expand Up @@ -54,61 +53,6 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider {
return this.mapPropertiesOfType(props);
}

getProp(propName: string): ts.CompletionEntry[] {
const props = this.getType('$$prop_def');
if (!props) {
return [];
}

const prop = props.getProperties().find((prop) => prop.name === propName);
if (!prop?.valueDeclaration) {
return [];
}

const propDef = this.typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);

if (!propDef.isUnion()) {
return [];
}

const types = flatten(propDef.types.map((type) => this.getStringLiteralTypes(type)));

// adopted from https://github.com/microsoft/TypeScript/blob/0921eac6dc9eba0be6319dff10b85d60c90155ea/src/services/stringCompletions.ts#L61
return types.map((v) => ({
name: v.value,
kindModifiers: ts.ScriptElementKindModifier.none,
kind: ts.ScriptElementKind.string,
sortText: /**LocationPriority: */ '11'
}));
}

/**
* adopted from https://github.com/microsoft/TypeScript/blob/0921eac6dc9eba0be6319dff10b85d60c90155ea/src/services/stringCompletions.ts#L310
*/
private getStringLiteralTypes(
type: ts.Type | undefined,
uniques = new Set<string>()
): ts.StringLiteralType[] {
if (!type) {
return [];
}

type = type.isTypeParameter() ? type.getConstraint() || type : type;

if (type.isUnion()) {
return flatten(type.types.map((t) => this.getStringLiteralTypes(t, uniques)));
}

if (
type.isStringLiteral() &&
!(type.flags & ts.TypeFlags.EnumLiteral) &&
!uniques.has(type.value)
) {
return [type];
}
return [];
}

private getType(classProperty: string) {
const symbol = this.classType.getProperty(classProperty);
if (!symbol?.valueDeclaration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,32 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
return null;
}

// one or two characters after start tag might be mapped to the component name
if (
svelteNode?.type === 'InlineComponent' &&
'name' in svelteNode &&
typeof svelteNode.name === 'string'
) {
const name = svelteNode.name;
const nameEnd = svelteNode.start + 1 + name.length;
const isWhitespaceAfterStartTag =
document.getText().slice(nameEnd, originalOffset).trim() === '' &&
this.mightBeAtStartTagWhitespace(document, originalOffset);

if (isWhitespaceAfterStartTag) {
// We can be sure only to get completions for directives and props here
// so don't bother with the expensive global completions
return this.getCompletionListForDirectiveOrProps(
attributeContext,
componentInfo,
document,
wordRange,
eventAndSlotLetCompletions,
tsDoc
);
}
}

const response = lang.getCompletionsAtPosition(
filePath,
offset,
Expand Down Expand Up @@ -236,28 +262,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
if (
completions.length > 500 &&
svelteNode?.type === 'InlineComponent' &&
[' ', ' >', ' /'].includes(
document.getText().substring(originalOffset - 1, originalOffset + 1)
)
this.mightBeAtStartTagWhitespace(document, originalOffset)
) {
// Very likely false global completions inside component start tag -> narrow
const props =
(!attributeContext?.inValue &&
componentInfo
?.getProps()
.map((entry) =>
this.componentInfoToCompletionEntry(
entry,
'',
CompletionItemKind.Field,
document,
wordRange
)
)) ||
[];
return CompletionList.create(
[...eventAndSlotLetCompletions, ...props],
!!tsDoc.parserError
return this.getCompletionListForDirectiveOrProps(
attributeContext,
componentInfo,
document,
wordRange,
eventAndSlotLetCompletions,
tsDoc
);
}

Expand Down Expand Up @@ -369,6 +383,12 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
return completionList;
}

private mightBeAtStartTagWhitespace(document: Document, originalOffset: number) {
return [' ', ' >', ' /'].includes(
document.getText().substring(originalOffset - 1, originalOffset + 1)
);
}

private canReuseLastCompletion(
lastCompletion: LastCompletion | undefined,
triggerKind: number | undefined,
Expand Down Expand Up @@ -463,6 +483,34 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
};
}

private getCompletionListForDirectiveOrProps(
attributeContext: AttributeContext | null,
componentInfo: ComponentInfoProvider | null,
document: Document,
wordRange: { start: number; end: number },
eventAndSlotLetCompletions: AppCompletionItem<CompletionEntryWithIdentifier>[],
tsDoc: SvelteDocumentSnapshot
) {
const props =
(!attributeContext?.inValue &&
componentInfo
?.getProps()
.map((entry) =>
this.componentInfoToCompletionEntry(
entry,
'',
CompletionItemKind.Field,
document,
wordRange
)
)) ||
[];
return CompletionList.create(
[...eventAndSlotLetCompletions, ...props],
!!tsDoc.parserError
);
}

private toCompletionItem(
snapshot: SvelteDocumentSnapshot,
comp: ts.CompletionEntry,
Expand Down
21 changes: 16 additions & 5 deletions packages/language-server/src/plugins/typescript/features/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,22 @@ export function getComponentAtPosition(
return null;
}

const generatedPosition = tsDoc.getGeneratedPosition(doc.positionAt(node.start + 1));
const def = lang.getDefinitionAtPosition(
tsDoc.filePath,
tsDoc.offsetAt(generatedPosition)
)?.[0];
const symbolPosWithinNode = node.tag?.includes('.') ? node.tag.lastIndexOf('.') + 1 : 0;

const generatedPosition = tsDoc.getGeneratedPosition(
doc.positionAt(node.start + symbolPosWithinNode + 1)
);

let def = lang.getDefinitionAtPosition(tsDoc.filePath, tsDoc.offsetAt(generatedPosition))?.[0];

while (def != null && def.kind !== ts.ScriptElementKind.classElement) {
const newDef = lang.getDefinitionAtPosition(tsDoc.filePath, def.textSpan.start)?.[0];
if (newDef?.fileName === def.fileName && newDef?.textSpan.start === def.textSpan.start) {
break;
}
def = newDef;
}

if (!def) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,26 @@ describe('CompletionProviderImpl', function () {
);
});

it(`provide props completions for namespaced component`, async () => {
const namespacedComponentTestList: [Position, string][] = [
[Position.create(9, 26), 'namespace import after tag name'],
[Position.create(9, 35), 'namespace import before tag end'],
[Position.create(10, 27), 'object namespace after tag name'],
[Position.create(10, 36), 'object namespace before tag end']
];

for (const [position, name] of namespacedComponentTestList) {
const { completionProvider, document } = setup('namespaced.svelte');

const completions = await completionProvider.getCompletions(document, position, {
triggerKind: CompletionTriggerKind.Invoked
});

const item = completions?.items.find((item) => item.label === 'hi2');
assert.ok(item, `expected to have completion for ${name}`);
}
});

// Hacky, but it works. Needed due to testing both new and old transformation
after(() => {
__resetCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export class ComponentDef2 extends SvelteComponentTyped<
},
{}
> {}

export class ComponentDef3 extends SvelteComponentTyped<
{ hi: string, hi2: string }
> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import * as Components from './ComponentDef'
import { ComponentDef3 } from './ComponentDef'

const Components2 = {
ComponentDef3
}
</script>

<Components.ComponentDef3 hi={''} ></Components.ComponentDef3>
<Components2.ComponentDef3 hi={''} ></Components2.ComponentDef3>