Skip to content

Add metadata to response if it exists for results from language service. #28385

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

Merged
merged 3 commits into from
Nov 7, 2018
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
27 changes: 14 additions & 13 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
namespace Harness.LanguageService {

export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
// tslint:disable-next-line:no-null-keyword
const proxy = Object.create(/*prototype*/ null);
const langSvc: any = info.languageService;
for (const k of Object.keys(langSvc)) {
// tslint:disable-next-line only-arrow-functions
proxy[k] = function () {
return langSvc[k].apply(langSvc, arguments);
};
}
return proxy;
}

export class ScriptInfo {
public version = 1;
public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = [];
Expand Down Expand Up @@ -869,19 +883,6 @@ namespace Harness.LanguageService {
error: new Error("Could not resolve module")
};
}

function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
// tslint:disable-next-line:no-null-keyword
const proxy = Object.create(/*prototype*/ null);
const langSvc: any = info.languageService;
for (const k of Object.keys(langSvc)) {
// tslint:disable-next-line only-arrow-functions
proxy[k] = function () {
return langSvc[k].apply(langSvc, arguments);
};
}
return proxy;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/harness/virtualFileSystemWithWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ interface Array<T> {}`
private readonly currentDirectory: string;
private readonly dynamicPriorityWatchFile: HostWatchFile | undefined;
private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined;
public require: (initialPath: string, moduleName: string) => server.RequireResult;

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>) {
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
Expand Down
5 changes: 5 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ namespace ts.server.protocol {
* Contains message body if success === true.
*/
body?: any;

/**
* Contains extra information that plugin can include to be passed on
*/
metadata?: unknown;
}

/**
Expand Down
28 changes: 25 additions & 3 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,26 @@ namespace ts.server {
success,
};
if (success) {
res.body = info;
let metadata: unknown;
if (isArray(info)) {
res.body = info;
metadata = (info as WithMetadata<ReadonlyArray<any>>).metadata;
delete (info as WithMetadata<ReadonlyArray<any>>).metadata;
}
else if (typeof info === "object") {
if ((info as WithMetadata<{}>).metadata) {
const { metadata: infoMetadata, ...body } = (info as WithMetadata<{}>);
res.body = body;
metadata = infoMetadata;
}
else {
res.body = info;
}
}
else {
res.body = info;
}
if (metadata) res.metadata = metadata;
}
else {
Debug.assert(info === undefined);
Expand Down Expand Up @@ -1467,7 +1486,7 @@ namespace ts.server {
});
}

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

if (kind === protocol.CommandTypes.Completions) return entries;
if (kind === protocol.CommandTypes.Completions) {
if (completions.metadata) (entries as WithMetadata<ReadonlyArray<protocol.CompletionEntry>>).metadata = completions.metadata;
return entries;
}

const res: protocol.CompletionInfo = {
...completions,
Expand Down
4 changes: 3 additions & 1 deletion src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ namespace ts {
/* @internal */
export const emptyOptions = {};

export type WithMetadata<T> = T & { metadata?: unknown; };

//
// Public services of a language service instance associated
// with a language service host instance
Expand Down Expand Up @@ -268,7 +270,7 @@ namespace ts {
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;

getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
// "options" and "source" are optional only for backwards-compatibility
getCompletionEntryDetails(
fileName: string,
Expand Down
112 changes: 107 additions & 5 deletions src/testRunner/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ namespace ts.projectSystem {
import safeList = TestFSWithWatch.safeList;
import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory;

const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
function mapOutputToJson(s: string) {
return convertToObject(
parseJsonText("json.json", s.replace(outputEventRegex, "")),
[]
);
}

export const customTypesMap = {
path: <Path>"/typesMap.json",
content: `{
Expand Down Expand Up @@ -353,12 +361,8 @@ namespace ts.projectSystem {
};

function getEvents() {
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
return mapDefined(host.getOutput(), s => {
const e = convertToObject(
parseJsonText("json.json", s.replace(outputEventRegex, "")),
[]
);
const e = mapOutputToJson(s);
return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
});
}
Expand Down Expand Up @@ -10735,6 +10739,104 @@ declare class TestLib {
});
});

