Merge pull request #28385 from Microsoft/metadata
Add metadata to response if it exists for results from language service.
This commit is contained in:
commit
59a8c94ff1
8 changed files with 167 additions and 24 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue