Merge pull request #28385 from Microsoft/metadata

Add metadata to response if it exists for results from language service.
This commit is contained in:
Sheetal Nandi 2018-11-06 20:12:58 -08:00 committed by GitHub
commit 59a8c94ff1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 24 deletions

View file

@ -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; }[] = [];
@ -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;
}
}
}

View file

@ -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);

View file

@ -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;
}
/**

View file

@ -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);
@ -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);
@ -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,

View file

@ -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
@ -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,

View file

@ -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: `{
@ -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;
});
}
@ -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),

View file

@ -4689,6 +4689,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[];
@ -4706,7 +4709,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;
@ -5788,6 +5791,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.

View file

@ -4689,6 +4689,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[];
@ -4706,7 +4709,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;