Skip to content

Commit 59a8c94

Browse files
authored
Merge pull request #28385 from Microsoft/metadata
Add metadata to response if it exists for results from language service.
2 parents 8e0c436 + f0f0275 commit 59a8c94

File tree

8 files changed

+167
-24
lines changed

8 files changed

+167
-24
lines changed

src/harness/harnessLanguageService.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
namespace Harness.LanguageService {
2+
3+
export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
4+
// tslint:disable-next-line:no-null-keyword
5+
const proxy = Object.create(/*prototype*/ null);
6+
const langSvc: any = info.languageService;
7+
for (const k of Object.keys(langSvc)) {
8+
// tslint:disable-next-line only-arrow-functions
9+
proxy[k] = function () {
10+
return langSvc[k].apply(langSvc, arguments);
11+
};
12+
}
13+
return proxy;
14+
}
15+
216
export class ScriptInfo {
317
public version = 1;
418
public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = [];
@@ -869,19 +883,6 @@ namespace Harness.LanguageService {
869883
error: new Error("Could not resolve module")
870884
};
871885
}
872-
873-
function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
874-
// tslint:disable-next-line:no-null-keyword
875-
const proxy = Object.create(/*prototype*/ null);
876-
const langSvc: any = info.languageService;
877-
for (const k of Object.keys(langSvc)) {
878-
// tslint:disable-next-line only-arrow-functions
879-
proxy[k] = function () {
880-
return langSvc[k].apply(langSvc, arguments);
881-
};
882-
}
883-
return proxy;
884-
}
885886
}
886887
}
887888

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ interface Array<T> {}`
341341
private readonly currentDirectory: string;
342342
private readonly dynamicPriorityWatchFile: HostWatchFile | undefined;
343343
private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined;
344+
public require: (initialPath: string, moduleName: string) => server.RequireResult;
344345

345346
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderorSymLinkList: ReadonlyArray<FileOrFolderOrSymLink>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map<string>) {
346347
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);

src/server/protocol.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ namespace ts.server.protocol {
221221
* Contains message body if success === true.
222222
*/
223223
body?: any;
224+
225+
/**
226+
* Contains extra information that plugin can include to be passed on
227+
*/
228+
metadata?: unknown;
224229
}
225230

226231
/**

src/server/session.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,26 @@ namespace ts.server {
688688
success,
689689
};
690690
if (success) {
691-
res.body = info;
691+
let metadata: unknown;
692+
if (isArray(info)) {
693+
res.body = info;
694+
metadata = (info as WithMetadata<ReadonlyArray<any>>).metadata;
695+
delete (info as WithMetadata<ReadonlyArray<any>>).metadata;
696+
}
697+
else if (typeof info === "object") {
698+
if ((info as WithMetadata<{}>).metadata) {
699+
const { metadata: infoMetadata, ...body } = (info as WithMetadata<{}>);
700+
res.body = body;
701+
metadata = infoMetadata;
702+
}
703+
else {
704+
res.body = info;
705+
}
706+
}
707+
else {
708+
res.body = info;
709+
}
710+
if (metadata) res.metadata = metadata;
692711
}
693712
else {
694713
Debug.assert(info === undefined);
@@ -1467,7 +1486,7 @@ namespace ts.server {
14671486
});
14681487
}
14691488

1470-
private getCompletions(args: protocol.CompletionsRequestArgs, kind: protocol.CommandTypes.CompletionInfo | protocol.CommandTypes.Completions | protocol.CommandTypes.CompletionsFull): ReadonlyArray<protocol.CompletionEntry> | protocol.CompletionInfo | CompletionInfo | undefined {
1489+
private getCompletions(args: protocol.CompletionsRequestArgs, kind: protocol.CommandTypes.CompletionInfo | protocol.CommandTypes.Completions | protocol.CommandTypes.CompletionsFull): WithMetadata<ReadonlyArray<protocol.CompletionEntry>> | protocol.CompletionInfo | CompletionInfo | undefined {
14711490
const { file, project } = this.getFileAndProject(args);
14721491
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
14731492
const position = this.getPosition(args, scriptInfo);
@@ -1492,7 +1511,10 @@ namespace ts.server {
14921511
}
14931512
}).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
14941513

1495-
if (kind === protocol.CommandTypes.Completions) return entries;
1514+
if (kind === protocol.CommandTypes.Completions) {
1515+
if (completions.metadata) (entries as WithMetadata<ReadonlyArray<protocol.CompletionEntry>>).metadata = completions.metadata;
1516+
return entries;
1517+
}
14961518

14971519
const res: protocol.CompletionInfo = {
14981520
...completions,

src/services/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ namespace ts {
238238
/* @internal */
239239
export const emptyOptions = {};
240240

241+
export type WithMetadata<T> = T & { metadata?: unknown; };
242+
241243
//
242244
// Public services of a language service instance associated
243245
// with a language service host instance
@@ -268,7 +270,7 @@ namespace ts {
268270
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
269271
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
270272

271-
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
273+
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
272274
// "options" and "source" are optional only for backwards-compatibility
273275
getCompletionEntryDetails(
274276
fileName: string,

src/testRunner/unittests/tsserverProjectSystem.ts

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ namespace ts.projectSystem {
1717
import safeList = TestFSWithWatch.safeList;
1818
import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory;
1919

20+
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
21+
function mapOutputToJson(s: string) {
22+
return convertToObject(
23+
parseJsonText("json.json", s.replace(outputEventRegex, "")),
24+
[]
25+
);
26+
}
27+
2028
export const customTypesMap = {
2129
path: <Path>"/typesMap.json",
2230
content: `{
@@ -353,12 +361,8 @@ namespace ts.projectSystem {
353361
};
354362

355363
function getEvents() {
356-
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
357364
return mapDefined(host.getOutput(), s => {
358-
const e = convertToObject(
359-
parseJsonText("json.json", s.replace(outputEventRegex, "")),
360-
[]
361-
);
365+
const e = mapOutputToJson(s);
362366
return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
363367
});
364368
}
@@ -10735,6 +10739,104 @@ declare class TestLib {
1073510739
});
1073610740
});
1073710741