describe("tsserverProjectSystem with metadata in response", () => {
const metadata = "Extra Info";
function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) {
const output = host.getOutput().map(mapOutputToJson);
assert.deepEqual(output, [expectedResponse]);
host.clearOutput();
}

function verifyCommandWithMetadata<T extends server.protocol.Request, U = undefined>(session: TestSession, host: TestServerHost, command: Partial<T>, expectedResponseBody: U) {
command.seq = session.getSeq();
command.type = "request";
session.onMessage(JSON.stringify(command));
verifyOutput(host, expectedResponseBody ?
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } :
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." }
);
}

const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` };
const tsconfig: File = {
path: "/tsconfig.json",
content: JSON.stringify({
compilerOptions: { plugins: [{ name: "myplugin" }] }
})
};
function createHostWithPlugin(files: ReadonlyArray<File>) {
const host = createServerHost(files);
host.require = (_initialPath, moduleName) => {
assert.equal(moduleName, "myplugin");
return {
module: () => ({
create(info: server.PluginCreateInfo) {
const proxy = Harness.LanguageService.makeDefaultProxy(info);
proxy.getCompletionsAtPosition = (filename, position, options) => {
const result = info.languageService.getCompletionsAtPosition(filename, position, options);
if (result) {
result.metadata = metadata;
}
return result;
};
return proxy;
}
}),
error: undefined
};
};
return host;
}

describe("With completion requests", () => {
const completionRequestArgs: protocol.CompletionsRequestArgs = {
file: aTs.path,
line: 1,
offset: aTs.content.indexOf("this.") + 1 + "this.".length
};
const expectedCompletionEntries: ReadonlyArray<protocol.CompletionEntry> = [
{ name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" },
{ name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" }
];

it("can pass through metadata when the command returns array", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, ReadonlyArray<protocol.CompletionEntry>>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: completionRequestArgs
}, expectedCompletionEntries);
});

it("can pass through metadata when the command returns object", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, protocol.CompletionInfo>(session, host, {
command: protocol.CommandTypes.CompletionInfo,
arguments: completionRequestArgs
}, {
isGlobalCompletion: false,
isMemberCompletion: true,
isNewIdentifierLocation: false,
entries: expectedCompletionEntries
});
});

it("returns undefined correctly", () => {
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` };
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 }
}, /*expectedResponseBody*/ undefined);
});
});
});

function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
return {
...protocolFileSpanFromSubstring(file, text, options),
Expand Down
9 changes: 8 additions & 1 deletion tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4688,6 +4688,9 @@ declare namespace ts {
installPackage?(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
writeFile?(fileName: string, content: string): void;
}
type WithMetadata<T> = T & {
metadata?: unknown;
};
interface LanguageService {
cleanupSemanticCache(): void;
getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[];
Expand All @@ -4705,7 +4708,7 @@ declare namespace ts {
getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[];
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;
Expand Down Expand Up @@ -5787,6 +5790,10 @@ declare namespace ts.server.protocol {
* Contains message body if success === true.
*/
body?: any;
/**
* Contains extra information that plugin can include to be passed on
*/
metadata?: unknown;
}
/**
* Arguments for FileRequest messages.
Expand Down
5 changes: 4 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4688,6 +4688,9 @@ declare namespace ts {
installPackage?(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult>;
writeFile?(fileName: string, content: string): void;
}
type WithMetadata<T> = T & {
metadata?: unknown;
};
interface LanguageService {
cleanupSemanticCache(): void;
getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[];
Expand All @@ -4705,7 +4708,7 @@ declare namespace ts {
getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[];
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications;
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications;
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): CompletionInfo | undefined;
getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions | undefined): WithMetadata<CompletionInfo> | undefined;
getCompletionEntryDetails(fileName: string, position: number, name: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined;
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined;
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined;
Expand Down