From 3ba6e198bcffc910b534410a04a10e09979ef5e8 Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Wed, 10 Jun 2026 20:23:20 +0200 Subject: [PATCH 1/3] API: introduce getCompletionsAtPosition --- Herebyfile.mjs | 1 + _packages/native-preview/src/api/async/api.ts | 19 +++++- .../native-preview/src/api/async/types.ts | 30 ++++++++++ _packages/native-preview/src/api/proto.ts | 21 +++++++ _packages/native-preview/src/api/sync/api.ts | 19 +++++- .../native-preview/src/api/sync/types.ts | 30 ++++++++++ .../src/enums/completionItemKind.enum.ts | 29 +++++++++ .../src/enums/completionItemKind.ts | 29 +++++++++ .../native-preview/test/async/api.test.ts | 60 +++++++++++++++++++ .../native-preview/test/sync/api.test.ts | 60 +++++++++++++++++++ internal/api/proto.go | 36 +++++++++++ internal/api/session.go | 52 ++++++++++++++++ internal/ls/completions.go | 5 ++ 13 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 _packages/native-preview/src/enums/completionItemKind.enum.ts create mode 100644 _packages/native-preview/src/enums/completionItemKind.ts diff --git a/Herebyfile.mjs b/Herebyfile.mjs index c867326f3a..3e1755aaab 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -323,6 +323,7 @@ const enumDefs = [ { name: "ModifierFlags", goPrefix: "ModifierFlags", goFile: "internal/ast/modifierflags.go", outDir: "_packages/native-preview/src/enums" }, { name: "TokenFlags", goPrefix: "TokenFlags", goFile: "internal/ast/tokenflags.go", outDir: "_packages/native-preview/src/enums" }, { name: "NodeBuilderFlags", goPrefix: "Flags", goFile: "internal/nodebuilder/types.go", outDir: "_packages/native-preview/src/enums" }, + { name: "CompletionItemKind", goPrefix: "CompletionItemKind", goFile: "internal/lsp/lsproto/lsp_generated.go", outDir: "_packages/native-preview/src/enums" }, ]; /** diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts index 919855198e..ef3875ba23 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -1,4 +1,5 @@ /// +import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; import { NodeBuilderFlags } from "#enums/nodeBuilderFlags"; @@ -40,6 +41,7 @@ import { toPath, } from "../path.ts"; import type { + CompletionInfoResponse, ConfigResponse, DocumentIdentifier, DocumentPosition, @@ -65,6 +67,8 @@ import { import type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, + CompletionEntry, + CompletionInfo, ConditionalType, Diagnostic, FreshableType, @@ -91,9 +95,9 @@ import type { UnionType, } from "./types.ts"; -export { DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; +export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; /** Type alias for the snapshot-scoped object registry */ @@ -578,6 +582,17 @@ export class Checker { })); } + async getCompletionsAtPosition(document: string, position: number, triggerCharacter?: string): Promise { + const data = await this.client.apiRequest("getCompletionsAtPosition", { + snapshot: this.snapshotId, + project: this.projectId, + file: document, + position, + triggerCharacter, + }); + return data ?? undefined; + } + getTypeAtLocation(node: Node): Promise; getTypeAtLocation(nodes: readonly Node[]): Promise<(Type | undefined)[]>; async getTypeAtLocation(nodeOrNodes: Node | readonly Node[]): Promise { diff --git a/_packages/native-preview/src/api/async/types.ts b/_packages/native-preview/src/api/async/types.ts index 0d344ba17b..cfe3c20d6f 100644 --- a/_packages/native-preview/src/api/async/types.ts +++ b/_packages/native-preview/src/api/async/types.ts @@ -1,3 +1,4 @@ +import type { CompletionItemKind } from "#enums/completionItemKind"; import type { DiagnosticCategory } from "#enums/diagnosticCategory"; import type { ElementFlags } from "#enums/elementFlags"; import type { ObjectFlags } from "#enums/objectFlags"; @@ -200,6 +201,35 @@ export interface IndexInfo { readonly isReadonly: boolean; } +export interface CompletionEntryLabelDetails { + detail?: string; + description?: string; +} + +/** Options for {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionOptions { + triggerCharacter?: string; + /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ + includeSymbol?: boolean; +} + +/** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionEntry { + readonly name: string; + readonly kind?: CompletionItemKind; + readonly sortText?: string; + readonly insertText?: string; + readonly filterText?: string; + readonly detail?: string; + readonly labelDetails?: CompletionEntryLabelDetails; +} + +/** The result of {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionInfo { + readonly isIncomplete: boolean; + readonly entries: readonly CompletionEntry[]; +} + /** * A diagnostic message from the TypeScript compiler. */ diff --git a/_packages/native-preview/src/api/proto.ts b/_packages/native-preview/src/api/proto.ts index f76102aa8f..275fbca72c 100644 --- a/_packages/native-preview/src/api/proto.ts +++ b/_packages/native-preview/src/api/proto.ts @@ -1,3 +1,4 @@ +import type { CompletionItemKind } from "#enums/completionItemKind"; import { documentURIToFileName, fileNameToDocumentURI, @@ -193,3 +194,23 @@ export interface ProfileParams { export interface ProfileResult { file: string; } + +export interface CompletionEntryLabelDetailsResponse { + detail?: string; + description?: string; +} + +export interface CompletionEntryResponse { + name: string; + kind?: CompletionItemKind; + sortText?: string; + insertText?: string; + filterText?: string; + detail?: string; + labelDetails?: CompletionEntryLabelDetailsResponse; +} + +export interface CompletionInfoResponse { + isIncomplete: boolean; + entries: CompletionEntryResponse[]; +} diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts index 0c097aa190..45993d6f25 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -7,6 +7,7 @@ // Regenerate: npm run generate (from _packages/native-preview) // /// +import { CompletionItemKind } from "#enums/completionItemKind"; import { DiagnosticCategory } from "#enums/diagnosticCategory"; import { ElementFlags } from "#enums/elementFlags"; import { NodeBuilderFlags } from "#enums/nodeBuilderFlags"; @@ -48,6 +49,7 @@ import { toPath, } from "../path.ts"; import type { + CompletionInfoResponse, ConfigResponse, DocumentIdentifier, DocumentPosition, @@ -73,6 +75,8 @@ import { import type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, + CompletionEntry, + CompletionInfo, ConditionalType, Diagnostic, FreshableType, @@ -99,9 +103,9 @@ import type { UnionType, } from "./types.ts"; -export { DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; +export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; /** Type alias for the snapshot-scoped object registry */ @@ -586,6 +590,17 @@ export class Checker { })); } + getCompletionsAtPosition(document: string, position: number, triggerCharacter?: string): CompletionInfo | undefined { + const data = this.client.apiRequest("getCompletionsAtPosition", { + snapshot: this.snapshotId, + project: this.projectId, + file: document, + position, + triggerCharacter, + }); + return data ?? undefined; + } + getTypeAtLocation(node: Node): Type | undefined; getTypeAtLocation(nodes: readonly Node[]): (Type | undefined)[]; getTypeAtLocation(nodeOrNodes: Node | readonly Node[]): Type | (Type | undefined)[] | undefined { diff --git a/_packages/native-preview/src/api/sync/types.ts b/_packages/native-preview/src/api/sync/types.ts index e788818d35..65afc4f97b 100644 --- a/_packages/native-preview/src/api/sync/types.ts +++ b/_packages/native-preview/src/api/sync/types.ts @@ -6,6 +6,7 @@ // Source: src/api/async/types.ts // Regenerate: npm run generate (from _packages/native-preview) // +import type { CompletionItemKind } from "#enums/completionItemKind"; import type { DiagnosticCategory } from "#enums/diagnosticCategory"; import type { ElementFlags } from "#enums/elementFlags"; import type { ObjectFlags } from "#enums/objectFlags"; @@ -208,6 +209,35 @@ export interface IndexInfo { readonly isReadonly: boolean; } +export interface CompletionEntryLabelDetails { + detail?: string; + description?: string; +} + +/** Options for {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionOptions { + triggerCharacter?: string; + /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ + includeSymbol?: boolean; +} + +/** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionEntry { + readonly name: string; + readonly kind?: CompletionItemKind; + readonly sortText?: string; + readonly insertText?: string; + readonly filterText?: string; + readonly detail?: string; + readonly labelDetails?: CompletionEntryLabelDetails; +} + +/** The result of {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionInfo { + readonly isIncomplete: boolean; + readonly entries: readonly CompletionEntry[]; +} + /** * A diagnostic message from the TypeScript compiler. */ diff --git a/_packages/native-preview/src/enums/completionItemKind.enum.ts b/_packages/native-preview/src/enums/completionItemKind.enum.ts new file mode 100644 index 0000000000..c5a4140c45 --- /dev/null +++ b/_packages/native-preview/src/enums/completionItemKind.enum.ts @@ -0,0 +1,29 @@ +// Code generated by Herebyfile.mjs generate:enums from internal/lsp/lsproto/lsp_generated.go. DO NOT EDIT. + +export enum CompletionItemKind { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} diff --git a/_packages/native-preview/src/enums/completionItemKind.ts b/_packages/native-preview/src/enums/completionItemKind.ts new file mode 100644 index 0000000000..cb1e5d9aa3 --- /dev/null +++ b/_packages/native-preview/src/enums/completionItemKind.ts @@ -0,0 +1,29 @@ +// Code generated by Herebyfile.mjs generate:enums from internal/lsp/lsproto/lsp_generated.go. DO NOT EDIT. +export var CompletionItemKind: any; +(function (CompletionItemKind) { + CompletionItemKind[CompletionItemKind["Text"] = 1] = "Text"; + CompletionItemKind[CompletionItemKind["Method"] = 2] = "Method"; + CompletionItemKind[CompletionItemKind["Function"] = 3] = "Function"; + CompletionItemKind[CompletionItemKind["Constructor"] = 4] = "Constructor"; + CompletionItemKind[CompletionItemKind["Field"] = 5] = "Field"; + CompletionItemKind[CompletionItemKind["Variable"] = 6] = "Variable"; + CompletionItemKind[CompletionItemKind["Class"] = 7] = "Class"; + CompletionItemKind[CompletionItemKind["Interface"] = 8] = "Interface"; + CompletionItemKind[CompletionItemKind["Module"] = 9] = "Module"; + CompletionItemKind[CompletionItemKind["Property"] = 10] = "Property"; + CompletionItemKind[CompletionItemKind["Unit"] = 11] = "Unit"; + CompletionItemKind[CompletionItemKind["Value"] = 12] = "Value"; + CompletionItemKind[CompletionItemKind["Enum"] = 13] = "Enum"; + CompletionItemKind[CompletionItemKind["Keyword"] = 14] = "Keyword"; + CompletionItemKind[CompletionItemKind["Snippet"] = 15] = "Snippet"; + CompletionItemKind[CompletionItemKind["Color"] = 16] = "Color"; + CompletionItemKind[CompletionItemKind["File"] = 17] = "File"; + CompletionItemKind[CompletionItemKind["Reference"] = 18] = "Reference"; + CompletionItemKind[CompletionItemKind["Folder"] = 19] = "Folder"; + CompletionItemKind[CompletionItemKind["EnumMember"] = 20] = "EnumMember"; + CompletionItemKind[CompletionItemKind["Constant"] = 21] = "Constant"; + CompletionItemKind[CompletionItemKind["Struct"] = 22] = "Struct"; + CompletionItemKind[CompletionItemKind["Event"] = 23] = "Event"; + CompletionItemKind[CompletionItemKind["Operator"] = 24] = "Operator"; + CompletionItemKind[CompletionItemKind["TypeParameter"] = 25] = "TypeParameter"; +})(CompletionItemKind || (CompletionItemKind = {})); diff --git a/_packages/native-preview/test/async/api.test.ts b/_packages/native-preview/test/async/api.test.ts index 996b507acb..8925236344 100644 --- a/_packages/native-preview/test/async/api.test.ts +++ b/_packages/native-preview/test/async/api.test.ts @@ -2407,6 +2407,66 @@ describe("Checker - isTypeAssignableTo", () => { }); }); +describe("Checker - getCompletionsAtPosition", () => { + test("returns member completions after a dot", async () => { + const src = `\nconst obj = { name: "hello", age: 42 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + // Position right after "obj." — member completion trigger + const pos = src.indexOf("obj.") + "obj.".length; + const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + assert.ok(completions, "Expected completions to be returned"); + assert.ok(completions.entries.length > 0, "Expected at least one completion entry"); + assert.ok(completions.entries.some(e => e.name === "name"), "Expected 'name' property in completions"); + assert.ok(completions.entries.some(e => e.name === "age"), "Expected 'age' property in completions"); + } + finally { + await api.close(); + } + }); + + test("completion entries include sortText", async () => { + const src = `\nconst obj = { value: 1 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = src.indexOf("obj.") + "obj.".length; + const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + assert.ok(completions); + assert.ok(completions.entries.length > 0); + assert.ok(completions.entries.every(e => e.sortText !== undefined), "Expected sortText on all entries"); + } + finally { + await api.close(); + } + }); + + test("returns undefined for a non-existent file", async () => { + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": `export {};`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const completions = await project.checker.getCompletionsAtPosition("/src/does-not-exist.ts", 0); + assert.equal(completions, undefined, "Expected undefined for non-existent file"); + } + finally { + await api.close(); + } + }); +}); + describe("Emitter - printNode", () => { const emitterFiles = { "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts index ef0a2430af..129e9f8b12 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -2415,6 +2415,66 @@ describe("Checker - isTypeAssignableTo", () => { }); }); +describe("Checker - getCompletionsAtPosition", () => { + test("returns member completions after a dot", () => { + const src = `\nconst obj = { name: "hello", age: 42 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + // Position right after "obj." — member completion trigger + const pos = src.indexOf("obj.") + "obj.".length; + const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + assert.ok(completions, "Expected completions to be returned"); + assert.ok(completions.entries.length > 0, "Expected at least one completion entry"); + assert.ok(completions.entries.some(e => e.name === "name"), "Expected 'name' property in completions"); + assert.ok(completions.entries.some(e => e.name === "age"), "Expected 'age' property in completions"); + } + finally { + api.close(); + } + }); + + test("completion entries include sortText", () => { + const src = `\nconst obj = { value: 1 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = src.indexOf("obj.") + "obj.".length; + const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + assert.ok(completions); + assert.ok(completions.entries.length > 0); + assert.ok(completions.entries.every(e => e.sortText !== undefined), "Expected sortText on all entries"); + } + finally { + api.close(); + } + }); + + test("returns undefined for a non-existent file", () => { + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": `export {};`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const completions = project.checker.getCompletionsAtPosition("/src/does-not-exist.ts", 0); + assert.equal(completions, undefined, "Expected undefined for non-existent file"); + } + finally { + api.close(); + } + }); +}); + describe("Emitter - printNode", () => { const emitterFiles = { "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), diff --git a/internal/api/proto.go b/internal/api/proto.go index c27089fdcd..5a77a3fed4 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -127,6 +127,9 @@ const ( MethodGetReferencedSymbolsForNode Method = "getReferencedSymbolsForNode" MethodGetSignatureUsages Method = "getSignatureUsages" + // Language service methods + MethodGetCompletionsAtPosition Method = "getCompletionsAtPosition" + // Diagnostic methods MethodGetSyntacticDiagnostics Method = "getSyntacticDiagnostics" MethodGetSemanticDiagnostics Method = "getSemanticDiagnostics" @@ -361,6 +364,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetReferencesToSymbolInFile: unmarshallerFor[GetReferencesToSymbolInFileParams], MethodGetReferencedSymbolsForNode: unmarshallerFor[GetReferencedSymbolsForNodeParams], MethodGetSignatureUsages: unmarshallerFor[GetSignatureUsagesParams], + MethodGetCompletionsAtPosition: unmarshallerFor[GetCompletionsAtPositionParams], MethodPrintNode: unmarshallerFor[PrintNodeParams], MethodGetAnyType: unmarshallerFor[GetIntrinsicTypeParams], MethodGetStringType: unmarshallerFor[GetIntrinsicTypeParams], @@ -764,6 +768,38 @@ type SignatureUsageResponse struct { Call NodeHandle `json:"call,omitempty"` } +// GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. +type GetCompletionsAtPositionParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + File DocumentIdentifier `json:"file"` + Position uint32 `json:"position"` + TriggerCharacter *string `json:"triggerCharacter,omitempty"` +} + +// CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. +type CompletionEntryLabelDetailsResponse struct { + Detail *string `json:"detail,omitempty"` + Description *string `json:"description,omitempty"` +} + +// CompletionEntryResponse represents a single completion item. +type CompletionEntryResponse struct { + Name string `json:"name"` + Kind uint32 `json:"kind,omitempty"` + SortText *string `json:"sortText,omitempty"` + InsertText *string `json:"insertText,omitempty"` + FilterText *string `json:"filterText,omitempty"` + Detail *string `json:"detail,omitempty"` + LabelDetails *CompletionEntryLabelDetailsResponse `json:"labelDetails,omitempty"` +} + +// CompletionInfoResponse wraps a list of completion entries. +type CompletionInfoResponse struct { + IsIncomplete bool `json:"isIncomplete"` + Entries []*CompletionEntryResponse `json:"entries"` +} + // GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). type GetIntrinsicTypeParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/internal/api/session.go b/internal/api/session.go index bf2f2341ae..42c6fbaa7e 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -544,6 +544,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetReferencedSymbolsForNode(ctx, parsed.(*GetReferencedSymbolsForNodeParams)) case string(MethodGetSignatureUsages): return s.handleGetSignatureUsages(ctx, parsed.(*GetSignatureUsagesParams)) + case string(MethodGetCompletionsAtPosition): + return s.handleGetCompletionsAtPosition(ctx, parsed.(*GetCompletionsAtPositionParams)) default: return nil, fmt.Errorf("unknown method: %s", method) } @@ -2304,6 +2306,56 @@ func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSigna return result, nil } +// handleGetCompletionsAtPosition returns completions at a position in a document. +func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *GetCompletionsAtPositionParams) (*CompletionInfoResponse, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + sourceFile := program.GetSourceFile(params.File.ToFileName()) + if sourceFile == nil { + return nil, nil + } + langSvc, err := s.setupLanguageService(sd, program, params.Project, "") + if err != nil { + return nil, err + } + positionMap := sourceFile.GetPositionMap() + internalPos := positionMap.UTF16ToUTF8(int(params.Position)) + completionList, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter) + if err != nil || completionList == nil { + return nil, err + } + entries := make([]*CompletionEntryResponse, 0, len(completionList.Items)) + for _, item := range completionList.Items { + entry := &CompletionEntryResponse{ + Name: item.Label, + SortText: item.SortText, + InsertText: item.InsertText, + FilterText: item.FilterText, + Detail: item.Detail, + } + if item.Kind != nil { + entry.Kind = uint32(*item.Kind) + } + if item.LabelDetails != nil { + entry.LabelDetails = &CompletionEntryLabelDetailsResponse{ + Detail: item.LabelDetails.Detail, + Description: item.LabelDetails.Description, + } + } + entries = append(entries, entry) + } + return &CompletionInfoResponse{ + IsIncomplete: completionList.IsIncomplete, + Entries: entries, + }, nil +} + // handleGetReferencedSymbolsForNode returns node handles for all references found at a node. func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params *GetReferencedSymbolsForNodeParams) ([]ReferencedSymbolEntry, error) { sd, err := s.getSnapshotData(params.Snapshot) diff --git a/internal/ls/completions.go b/internal/ls/completions.go index b8d1dfeed4..23afa4fec2 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -290,6 +290,11 @@ const ( globalsSearchFail ) +// GetCompletionsAtPosition returns completions at the given position in a source file. +func (l *LanguageService) GetCompletionsAtPosition(ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string) (*lsproto.CompletionList, error) { + return l.getCompletionsAtPosition(ctx, file, position, triggerCharacter) +} + func (l *LanguageService) getCompletionsAtPosition( ctx context.Context, file *ast.SourceFile, From 42df64547f2fbd6842a11040bf9e7a5d26a5ad5f Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Wed, 10 Jun 2026 23:02:08 +0200 Subject: [PATCH 2/3] API: support includeSymbols parameter in getCompletionsAtPosition --- _packages/native-preview/src/api/async/api.ts | 17 +- .../native-preview/src/api/async/types.ts | 9 + _packages/native-preview/src/api/proto.ts | 1 + _packages/native-preview/src/api/sync/api.ts | 17 +- .../native-preview/src/api/sync/types.ts | 9 + .../native-preview/test/async/api.test.ts | 29 ++- .../native-preview/test/sync/api.test.ts | 29 ++- internal/api/proto.go | 2 + internal/api/session.go | 13 +- internal/ls/completions.go | 205 +++++++++++------- internal/ls/string_completions.go | 30 ++- 11 files changed, 258 insertions(+), 103 deletions(-) diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts index ef3875ba23..6406c54485 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -69,6 +69,7 @@ import type { AssertsThisTypePredicate, CompletionEntry, CompletionInfo, + CompletionOptions, ConditionalType, Diagnostic, FreshableType, @@ -97,7 +98,7 @@ import type { export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; /** Type alias for the snapshot-scoped object registry */ @@ -582,15 +583,23 @@ export class Checker { })); } - async getCompletionsAtPosition(document: string, position: number, triggerCharacter?: string): Promise { + async getCompletionsAtPosition(document: string, position: number, options?: CompletionOptions): Promise { const data = await this.client.apiRequest("getCompletionsAtPosition", { snapshot: this.snapshotId, project: this.projectId, file: document, position, - triggerCharacter, + triggerCharacter: options?.triggerCharacter, + includeSymbol: options?.includeSymbol, }); - return data ?? undefined; + if (!data) return undefined; + return { + isIncomplete: data.isIncomplete, + entries: data.entries.map(e => ({ + ...e, + symbol: e.symbol ? this.objectRegistry.getOrCreateSymbol(e.symbol) : undefined, + })), + }; } getTypeAtLocation(node: Node): Promise; diff --git a/_packages/native-preview/src/api/async/types.ts b/_packages/native-preview/src/api/async/types.ts index cfe3c20d6f..63536fe910 100644 --- a/_packages/native-preview/src/api/async/types.ts +++ b/_packages/native-preview/src/api/async/types.ts @@ -213,6 +213,13 @@ export interface CompletionOptions { includeSymbol?: boolean; } +/** Options for {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionOptions { + triggerCharacter?: string; + /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ + includeSymbol?: boolean; +} + /** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ export interface CompletionEntry { readonly name: string; @@ -222,6 +229,8 @@ export interface CompletionEntry { readonly filterText?: string; readonly detail?: string; readonly labelDetails?: CompletionEntryLabelDetails; + /** The symbol associated with this completion entry. Only set when `includeSymbol: true` is passed and a symbol is available. */ + readonly symbol?: Symbol; } /** The result of {@link Checker.getCompletionsAtPosition}. */ diff --git a/_packages/native-preview/src/api/proto.ts b/_packages/native-preview/src/api/proto.ts index 275fbca72c..30d2f62297 100644 --- a/_packages/native-preview/src/api/proto.ts +++ b/_packages/native-preview/src/api/proto.ts @@ -208,6 +208,7 @@ export interface CompletionEntryResponse { filterText?: string; detail?: string; labelDetails?: CompletionEntryLabelDetailsResponse; + symbol?: SymbolResponse; } export interface CompletionInfoResponse { diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts index 45993d6f25..775e8da3bf 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -77,6 +77,7 @@ import type { AssertsThisTypePredicate, CompletionEntry, CompletionInfo, + CompletionOptions, ConditionalType, Diagnostic, FreshableType, @@ -105,7 +106,7 @@ import type { export { CompletionItemKind, DiagnosticCategory, ElementFlags, ModifierFlags, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypePredicateKind }; export type { APIOptions, ClientSocketOptions, ClientSpawnOptions, DocumentIdentifier, DocumentPosition, LSPConnectionOptions }; -export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; +export type { AssertsIdentifierTypePredicate, AssertsThisTypePredicate, CompletionEntry, CompletionInfo, CompletionOptions, ConditionalType, Diagnostic, FreshableType, IdentifierTypePredicate, IndexedAccessType, IndexInfo, IndexType, InterfaceType, IntersectionType, IntrinsicType, LiteralType, ObjectType, StringMappingType, SubstitutionType, TemplateLiteralType, ThisTypePredicate, TupleType, Type, TypeParameter, TypePredicate, TypePredicateBase, TypeReference, UnionOrIntersectionType, UnionType }; export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts"; /** Type alias for the snapshot-scoped object registry */ @@ -590,15 +591,23 @@ export class Checker { })); } - getCompletionsAtPosition(document: string, position: number, triggerCharacter?: string): CompletionInfo | undefined { + getCompletionsAtPosition(document: string, position: number, options?: CompletionOptions): CompletionInfo | undefined { const data = this.client.apiRequest("getCompletionsAtPosition", { snapshot: this.snapshotId, project: this.projectId, file: document, position, - triggerCharacter, + triggerCharacter: options?.triggerCharacter, + includeSymbol: options?.includeSymbol, }); - return data ?? undefined; + if (!data) return undefined; + return { + isIncomplete: data.isIncomplete, + entries: data.entries.map(e => ({ + ...e, + symbol: e.symbol ? this.objectRegistry.getOrCreateSymbol(e.symbol) : undefined, + })), + }; } getTypeAtLocation(node: Node): Type | undefined; diff --git a/_packages/native-preview/src/api/sync/types.ts b/_packages/native-preview/src/api/sync/types.ts index 65afc4f97b..247a81be33 100644 --- a/_packages/native-preview/src/api/sync/types.ts +++ b/_packages/native-preview/src/api/sync/types.ts @@ -221,6 +221,13 @@ export interface CompletionOptions { includeSymbol?: boolean; } +/** Options for {@link Checker.getCompletionsAtPosition}. */ +export interface CompletionOptions { + triggerCharacter?: string; + /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ + includeSymbol?: boolean; +} + /** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ export interface CompletionEntry { readonly name: string; @@ -230,6 +237,8 @@ export interface CompletionEntry { readonly filterText?: string; readonly detail?: string; readonly labelDetails?: CompletionEntryLabelDetails; + /** The symbol associated with this completion entry. Only set when `includeSymbol: true` is passed and a symbol is available. */ + readonly symbol?: Symbol; } /** The result of {@link Checker.getCompletionsAtPosition}. */ diff --git a/_packages/native-preview/test/async/api.test.ts b/_packages/native-preview/test/async/api.test.ts index 8925236344..ae2fe5c7af 100644 --- a/_packages/native-preview/test/async/api.test.ts +++ b/_packages/native-preview/test/async/api.test.ts @@ -2419,11 +2419,12 @@ describe("Checker - getCompletionsAtPosition", () => { const project = snapshot.getProject("/tsconfig.json")!; // Position right after "obj." — member completion trigger const pos = src.indexOf("obj.") + "obj.".length; - const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions, "Expected completions to be returned"); assert.ok(completions.entries.length > 0, "Expected at least one completion entry"); assert.ok(completions.entries.some(e => e.name === "name"), "Expected 'name' property in completions"); assert.ok(completions.entries.some(e => e.name === "age"), "Expected 'age' property in completions"); + assert.ok(completions.entries.every(e => e.symbol === undefined), "Expected no symbol information"); } finally { await api.close(); @@ -2440,10 +2441,10 @@ describe("Checker - getCompletionsAtPosition", () => { const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); const project = snapshot.getProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; - const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions); assert.ok(completions.entries.length > 0); - assert.ok(completions.entries.every(e => e.sortText !== undefined), "Expected sortText on all entries"); + assert.ok(completions.entries.some(e => e.sortText !== undefined), "Expected sortText on all entries"); } finally { await api.close(); @@ -2465,6 +2466,28 @@ describe("Checker - getCompletionsAtPosition", () => { await api.close(); } }); + + test("includeSymbol: true populates symbol on property completions", async () => { + const src = `\nconst obj = { name: "hello", age: 42 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = src.indexOf("obj.") + "obj.".length; + const completions = await project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: ".", includeSymbol: true }); + assert.ok(completions, "Expected completions"); + const nameEntry = completions.entries.find(e => e.name === "name"); + assert.ok(nameEntry, "Expected 'name' entry"); + assert.ok(nameEntry.symbol, "Expected symbol to be set on 'name' entry when includeSymbol: true"); + assert.equal(nameEntry.symbol.name, "name", "Symbol name should match completion name"); + } + finally { + await api.close(); + } + }); }); describe("Emitter - printNode", () => { diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts index 129e9f8b12..8aeaa64bdb 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -2427,11 +2427,12 @@ describe("Checker - getCompletionsAtPosition", () => { const project = snapshot.getProject("/tsconfig.json")!; // Position right after "obj." — member completion trigger const pos = src.indexOf("obj.") + "obj.".length; - const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions, "Expected completions to be returned"); assert.ok(completions.entries.length > 0, "Expected at least one completion entry"); assert.ok(completions.entries.some(e => e.name === "name"), "Expected 'name' property in completions"); assert.ok(completions.entries.some(e => e.name === "age"), "Expected 'age' property in completions"); + assert.ok(completions.entries.every(e => e.symbol === undefined), "Expected no symbol information"); } finally { api.close(); @@ -2448,10 +2449,10 @@ describe("Checker - getCompletionsAtPosition", () => { const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); const project = snapshot.getProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; - const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, "."); + const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions); assert.ok(completions.entries.length > 0); - assert.ok(completions.entries.every(e => e.sortText !== undefined), "Expected sortText on all entries"); + assert.ok(completions.entries.some(e => e.sortText !== undefined), "Expected sortText on all entries"); } finally { api.close(); @@ -2473,6 +2474,28 @@ describe("Checker - getCompletionsAtPosition", () => { api.close(); } }); + + test("includeSymbol: true populates symbol on property completions", () => { + const src = `\nconst obj = { name: "hello", age: 42 };\nobj.\n`; + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/main.ts": src, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const pos = src.indexOf("obj.") + "obj.".length; + const completions = project.checker.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: ".", includeSymbol: true }); + assert.ok(completions, "Expected completions"); + const nameEntry = completions.entries.find(e => e.name === "name"); + assert.ok(nameEntry, "Expected 'name' entry"); + assert.ok(nameEntry.symbol, "Expected symbol to be set on 'name' entry when includeSymbol: true"); + assert.equal(nameEntry.symbol.name, "name", "Symbol name should match completion name"); + } + finally { + api.close(); + } + }); }); describe("Emitter - printNode", () => { diff --git a/internal/api/proto.go b/internal/api/proto.go index 5a77a3fed4..8d1b5e9f31 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -775,6 +775,7 @@ type GetCompletionsAtPositionParams struct { File DocumentIdentifier `json:"file"` Position uint32 `json:"position"` TriggerCharacter *string `json:"triggerCharacter,omitempty"` + IncludeSymbol bool `json:"includeSymbol,omitempty"` } // CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. @@ -792,6 +793,7 @@ type CompletionEntryResponse struct { FilterText *string `json:"filterText,omitempty"` Detail *string `json:"detail,omitempty"` LabelDetails *CompletionEntryLabelDetailsResponse `json:"labelDetails,omitempty"` + Symbol *SymbolResponse `json:"symbol,omitempty"` } // CompletionInfoResponse wraps a list of completion entries. diff --git a/internal/api/session.go b/internal/api/session.go index 42c6fbaa7e..e05c0998f7 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -2326,12 +2326,12 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge } positionMap := sourceFile.GetPositionMap() internalPos := positionMap.UTF16ToUTF8(int(params.Position)) - completionList, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter) - if err != nil || completionList == nil { + result, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol) + if err != nil || result == nil { return nil, err } - entries := make([]*CompletionEntryResponse, 0, len(completionList.Items)) - for _, item := range completionList.Items { + entries := make([]*CompletionEntryResponse, 0, len(result.Items)) + for _, item := range result.Items { entry := &CompletionEntryResponse{ Name: item.Label, SortText: item.SortText, @@ -2348,10 +2348,13 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge Description: item.LabelDetails.Description, } } + if item.Symbol != nil { + entry.Symbol = sd.registerSymbol(item.Symbol) + } entries = append(entries, entry) } return &CompletionInfoResponse{ - IsIncomplete: completionList.IsIncomplete, + IsIncomplete: result.IsIncomplete, Entries: entries, }, nil } diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 23afa4fec2..5eb4015730 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -44,19 +44,36 @@ func (l *LanguageService) ProvideCompletion( } ctx = format.WithFormatCodeSettings(ctx, l.FormatOptions(), l.FormatOptions().NewLineCharacter) position := int(l.converters.LineAndCharacterToPosition(file, LSPPosition)) - completionList, err := l.getCompletionsAtPosition( + completionListInternal, err := l.getCompletionsAtPosition( ctx, file, position, triggerCharacter, + false, /*includeSymbols*/ ) if err != nil { return lsproto.CompletionItemsOrListOrNull{}, err } - completionList = ensureItemData(file.FileName(), position, completionList) + completionList := ensureItemData(file.FileName(), position, completionListInternal.toLSP()) return lsproto.CompletionItemsOrListOrNull{List: completionList}, nil } +func (l *LanguageService) GetCompletionsAtPosition(ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string, includeSymbols bool) (*CompletionList, error) { + return l.getCompletionsAtPosition(ctx, file, position, triggerCharacter, includeSymbols) +} + +type CompletionItem struct { + *lsproto.CompletionItem + Symbol *ast.Symbol // non-nil for symbol completions when IncludeSymbols is set; nil otherwise +} + +type CompletionList struct { + IsIncomplete bool + ItemDefaults *lsproto.CompletionItemDefaults + ApplyKind *lsproto.CompletionItemApplyKinds + Items []*CompletionItem +} + func ensureItemData(fileName string, pos int, list *lsproto.CompletionList) *lsproto.CompletionList { if list == nil { return nil @@ -106,7 +123,7 @@ type completionDataData struct { } type completionDataKeyword struct { - keywordCompletions []*lsproto.CompletionItem + keywordCompletions []*CompletionItem isNewIdentifierLocation bool } @@ -290,9 +307,22 @@ const ( globalsSearchFail ) -// GetCompletionsAtPosition returns completions at the given position in a source file. -func (l *LanguageService) GetCompletionsAtPosition(ctx context.Context, file *ast.SourceFile, position int, triggerCharacter *string) (*lsproto.CompletionList, error) { - return l.getCompletionsAtPosition(ctx, file, position, triggerCharacter) +func (l *CompletionList) toLSP() *lsproto.CompletionList { + if l == nil { + return nil + } + items := make([]*lsproto.CompletionItem, 0, len(l.Items)) + for _, entry := range l.Items { + if entry != nil && entry.CompletionItem != nil { + items = append(items, entry.CompletionItem) + } + } + return &lsproto.CompletionList{ + IsIncomplete: l.IsIncomplete, + ItemDefaults: l.ItemDefaults, + ApplyKind: l.ApplyKind, + Items: items, + } } func (l *LanguageService) getCompletionsAtPosition( @@ -300,7 +330,8 @@ func (l *LanguageService) getCompletionsAtPosition( file *ast.SourceFile, position int, triggerCharacter *string, -) (*lsproto.CompletionList, error) { + includeSymbols bool, +) (*CompletionList, error) { _, previousToken := getRelevantTokens(position, file) if triggerCharacter != nil && !IsInString(file, position, previousToken) && !isValidTrigger(file, *triggerCharacter, previousToken, position) { return nil, nil @@ -309,9 +340,7 @@ func (l *LanguageService) getCompletionsAtPosition( if triggerCharacter != nil && *triggerCharacter == " " { // `isValidTrigger` ensures we are at `import |` if l.UserPreferences().IncludeCompletionsForImportStatements.IsTrue() { - return &lsproto.CompletionList{ - IsIncomplete: true, - }, nil + return &CompletionList{IsIncomplete: true}, nil } return nil, nil } @@ -330,6 +359,7 @@ func (l *LanguageService) getCompletionsAtPosition( previousToken, checker, compilerOptions, + includeSymbols, ) if stringCompletions != nil { return stringCompletions, nil @@ -368,6 +398,7 @@ func (l *LanguageService) getCompletionsAtPosition( data, position, optionalReplacementSpan, + includeSymbols, ) if err != nil { return nil, err @@ -535,10 +566,12 @@ func (l *LanguageService) getCompletionData( if importStatementCompletionInfo.keywordCompletion != ast.KindUnknown { if importStatementCompletionInfo.isKeywordOnlyCompletion { return &completionDataKeyword{ - keywordCompletions: []*lsproto.CompletionItem{{ - Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), - Kind: new(lsproto.CompletionItemKindKeyword), - SortText: new(string(SortTextGlobalsOrKeywords)), + keywordCompletions: []*CompletionItem{{ + CompletionItem: &lsproto.CompletionItem{ + Label: scanner.TokenToString(importStatementCompletionInfo.keywordCompletion), + Kind: new(lsproto.CompletionItemKindKeyword), + SortText: new(string(SortTextGlobalsOrKeywords)), + }, }}, isNewIdentifierLocation: importStatementCompletionInfo.isNewIdentifierLocation, }, nil @@ -1699,7 +1732,8 @@ func (l *LanguageService) completionInfoFromData( data *completionDataData, position int, optionalReplacementSpan *lsproto.Range, -) (*lsproto.CompletionList, error) { + includeSymbols bool, +) (*CompletionList, error) { keywordFilters := data.keywordFilters isNewIdentifierLocation := data.isNewIdentifierLocation contextToken := data.contextToken @@ -1748,6 +1782,7 @@ func (l *LanguageService) completionInfoFromData( position, file, compilerOptions, + includeSymbols, ) if data.keywordFilters != KeywordCompletionFiltersNone { @@ -1765,14 +1800,14 @@ func (l *LanguageService) completionInfoFromData( for _, keywordEntry := range getContextualKeywords(file, contextToken, position) { if !uniqueNames.Has(keywordEntry.Label) { uniqueNames.Add(keywordEntry.Label) - sortedEntries = append(sortedEntries, keywordEntry) + sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: keywordEntry}) } } for _, literal := range literals { literalEntry := createCompletionItemForLiteral(file, preferences, literal) uniqueNames.Add(literalEntry.Label) - sortedEntries = append(sortedEntries, literalEntry) + sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: literalEntry}) } if !isChecked { @@ -1800,7 +1835,7 @@ func (l *LanguageService) completionInfoFromData( return nil, err } if casesItem != nil { - sortedEntries = append(sortedEntries, casesItem) + sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: casesItem}) } } } @@ -1814,7 +1849,7 @@ func (l *LanguageService) completionInfoFromData( optionalReplacementSpan, ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: data.hasUnresolvedAutoImports, ItemDefaults: itemDefaults, Items: sortedEntries, @@ -1829,7 +1864,8 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( position int, file *ast.SourceFile, compilerOptions *core.CompilerOptions, -) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) { + includeSymbols bool, +) (uniqueNames collections.Set[string], sortedEntries []*CompletionItem) { closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location) useSemicolons := lsutil.ProbablyUsesSemicolons(file) isMemberCompletion := isMemberCompletionKind(data.completionKind) @@ -1895,7 +1931,11 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( !(symbol.Parent == nil && !core.Some(symbol.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == file })) uniques[name] = shouldShadowLaterSymbols - sortedEntries = append(sortedEntries, entry) + var sym *ast.Symbol + if includeSymbols { + sym = symbol + } + sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry, Symbol: sym}) } for _, autoImport := range data.autoImports { @@ -1949,7 +1989,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols( if isShadowed, _ := uniques[autoImport.Fix.Name]; !isShadowed { uniques[autoImport.Fix.Name] = false - sortedEntries = append(sortedEntries, entry) + sortedEntries = append(sortedEntries, &CompletionItem{CompletionItem: entry}) } } @@ -3226,16 +3266,19 @@ var ( }) ) -func cloneItems(items []*lsproto.CompletionItem) []*lsproto.CompletionItem { - result := make([]*lsproto.CompletionItem, len(items)) +func cloneItems(items []*lsproto.CompletionItem) []*CompletionItem { + if items == nil { + return nil + } + entries := make([]*CompletionItem, len(items)) for i, item := range items { itemClone := *item - result[i] = &itemClone + entries[i] = &CompletionItem{CompletionItem: &itemClone} } - return result + return entries } -func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*lsproto.CompletionItem { +func getKeywordCompletions(keywordFilter KeywordCompletionFilters, filterOutTsOnlyKeywords bool) []*CompletionItem { if !filterOutTsOnlyKeywords { return cloneItems(getTypescriptKeywordCompletions(keywordFilter)) } @@ -3396,8 +3439,8 @@ func (l *LanguageService) getJSCompletionEntries( file *ast.SourceFile, position int, uniqueNames *collections.Set[string], - sortedEntries []*lsproto.CompletionItem, -) []*lsproto.CompletionItem { + sortedEntries []*CompletionItem, +) []*CompletionItem { nameTable := file.GetNameTable() for name, pos := range nameTable { // Skip identifiers produced only from the current location @@ -3406,11 +3449,13 @@ func (l *LanguageService) getJSCompletionEntries( } if !uniqueNames.Has(name) && scanner.IsIdentifierText(name, core.LanguageVariantStandard) { uniqueNames.Add(name) - sortedEntries = append(sortedEntries, &lsproto.CompletionItem{ - Label: name, - Kind: new(lsproto.CompletionItemKindText), - SortText: new(string(SortTextJavascriptIdentifiers)), - CommitCharacters: new([]string{}), + sortedEntries = append(sortedEntries, &CompletionItem{ + CompletionItem: &lsproto.CompletionItem{ + Label: name, + Kind: new(lsproto.CompletionItemKindText), + SortText: new(string(SortTextJavascriptIdentifiers)), + CommitCharacters: new([]string{}), + }, }) } } @@ -4198,7 +4243,7 @@ func (l *LanguageService) setItemDefaults( ctx context.Context, position int, file *ast.SourceFile, - items []*lsproto.CompletionItem, + items []*CompletionItem, defaultCommitCharacters *[]string, optionalReplacementSpan *lsproto.Range, ) *lsproto.CompletionItemDefaults { @@ -4267,10 +4312,10 @@ func (l *LanguageService) specificKeywordCompletionInfo( ctx context.Context, position int, file *ast.SourceFile, - items []*lsproto.CompletionItem, + items []*CompletionItem, isNewIdentifierLocation bool, optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionList { +) *CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) itemDefaults := l.setItemDefaults( ctx, @@ -4280,7 +4325,7 @@ func (l *LanguageService) specificKeywordCompletionInfo( &defaultCommitCharacters, optionalReplacementSpan, ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -4292,7 +4337,7 @@ func (l *LanguageService) getJsxClosingTagCompletion( location *ast.Node, file *ast.SourceFile, position int, -) *lsproto.CompletionList { +) *CompletionList { // We wanna walk up the tree till we find a JSX closing element. jsxClosingElement := ast.FindAncestorOrQuit(location, func(node *ast.Node) ast.FindAncestorResult { switch node.Kind { @@ -4328,7 +4373,7 @@ func (l *LanguageService) getJsxClosingTagCompletion( optionalReplacementSpan := new(l.createLspRangeFromNode(jsxClosingElement.TagName(), file)) defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) - item := l.createLSPCompletionItem( + lspItem := l.createLSPCompletionItem( ctx, fullClosingTag, /*name*/ "", /*insertText*/ @@ -4349,7 +4394,10 @@ func (l *LanguageService) getJsxClosingTagCompletion( nil, /*autoImportEntryData*/ // !!! jsx autoimports nil, /*detail*/ ) - items := []*lsproto.CompletionItem{item} + item := &CompletionItem{ + CompletionItem: lspItem, + } + items := []*CompletionItem{item} itemDefaults := l.setItemDefaults( ctx, position, @@ -4359,7 +4407,7 @@ func (l *LanguageService) getJsxClosingTagCompletion( optionalReplacementSpan, ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -4465,7 +4513,7 @@ func (l *LanguageService) getLabelCompletionsAtPosition( file *ast.SourceFile, position int, optionalReplacementSpan *lsproto.Range, -) *lsproto.CompletionList { +) *CompletionList { items := l.getLabelStatementCompletions(ctx, node, file, position) if len(items) == 0 { return nil @@ -4479,7 +4527,7 @@ func (l *LanguageService) getLabelCompletionsAtPosition( &defaultCommitCharacters, optionalReplacementSpan, ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -4491,9 +4539,9 @@ func (l *LanguageService) getLabelStatementCompletions( node *ast.BreakOrContinueStatement, file *ast.SourceFile, position int, -) []*lsproto.CompletionItem { +) []*CompletionItem { var uniques collections.Set[string] - var items []*lsproto.CompletionItem + var items []*CompletionItem current := node for current != nil { if ast.IsFunctionLike(current) { @@ -4503,7 +4551,7 @@ func (l *LanguageService) getLabelStatementCompletions( name := current.Label().Text() if !uniques.Has(name) { uniques.Add(name) - items = append(items, l.createLSPCompletionItem( + lspItem := l.createLSPCompletionItem( ctx, name, "", /*insertText*/ @@ -4523,7 +4571,10 @@ func (l *LanguageService) getLabelStatementCompletions( "", /*source*/ nil, /*autoImportEntryData*/ nil, /*detail*/ - )) + ) + items = append(items, &CompletionItem{ + CompletionItem: lspItem, + }) } } current = current.Parent @@ -4900,7 +4951,7 @@ func (l *LanguageService) getCompletionItemDetails( case *completionDataJSDocParameterName: return createSimpleDetails(item, data.Name, docFormat) case *completionDataKeyword: - if core.Some(request.keywordCompletions, func(c *lsproto.CompletionItem) bool { + if core.Some(request.keywordCompletions, func(c *CompletionItem) bool { return c.Label == data.Name }) { return createSimpleDetails(item, data.Name, docFormat) @@ -5305,8 +5356,8 @@ func (l *LanguageService) jsDocCompletionInfo( ctx context.Context, position int, file *ast.SourceFile, - items []*lsproto.CompletionItem, -) *lsproto.CompletionList { + items []*CompletionItem, +) *CompletionList { defaultCommitCharacters := getDefaultCommitCharacters(false /*isNewIdentifierLocation*/) itemDefaults := l.setItemDefaults( ctx, @@ -5316,7 +5367,7 @@ func (l *LanguageService) jsDocCompletionInfo( &defaultCommitCharacters, nil, /*optionalReplacementSpan*/ ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -5436,11 +5487,11 @@ var jsDocTagCompletionItems = sync.OnceValue(func() []*lsproto.CompletionItem { return items }) -func getJSDocTagNameCompletions() []*lsproto.CompletionItem { +func getJSDocTagNameCompletions() []*CompletionItem { return cloneItems(jsDocTagNameCompletionItems()) } -func getJSDocTagCompletions() []*lsproto.CompletionItem { +func getJSDocTagCompletions() []*CompletionItem { return cloneItems(jsDocTagCompletionItems()) } @@ -5452,7 +5503,7 @@ func getJSDocParameterCompletions( options *core.CompilerOptions, preferences lsutil.UserPreferences, tagNameOnly bool, -) []*lsproto.CompletionItem { +) []*CompletionItem { currentToken := astnav.GetTokenAtPosition(file, position) if !ast.IsJSDocTag(currentToken) && !currentToken.IsJSDoc() { return nil @@ -5487,7 +5538,7 @@ func getJSDocParameterCompletions( } } paramIndex := -1 - return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + return core.MapNonNil(fun.Parameters(), func(param *ast.ParameterDeclarationNode) *CompletionItem { paramIndex++ if paramIndex < paramTagCount { // This parameter is already annotated. @@ -5530,12 +5581,14 @@ func getJSDocParameterCompletions( } } - return &lsproto.CompletionItem{ - Label: displayText, - Kind: new(lsproto.CompletionItemKindVariable), - SortText: new(string(SortTextLocationPriority)), - InsertText: strPtrTo(snippetText), - InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil), + return &CompletionItem{ + CompletionItem: &lsproto.CompletionItem{ + Label: displayText, + Kind: new(lsproto.CompletionItemKindVariable), + SortText: new(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil), + }, } } else if paramIndex == paramTagCount { // Destructuring parameter; do it positionally @@ -5571,12 +5624,14 @@ func getJSDocParameterCompletions( displayText = strings.TrimPrefix(displayText, "@") snippetText = strings.TrimPrefix(snippetText, "@") } - return &lsproto.CompletionItem{ - Label: displayText, - Kind: new(lsproto.CompletionItemKindVariable), - SortText: new(string(SortTextLocationPriority)), - InsertText: strPtrTo(snippetText), - InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil), + return &CompletionItem{ + CompletionItem: &lsproto.CompletionItem{ + Label: displayText, + Kind: new(lsproto.CompletionItemKindVariable), + SortText: new(string(SortTextLocationPriority)), + InsertText: strPtrTo(snippetText), + InsertTextFormat: core.IfElse(isSnippet, new(lsproto.InsertTextFormatSnippet), nil), + }, } } return nil @@ -5841,7 +5896,7 @@ func jsDocParamElementWorker( return nil } -func getJSDocParameterNameCompletions(tag *ast.JSDocParameterOrPropertyTag) []*lsproto.CompletionItem { +func getJSDocParameterNameCompletions(tag *ast.JSDocParameterOrPropertyTag) []*CompletionItem { if !ast.IsIdentifier(tag.Name()) { return nil } @@ -5857,7 +5912,7 @@ func getJSDocParameterNameCompletions(tag *ast.JSDocParameterOrPropertyTag) []*l tags = jsDoc.AsJSDoc().Tags.Nodes } - return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *lsproto.CompletionItem { + return core.MapNonNil(fn.Parameters(), func(param *ast.ParameterDeclarationNode) *CompletionItem { if !ast.IsIdentifier(param.Name()) { return nil } @@ -5872,10 +5927,12 @@ func getJSDocParameterNameCompletions(tag *ast.JSDocParameterOrPropertyTag) []*l return nil } - return &lsproto.CompletionItem{ - Label: name, - Kind: new(lsproto.CompletionItemKindVariable), - SortText: new(string(SortTextLocationPriority)), + return &CompletionItem{ + CompletionItem: &lsproto.CompletionItem{ + Label: name, + Kind: new(lsproto.CompletionItemKindVariable), + SortText: new(string(SortTextLocationPriority)), + }, } }) } diff --git a/internal/ls/string_completions.go b/internal/ls/string_completions.go index 9bba30eb9a..f5240bcf32 100644 --- a/internal/ls/string_completions.go +++ b/internal/ls/string_completions.go @@ -57,7 +57,8 @@ func (l *LanguageService) getStringLiteralCompletions( contextToken *ast.Node, checker *checker.Checker, compilerOptions *core.CompilerOptions, -) *lsproto.CompletionList { + includeSymbols bool, +) *CompletionList { if isInReferenceComment(file, position) { entries := l.getTripleSlashReferenceCompletions(file, position, l.GetProgram(), checker) return l.convertPathCompletions(ctx, entries, file, position) @@ -81,6 +82,7 @@ func (l *LanguageService) getStringLiteralCompletions( position, checker, compilerOptions, + includeSymbols, ) } return nil @@ -94,7 +96,8 @@ func (l *LanguageService) convertStringLiteralCompletions( position int, typeChecker *checker.Checker, options *core.CompilerOptions, -) *lsproto.CompletionList { + includeSymbols bool, +) *CompletionList { if completion == nil { return nil } @@ -121,6 +124,7 @@ func (l *LanguageService) convertStringLiteralCompletions( position, file, options, + includeSymbols, /*includeSymbols*/ ) defaultCommitCharacters := getDefaultCommitCharacters(completion.hasIndexSignature) itemDefaults := l.setItemDefaults( @@ -131,7 +135,7 @@ func (l *LanguageService) convertStringLiteralCompletions( &defaultCommitCharacters, optionalReplacementRange, ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -146,9 +150,9 @@ func (l *LanguageService) convertStringLiteralCompletions( } else { quoteChar = printer.QuoteCharDoubleQuote } - items := core.Map(completion.types, func(t *checker.StringLiteralType) *lsproto.CompletionItem { + items := core.Map(completion.types, func(t *checker.StringLiteralType) *CompletionItem { name := printer.EscapeString(t.AsLiteralType().Value().(string), quoteChar) - return l.createLSPCompletionItem( + lspItem := l.createLSPCompletionItem( ctx, name, "", /*insertText*/ @@ -169,6 +173,9 @@ func (l *LanguageService) convertStringLiteralCompletions( nil, /*autoImportEntryData*/ nil, /*detail*/ ) + return &CompletionItem{ + CompletionItem: lspItem, + } }) defaultCommitCharacters := getDefaultCommitCharacters(completion.isNewIdentifier) itemDefaults := l.setItemDefaults( @@ -179,7 +186,7 @@ func (l *LanguageService) convertStringLiteralCompletions( &defaultCommitCharacters, nil, /*optionalReplacementSpan*/ ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, @@ -194,10 +201,10 @@ func (l *LanguageService) convertPathCompletions( pathCompletions []*pathCompletion, file *ast.SourceFile, position int, -) *lsproto.CompletionList { +) *CompletionList { isNewIdentifierLocation := true // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of. defaultCommitCharacters := getDefaultCommitCharacters(isNewIdentifierLocation) - items := core.Map(pathCompletions, func(pathCompletion *pathCompletion) *lsproto.CompletionItem { + items := core.Map(pathCompletions, func(pathCompletion *pathCompletion) *CompletionItem { var replacementSpan *lsproto.Range if pathCompletion.textRange != nil { replacementSpan = new(l.createLspRangeFromBounds(pathCompletion.textRange.Pos(), pathCompletion.textRange.End(), file)) @@ -206,7 +213,7 @@ func (l *LanguageService) convertPathCompletions( if !strings.HasSuffix(pathCompletion.name, pathCompletion.extension) { detail += pathCompletion.extension } - return l.createLSPCompletionItem( + lspItem := l.createLSPCompletionItem( ctx, pathCompletion.name, "", /*insertText*/ @@ -227,6 +234,9 @@ func (l *LanguageService) convertPathCompletions( nil, /*autoImportEntryData*/ &detail, ) + return &CompletionItem{ + CompletionItem: lspItem, + } }) itemDefaults := l.setItemDefaults( ctx, @@ -236,7 +246,7 @@ func (l *LanguageService) convertPathCompletions( &defaultCommitCharacters, nil, /*optionalReplacementSpan*/ ) - return &lsproto.CompletionList{ + return &CompletionList{ IsIncomplete: false, ItemDefaults: itemDefaults, Items: items, From 2d42b2b02b21f6784f413844e5abc3b41d5abc0e Mon Sep 17 00:00:00 2001 From: Piotr Tomiak Date: Thu, 11 Jun 2026 22:32:49 +0200 Subject: [PATCH 3/3] API: remove duplicated CompletionOptions interface --- _packages/native-preview/src/api/async/types.ts | 7 ------- _packages/native-preview/src/api/sync/types.ts | 7 ------- 2 files changed, 14 deletions(-) diff --git a/_packages/native-preview/src/api/async/types.ts b/_packages/native-preview/src/api/async/types.ts index 63536fe910..43101feb4a 100644 --- a/_packages/native-preview/src/api/async/types.ts +++ b/_packages/native-preview/src/api/async/types.ts @@ -213,13 +213,6 @@ export interface CompletionOptions { includeSymbol?: boolean; } -/** Options for {@link Checker.getCompletionsAtPosition}. */ -export interface CompletionOptions { - triggerCharacter?: string; - /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ - includeSymbol?: boolean; -} - /** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ export interface CompletionEntry { readonly name: string; diff --git a/_packages/native-preview/src/api/sync/types.ts b/_packages/native-preview/src/api/sync/types.ts index 247a81be33..3433032260 100644 --- a/_packages/native-preview/src/api/sync/types.ts +++ b/_packages/native-preview/src/api/sync/types.ts @@ -221,13 +221,6 @@ export interface CompletionOptions { includeSymbol?: boolean; } -/** Options for {@link Checker.getCompletionsAtPosition}. */ -export interface CompletionOptions { - triggerCharacter?: string; - /** Include a `symbol` property on each completion entry. Only populated for symbol-based completions (not keywords or literals). */ - includeSymbol?: boolean; -} - /** A single completion item returned by {@link Checker.getCompletionsAtPosition}. */ export interface CompletionEntry { readonly name: string;