10742+
describe("tsserverProjectSystem with metadata in response", () => {
10743+
const metadata = "Extra Info";
10744+
function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) {
10745+
const output = host.getOutput().map(mapOutputToJson);
10746+
assert.deepEqual(output, [expectedResponse]);
10747+
host.clearOutput();
10748+
}
10749+
10750+
function verifyCommandWithMetadata<T extends server.protocol.Request, U = undefined>(session: TestSession, host: TestServerHost, command: Partial<T>, expectedResponseBody: U) {
10751+
command.seq = session.getSeq();
10752+
command.type = "request";
10753+
session.onMessage(JSON.stringify(command));
10754+
verifyOutput(host, expectedResponseBody ?
10755+
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } :
10756+
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." }
10757+
);
10758+
}
10759+
10760+
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` };
10761+
const tsconfig: File = {
10762+
path: "/tsconfig.json",
10763+
content: JSON.stringify({
10764+
compilerOptions: { plugins: [{ name: "myplugin" }] }
10765+
})
10766+
};
10767+
function createHostWithPlugin(files: ReadonlyArray<File>) {
10768+
const host = createServerHost(files);
10769+
host.require = (_initialPath, moduleName) => {
10770+
assert.equal(moduleName, "myplugin");
10771+
return {
10772+
module: () => ({
10773+
create(info: server.PluginCreateInfo) {
10774+
const proxy = Harness.LanguageService.makeDefaultProxy(info);
10775+
proxy.getCompletionsAtPosition = (filename, position, options) => {
10776+
const result = info.languageService.getCompletionsAtPosition(filename, position, options);
10777+
if (result) {
10778+
result.metadata = metadata;
10779+
}
10780+
return result;
10781+
};
10782+
return proxy;
10783+
}
10784+
}),
10785+
error: undefined
10786+
};
10787+
};
10788+
return host;
10789+
}
10790+
10791+
describe("With completion requests", () => {
10792+
const completionRequestArgs: protocol.CompletionsRequestArgs = {
10793+
file: aTs.path,
10794+
line: 1,
10795+
offset: aTs.content.indexOf("this.") + 1 + "this.".length
10796+
};
10797+
const expectedCompletionEntries: ReadonlyArray<protocol.CompletionEntry> = [
10798+
{ name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" },
10799+
{ name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" }
10800+
];
10801+
10802+
it("can pass through metadata when the command returns array", () => {
10803+
const host = createHostWithPlugin([aTs, tsconfig]);
10804+
const session = createSession(host);
10805+
openFilesForSession([aTs], session);
10806+
verifyCommandWithMetadata<protocol.CompletionsRequest, ReadonlyArray<protocol.CompletionEntry>>(session, host, {
10807+
command: protocol.CommandTypes.Completions,
10808+
arguments: completionRequestArgs
10809+
}, expectedCompletionEntries);
10810+
});
10811+
10812+
it("can pass through metadata when the command returns object", () => {
10813+
const host = createHostWithPlugin([aTs, tsconfig]);
10814+
const session = createSession(host);
10815+
openFilesForSession([aTs], session);
10816+
verifyCommandWithMetadata<protocol.CompletionsRequest, protocol.CompletionInfo>(session, host, {
10817+
command: protocol.CommandTypes.CompletionInfo,
10818+
arguments: completionRequestArgs
10819+
}, {
10820+
isGlobalCompletion: false,
10821+
isMemberCompletion: true,
10822+
isNewIdentifierLocation: false,
10823+
entries: expectedCompletionEntries
10824+
});
10825+
});
10826+
10827+
it("returns undefined correctly", () => {
10828+
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` };
10829+
const host = createHostWithPlugin([aTs, tsconfig]);
10830+
const session = createSession(host);
10831+
openFilesForSession([aTs], session);
10832+
verifyCommandWithMetadata<protocol.CompletionsRequest>(session, host, {
10833+
command: protocol.CommandTypes.Completions,
10834+
arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 }
10835+
}, /*expectedResponseBody*/ undefined);
10836+
});
10837+
});
10838+
});
10839+
1073810840
function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
1073910841
return {
1074010842
...protocolFileSpanFromSubstring(file, text, options),

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4689,6 +4689,9 @@ declare namespace ts {
46894689
installPackage?(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
46904690
writeFile?(fileName: string, content: string): void;
46914691
}
4692+
type WithMetadata<T> = T & {
4693+
metadata?: unknown;
4694+
};
46924695
interface LanguageService {
46934696
cleanupSemanticCache(): void;
46944697
getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[];
@@ -4706,7 +4709,7 @@ declare namespace ts {
47064709
getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[];
47074710
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
47084711
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
4709-
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
4712+
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
47104713
getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
47114714
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
47124715
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;
@@ -5788,6 +5791,10 @@ declare namespace ts.server.protocol {
57885791
* Contains message body if success === true.
57895792
*/
57905793
body?: any;
5794+
/**
5795+
* Contains extra information that plugin can include to be passed on
5796+
*/
5797+
metadata?: unknown;
57915798
}
57925799
/**
57935800
* Arguments for FileRequest messages.

tests/baselines/reference/api/typescript.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4689,6 +4689,9 @@ declare namespace ts {
46894689
installPackage?(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
46904690
writeFile?(fileName: string, content: string): void;
46914691
}
4692+
type WithMetadata<T> = T & {
4693+
metadata?: unknown;
4694+
};
46924695
interface LanguageService {
46934696
cleanupSemanticCache(): void;
46944697
getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[];
@@ -4706,7 +4709,7 @@ declare namespace ts {
47064709
getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[];
47074710
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
47084711
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
4709-
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
4712+
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
47104713
getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
47114714
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
47124715
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;

0 commit comments

Comments
 (0)