From 23da1cf822c6965fdb34ae3df8a72ca174ea3137 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 18:28:50 -0800 Subject: [PATCH 01/29] send all events through common stream --- src/server/editorServices.ts | 4 +- src/server/server.ts | 66 ++++++++--------------- src/server/session.ts | 100 ++++++++++++++++++++++++++--------- src/server/typingsCache.ts | 2 +- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2205f65cbe4..d7ee0af7ef9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,6 +16,7 @@ namespace ts.server { export const ProjectInfoTelemetryEvent = "projectInfo"; // tslint:enable variable-name + // TODO: make these inherit from protocol.Event? export interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { openFiles: string[]; }; @@ -320,6 +321,7 @@ namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + eventSender?: EventSender; } type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; @@ -436,7 +438,7 @@ namespace ts.server { this.loadTypesMap(); } - this.typingsInstaller.attach(this); + this.typingsInstaller.attach(this, opts.eventSender); this.typingsCache = new TypingsCache(this.typingsInstaller); diff --git a/src/server/server.ts b/src/server/server.ts index f4faa0d3c77..97eba991884 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,3 @@ -/// /// /// @@ -7,7 +6,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - installerEventPort: number; + eventPort: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; @@ -22,10 +21,6 @@ namespace ts.server { allowLocalPluginLoads: boolean; } - const net: { - connect(options: { port: number }, onConnect?: () => void): NodeSocket - } = require("net"); - const childProcess: { fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike }): NodeChildProcess; execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike }): string | Buffer; @@ -83,10 +78,6 @@ namespace ts.server { pid: number; } - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - interface ReadLineOptions { input: NodeJS.ReadableStream; output?: NodeJS.WritableStream; @@ -244,9 +235,8 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; private installerPidReported = false; - private socket: NodeSocket; private projectService: ProjectService; - private eventSender: EventSender; + private eventSender: EventSender | undefined; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -267,18 +257,10 @@ namespace ts.server { private readonly telemetryEnabled: boolean, private readonly logger: server.Logger, private readonly host: ServerHost, - eventPort: number, readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, - private readonly npmLocation: string | undefined, - private newLine: string) { - if (eventPort) { - const s = net.connect({ port: eventPort }, () => { - this.socket = s; - this.reportInstallerProcessId(); - }); - } + private readonly npmLocation: string | undefined) { } isKnownTypesPackageName(name: string): boolean { @@ -310,26 +292,23 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.socket && this.installer) { - this.sendEvent(0, "typingsInstallerPid", { pid: this.installer.pid }); + if (this.installer && this.eventSender) { + this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } } - private sendEvent(seq: number, event: string, body: any): void { - this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8"); - } - setTelemetrySender(telemetrySender: EventSender) { - this.eventSender = telemetrySender; - } - - attach(projectService: ProjectService) { + attach(projectService: ProjectService, eventSender?: EventSender) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } + if (eventSender) { + this.eventSender = eventSender; + } + const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; if (this.telemetryEnabled) { args.push(Arguments.EnableTelemetry); @@ -353,10 +332,10 @@ namespace ts.server { if (match) { // if port is specified - use port + 1 // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1 - const currentPort = match[2] !== undefined - ? +match[2] - : match[1].charAt(0) === "d" ? 5858 : 9229; - execArgv.push(`--${match[1]}=${currentPort + 1}`); + // const currentPort = match[2] !== undefined + // ? +match[2] + // : match[1].charAt(0) === "d" ? 5858 : 9229; + // execArgv.push(`--${match[1]}=${currentPort + 1}`); break; } } @@ -508,8 +487,8 @@ namespace ts.server { this.projectService.updateTypingsForProject(response); - if (this.socket) { - this.sendEvent(0, "setTypings", response); + if (this.eventSender) { + this.eventSender.event(response, "setTypings"); } break; @@ -530,10 +509,10 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { - const { host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, host.newLine); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); super({ host, @@ -545,13 +524,10 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, + eventPort, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); - - if (telemetryEnabled && typingsInstaller) { - typingsInstaller.setTelemetrySender(this); - } } exit() { @@ -936,8 +912,8 @@ namespace ts.server { const options: IoSessionOptions = { host: sys, cancellationToken, - installerEventPort: eventPort, - canUseEvents: eventPort === undefined, + eventPort, + canUseEvents: true, useSingleInferredProject, useInferredProjectPerProjectRoot, disableAutomaticTypingAcquisition, diff --git a/src/server/session.ts b/src/server/session.ts index 6c97c3c8bd4..b728ea6d552 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,9 +1,19 @@ +/// /// /// /// /// namespace ts.server { + + interface NodeSocket { + write(data: string, encoding: string): boolean; + } + + const net: { + connect(options: { port: number }, onConnect?: () => void): NodeSocket + } = require("net"); + interface StackTraceError extends Error { stack?: string; } @@ -253,6 +263,10 @@ namespace ts.server { hrtime: (start?: number[]) => number[]; logger: Logger; canUseEvents: boolean; + /** + * If defined, the Session will send events through `eventPort` instead of stdout. + */ + eventPort?: number; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -269,15 +283,19 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private eventHandler: ProjectServiceEventHandler; - private host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; private byteLength: (buf: string, encoding?: string) => number; private hrtime: (start?: number[]) => number[]; protected logger: Logger; + private canUseEvents: boolean; + private eventPort: number | undefined; + private eventSocket: NodeSocket; + private eventHandler: ProjectServiceEventHandler; + public readonly event: EventSender["event"]; + private socketEventQueue: { info: any, eventName: string}[] | undefined; constructor(opts: SessionOptions) { this.host = opts.host; @@ -286,14 +304,49 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; + this.eventPort = opts.eventPort; this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; + if (!this.canUseEvents) { + this.event = noop; + } + else if (this.eventPort) { + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + this.clearSocketEventQueue(); + }); + + this.event = function (info: T, eventName: string) { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ info, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(info, eventName); + } + }; + } + else { + this.event = function (info: T, eventName: string) { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body: info + }; + this.send(ev); + }; + } + this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; - const multistepOperationHost: MultistepOperationHost = { executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action), getCurrentRequestId: () => this.currentRequestId, @@ -314,20 +367,26 @@ namespace ts.server { eventHandler: this.eventHandler, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, - allowLocalPluginLoads: opts.allowLocalPluginLoads + allowLocalPluginLoads: opts.allowLocalPluginLoads, + eventSender: this }; this.projectService = new ProjectService(settings); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } + private clearSocketEventQueue() { + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.info, event.eventName); + } + this.socketEventQueue = undefined; + } + + private writeToEventSocket(info: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + private sendRequestCompletedEvent(requestId: number): void { - const event: protocol.RequestCompletedEvent = { - seq: 0, - type: "event", - event: "requestCompleted", - body: { request_seq: requestId } - }; - this.send(event); + this.event({ request_seq: requestId }, "requestCompleted"); } private defaultEventHandler(event: ProjectServiceEvent) { @@ -392,26 +451,15 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event" && !this.canUseEvents) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; + if (msg.type === "event") { + Debug.assert(this.canUseEvents); + Debug.assert(!this.eventPort); } this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } - public event(info: T, eventName: string) { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body: info - }; - this.send(ev); - } - // For backwards-compatibility only. + /** @deprecated */ public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg); } diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index cde303bfd39..6fbf0939431 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -10,7 +10,7 @@ namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService): void; + attach(projectService: ProjectService, eventSender?: EventSender): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } From 0d3002c5f0ae1d3a34e238dfda5fc1a9c24919d9 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 22:17:05 -0800 Subject: [PATCH 02/29] accept baselines --- .../reference/api/tsserverlibrary.d.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a3499de08ab..59e289ed4a2 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6918,6 +6918,10 @@ declare namespace ts.server { hrtime: (start?: number[]) => number[]; logger: Logger; canUseEvents: boolean; + /** + * If defined, the Session will send events through `eventPort` instead of stdout. + */ + eventPort?: number; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6930,7 +6934,6 @@ declare namespace ts.server { private changeSeq; private currentRequestId; private errorCheck; - private eventHandler; private host; private readonly cancellationToken; protected readonly typingsInstaller: ITypingsInstaller; @@ -6938,13 +6941,20 @@ declare namespace ts.server { private hrtime; protected logger: Logger; private canUseEvents; + private eventPort; + private eventSocket; + private eventHandler; + readonly event: EventSender["event"]; + private socketEventQueue; constructor(opts: SessionOptions); + private clearSocketEventQueue(); + private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); private projectsUpdatedInBackgroundEvent(openFiles); logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; - event(info: T, eventName: string): void; + /** @deprecated */ output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; private doOutput(info, cmdName, reqSeq, success, message?); private semanticCheck(file, project); @@ -7102,7 +7112,7 @@ declare namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService): void; + attach(projectService: ProjectService, eventSender?: EventSender): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } @@ -7461,6 +7471,7 @@ declare namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; + eventSender?: EventSender; } class ProjectService { readonly typingsCache: TypingsCache; From 9f991d375a95c399f5d0f672b0a382851de46b50 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 8 Nov 2017 23:03:16 -0800 Subject: [PATCH 03/29] cleanup --- src/server/editorServices.ts | 1 - src/server/server.ts | 10 ++++---- src/server/session.ts | 23 ++++++++++--------- .../reference/api/tsserverlibrary.d.ts | 1 - 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d7ee0af7ef9..07726aeba29 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -16,7 +16,6 @@ namespace ts.server { export const ProjectInfoTelemetryEvent = "projectInfo"; // tslint:enable variable-name - // TODO: make these inherit from protocol.Event? export interface ProjectsUpdatedInBackgroundEvent { eventName: typeof ProjectsUpdatedInBackgroundEvent; data: { openFiles: string[]; }; diff --git a/src/server/server.ts b/src/server/server.ts index 97eba991884..b9dde6c1ed6 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -292,7 +292,7 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.installer && this.eventSender) { + if (this.eventSender && this.installer) { this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } @@ -332,10 +332,10 @@ namespace ts.server { if (match) { // if port is specified - use port + 1 // otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1 - // const currentPort = match[2] !== undefined - // ? +match[2] - // : match[1].charAt(0) === "d" ? 5858 : 9229; - // execArgv.push(`--${match[1]}=${currentPort + 1}`); + const currentPort = match[2] !== undefined + ? +match[2] + : match[1].charAt(0) === "d" ? 5858 : 9229; + execArgv.push(`--${match[1]}=${currentPort + 1}`); break; } } diff --git a/src/server/session.ts b/src/server/session.ts index b728ea6d552..5fe38fe64c8 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -315,7 +315,13 @@ namespace ts.server { else if (this.eventPort) { const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; - this.clearSocketEventQueue(); + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.info, event.eventName); + } + this.socketEventQueue = undefined; + } }); this.event = function (info: T, eventName: string) { @@ -374,13 +380,6 @@ namespace ts.server { this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } - private clearSocketEventQueue() { - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.info, event.eventName); - } - this.socketEventQueue = undefined; - } - private writeToEventSocket(info: any, eventName: string): void { this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); } @@ -451,9 +450,11 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event") { - Debug.assert(this.canUseEvents); - Debug.assert(!this.eventPort); + if (msg.type === "event" && !this.canUseEvents) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; } this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 59e289ed4a2..5c1d6401c1e 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6947,7 +6947,6 @@ declare namespace ts.server { readonly event: EventSender["event"]; private socketEventQueue; constructor(opts: SessionOptions); - private clearSocketEventQueue(); private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); From 1746f4b3d36a5ebbfce5bb7c5f6963eeec32fca8 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 15:10:44 -0800 Subject: [PATCH 04/29] factor out socket-event-sending --- src/server/editorServices.ts | 4 +- src/server/server.ts | 4 +- src/server/session.ts | 105 +++++++++++++++++++++-------------- 3 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 07726aeba29..610903135cf 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1441,7 +1441,9 @@ namespace ts.server { } this.seenProjects.set(projectKey, true); - if (!this.eventHandler) return; + if (!this.eventHandler) { + return; + } const data: ProjectInfoTelemetryEventData = { projectId: this.host.createHash(projectKey), diff --git a/src/server/server.ts b/src/server/server.ts index b9dde6c1ed6..8acc42c76cb 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -6,7 +6,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - eventPort: number; + eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; @@ -872,7 +872,7 @@ namespace ts.server { cancellationToken = nullCancellationToken; } - let eventPort: number; + let eventPort: number | undefined; { const str = findArgument("--eventPort"); const v = str && parseInt(str); diff --git a/src/server/session.ts b/src/server/session.ts index 5fe38fe64c8..13f65a8cb4e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -115,10 +115,6 @@ namespace ts.server { project: Project; } - export interface EventSender { - event(payload: T, eventName: string): void; - } - function allEditsBeforePos(edits: ts.TextChange[], pos: number) { for (const edit of edits) { if (textSpanEnd(edit.span) >= pos) { @@ -253,6 +249,55 @@ namespace ts.server { } } + export type Event = (body: T, eventName: string) => void; + + export interface EventSender { + event: Event; + } + + class SocketEventSender implements EventSender { + private host: ServerHost; + private logger: Logger; + private eventPort: number; + private eventSocket: NodeSocket; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + + constructor(host: ServerHost, logger: Logger, eventPort: number) { + this.host = host; + this.logger = logger; + this.eventPort = eventPort; + + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + public event(body: T, eventName: string): void { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -262,9 +307,13 @@ namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; /** - * If defined, the Session will send events through `eventPort` instead of stdout. + * If defined, specifies the socket to send events to the client. + * Otherwise, events are sent through the host. */ eventPort?: number; eventHandler?: ProjectServiceEventHandler; @@ -276,6 +325,8 @@ namespace ts.server { } export class Session implements EventSender { + public readonly event: Event; + private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -292,10 +343,7 @@ namespace ts.server { private canUseEvents: boolean; private eventPort: number | undefined; - private eventSocket: NodeSocket; private eventHandler: ProjectServiceEventHandler; - public readonly event: EventSender["event"]; - private socketEventQueue: { info: any, eventName: string}[] | undefined; constructor(opts: SessionOptions) { this.host = opts.host; @@ -308,43 +356,18 @@ namespace ts.server { this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; - - if (!this.canUseEvents) { - this.event = noop; - } - else if (this.eventPort) { - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.info, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - - this.event = function (info: T, eventName: string) { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ info, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(info, eventName); - } - }; + + if (this.eventPort && this.canUseEvents) { + const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); + this.event = eventSender.event; } else { - this.event = function (info: T, eventName: string) { + this.event = function (body: T, eventName: string): void { const ev: protocol.Event = { seq: 0, type: "event", event: eventName, - body: info + body }; this.send(ev); }; @@ -380,10 +403,6 @@ namespace ts.server { this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); } - private writeToEventSocket(info: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: info }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); - } - private sendRequestCompletedEvent(requestId: number): void { this.event({ request_seq: requestId }, "requestCompleted"); } From c453e081354f7b3449a487cfec2654b7ef5339c9 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 15:10:58 -0800 Subject: [PATCH 05/29] accept baseline --- .../reference/api/tsserverlibrary.d.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5c1d6401c1e..467bce5db6c 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6902,12 +6902,13 @@ declare namespace ts.server { fileName: NormalizedPath; project: Project; } - interface EventSender { - event(payload: T, eventName: string): void; - } type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; + type Event = (body: T, eventName: string) => void; + interface EventSender { + event: Event; + } interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -6917,9 +6918,13 @@ declare namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; /** - * If defined, the Session will send events through `eventPort` instead of stdout. + * If defined, specifies the socket to send events to the client. + * Otherwise, events are sent through the host. */ eventPort?: number; eventHandler?: ProjectServiceEventHandler; @@ -6929,6 +6934,7 @@ declare namespace ts.server { allowLocalPluginLoads?: boolean; } class Session implements EventSender { + readonly event: Event; private readonly gcTimer; protected projectService: ProjectService; private changeSeq; @@ -6942,12 +6948,8 @@ declare namespace ts.server { protected logger: Logger; private canUseEvents; private eventPort; - private eventSocket; private eventHandler; - readonly event: EventSender["event"]; - private socketEventQueue; constructor(opts: SessionOptions); - private writeToEventSocket(info, eventName); private sendRequestCompletedEvent(requestId); private defaultEventHandler(event); private projectsUpdatedInBackgroundEvent(openFiles); From bf8f9be6775234ba6405236cb9319e0bd9cb87f0 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 16:42:18 -0800 Subject: [PATCH 06/29] expose event to overwrite in SessionOptions * need to retain eventPort and canUseEvents arguments and functionality for backwards compatibility. --- src/server/server.ts | 8 ++++++++ src/server/session.ts | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 8acc42c76cb..22112c4cb7d 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -510,6 +510,13 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + + let event: Event; + if (canUseEvents && eventPort) { + const eventSender = new SocketEventSender(host, logger, eventPort); + event = eventSender.event; + } + const typingsInstaller = disableAutomaticTypingAcquisition ? undefined : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); @@ -525,6 +532,7 @@ namespace ts.server { logger, canUseEvents, eventPort, + event, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index 13f65a8cb4e..8ad9f7b245b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -255,7 +255,8 @@ namespace ts.server { event: Event; } - class SocketEventSender implements EventSender { + /** @internal */ + export class SocketEventSender implements EventSender { private host: ServerHost; private logger: Logger; private eventPort: number; @@ -316,6 +317,11 @@ namespace ts.server { * Otherwise, events are sent through the host. */ eventPort?: number; + /** + * An optional callback overriding the default behavior for sending events. + * if set, `canUseEvents` and `eventPort` are ignored. + */ + event?: Event; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -353,11 +359,14 @@ namespace ts.server { this.hrtime = opts.hrtime; this.logger = opts.logger; this.eventPort = opts.eventPort; - this.canUseEvents = opts.canUseEvents; + this.canUseEvents = opts.canUseEvents || !!opts.event; const { throttleWaitMilliseconds } = opts; - - if (this.eventPort && this.canUseEvents) { + + if (opts.event) { + this.event = opts.event; + } + else if (this.eventPort && this.canUseEvents) { const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); this.event = eventSender.event; } From ab332cffac8aeaf0e06dca7a112575d975ab613f Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 16:44:04 -0800 Subject: [PATCH 07/29] update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 467bce5db6c..1dca7c720b9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6927,6 +6927,11 @@ declare namespace ts.server { * Otherwise, events are sent through the host. */ eventPort?: number; + /** + * An optional callback overriding the default behavior for sending events. + * if set, `canUseEvents` and `eventPort` are ignored. + */ + event?: Event; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; From 6a910919acd9731316b4ddae6cbff6d8ee16f4fd Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 18:09:32 -0800 Subject: [PATCH 08/29] use arrow to capture this --- src/server/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 9664b830bc6..65d113327bd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -260,7 +260,7 @@ namespace ts.server { private host: ServerHost; private logger: Logger; private eventPort: number; - private eventSocket: NodeSocket; + private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; constructor(host: ServerHost, logger: Logger, eventPort: number) { @@ -280,7 +280,7 @@ namespace ts.server { }); } - public event(body: T, eventName: string): void { + public event = (body: T, eventName: string) => { if (!this.eventSocket) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); From 930be75e0ca4f19a4ff4988f0c8cd4c22183e536 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 18:14:15 -0800 Subject: [PATCH 09/29] lint --- src/server/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 22112c4cb7d..2fa84e04dcb 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -510,13 +510,13 @@ namespace ts.server { class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - + let event: Event; if (canUseEvents && eventPort) { const eventSender = new SocketEventSender(host, logger, eventPort); event = eventSender.event; } - + const typingsInstaller = disableAutomaticTypingAcquisition ? undefined : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); From 93ff7ba2c5b4234be1c02f71beb89880283ea487 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Nov 2017 22:27:01 -0800 Subject: [PATCH 10/29] lint more --- src/server/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/session.ts b/src/server/session.ts index 65d113327bd..5912536f4f3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -295,7 +295,7 @@ namespace ts.server { } private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body: body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); } } From 005c86340f02fe904a410ef10ff71db4e45c5841 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 13:12:48 -0800 Subject: [PATCH 11/29] move socketSender to IOSession --- src/server/server.ts | 56 ++++++++++++++++++++++++++++++++++++- src/server/session.ts | 65 ------------------------------------------- 2 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 2fa84e04dcb..68909227ac8 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -6,6 +6,10 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; + /** + * If defined, specifies the socket used to send events to the client. + * Otherwise, events are sent through the host. + */ eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; @@ -31,6 +35,14 @@ namespace ts.server { tmpdir(): string; } = require("os"); + interface NodeSocket { + write(data: string, encoding: string): boolean; + } + + const net: { + connect(options: { port: number }, onConnect?: () => void): NodeSocket + } = require("net"); + function getGlobalTypingsCacheLocation() { switch (process.platform) { case "win32": { @@ -507,6 +519,49 @@ namespace ts.server { } } + class SocketEventSender implements EventSender { + private host: ServerHost; + private logger: Logger; + private eventPort: number; + private eventSocket: NodeSocket | undefined; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + + constructor(host: ServerHost, logger: Logger, eventPort: number) { + this.host = host; + this.logger = logger; + this.eventPort = eventPort; + + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + public event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + } + } + class IOSession extends Session { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; @@ -531,7 +586,6 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - eventPort, event, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, diff --git a/src/server/session.ts b/src/server/session.ts index 5912536f4f3..73575672bec 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,19 +1,9 @@ -/// /// /// /// /// namespace ts.server { - - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - - const net: { - connect(options: { port: number }, onConnect?: () => void): NodeSocket - } = require("net"); - interface StackTraceError extends Error { stack?: string; } @@ -255,50 +245,6 @@ namespace ts.server { event: Event; } - /** @internal */ - export class SocketEventSender implements EventSender { - private host: ServerHost; - private logger: Logger; - private eventPort: number; - private eventSocket: NodeSocket | undefined; - private socketEventQueue: { body: any, eventName: string }[] | undefined; - - constructor(host: ServerHost, logger: Logger, eventPort: number) { - this.host = host; - this.logger = logger; - this.eventPort = eventPort; - - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.body, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - } - - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - } - - private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); - } - } - export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -312,11 +258,6 @@ namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * If defined, specifies the socket to send events to the client. - * Otherwise, events are sent through the host. - */ - eventPort?: number; /** * An optional callback overriding the default behavior for sending events. * if set, `canUseEvents` and `eventPort` are ignored. @@ -348,7 +289,6 @@ namespace ts.server { protected logger: Logger; private canUseEvents: boolean; - private eventPort: number | undefined; private eventHandler: ProjectServiceEventHandler; constructor(opts: SessionOptions) { @@ -358,7 +298,6 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; - this.eventPort = opts.eventPort; this.canUseEvents = opts.canUseEvents || !!opts.event; const { throttleWaitMilliseconds } = opts; @@ -366,10 +305,6 @@ namespace ts.server { if (opts.event) { this.event = opts.event; } - else if (this.eventPort && this.canUseEvents) { - const eventSender = new SocketEventSender(this.host, this.logger, this.eventPort); - this.event = eventSender.event; - } else { this.event = function (body: T, eventName: string): void { const ev: protocol.Event = { From d6c3a15ea62c72b54168ca6d64f93eff4be36aa7 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 18:51:25 -0800 Subject: [PATCH 12/29] Move event into IoSession --- src/server/editorServices.ts | 3 +- src/server/server.ts | 67 ++++++++++++------------- src/server/session.ts | 94 +++++++++++++++++++++++++----------- src/server/typingsCache.ts | 2 +- 4 files changed, 97 insertions(+), 69 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 18231e0632e..093437e5565 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -320,7 +320,6 @@ namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; - eventSender?: EventSender; } type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; @@ -441,7 +440,7 @@ namespace ts.server { this.logger.info("No types map provided; using the default"); } - this.typingsInstaller.attach(this, opts.eventSender); + this.typingsInstaller.attach(this); this.typingsCache = new TypingsCache(this.typingsInstaller); diff --git a/src/server/server.ts b/src/server/server.ts index 68909227ac8..2f5061bc715 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,9 +7,9 @@ namespace ts.server { cancellationToken: ServerCancellationToken; canUseEvents: boolean; /** - * If defined, specifies the socket used to send events to the client. - * Otherwise, events are sent through the host. - */ + * If defined, specifies the socket used to send events to the client. + * Otherwise, events are sent through the host. + */ eventPort?: number; useSingleInferredProject: boolean; useInferredProjectPerProjectRoot: boolean; @@ -248,7 +248,6 @@ namespace ts.server { private installer: NodeChildProcess; private installerPidReported = false; private projectService: ProjectService; - private eventSender: EventSender | undefined; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -272,7 +271,11 @@ namespace ts.server { readonly globalTypingsCacheLocation: string, readonly typingSafeListLocation: string, readonly typesMapLocation: string, - private readonly npmLocation: string | undefined) { + private readonly npmLocation: string | undefined, + /** + * If undefined, event-related work will be suppressed. + */ + private eventSender: EventSender | undefined) { } isKnownTypesPackageName(name: string): boolean { @@ -311,16 +314,12 @@ namespace ts.server { } - attach(projectService: ProjectService, eventSender?: EventSender) { + attach(projectService: ProjectService) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { this.logger.info("Binding..."); } - if (eventSender) { - this.eventSender = eventSender; - } - const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation]; if (this.telemetryEnabled) { args.push(Arguments.EnableTelemetry); @@ -519,17 +518,15 @@ namespace ts.server { } } - class SocketEventSender implements EventSender { - private host: ServerHost; - private logger: Logger; - private eventPort: number; + class SocketEventSender extends DefaultMessageSender { private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; - constructor(host: ServerHost, logger: Logger, eventPort: number) { - this.host = host; - this.logger = logger; - this.eventPort = eventPort; + constructor(host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + private eventPort: number) { + super(host, byteLength, logger, /*canUseEvents*/ true); const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; @@ -541,20 +538,20 @@ namespace ts.server { this.socketEventQueue = undefined; } }); - } - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + this.event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + }; } private writeToEventSocket(body: any, eventName: string): void { @@ -566,15 +563,11 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - let event: Event; - if (canUseEvents && eventPort) { - const eventSender = new SocketEventSender(host, logger, eventPort); - event = eventSender.event; - } + const messageSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultMessageSender(host, Buffer.byteLength, logger, canUseEvents); const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? messageSender : undefined); super({ host, @@ -586,7 +579,7 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - event, + messageSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index 73575672bec..ff41138ae29 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -245,6 +245,58 @@ namespace ts.server { event: Event; } + export type Send = (msg: protocol.Message) => void; + + export interface MessageSender extends EventSender { + send: Send; + event: Event; + } + + function defaultSend( + host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + canUseEvents: boolean, + msg: protocol.Message) { + if (msg.type === "event" && !canUseEvents) { + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; + } + host.write(formatMessage(msg, logger, byteLength, host.newLine)); + } + + function defaultEvent( + host: ServerHost, + byteLength: (buf: string, encoding?: string) => number, + logger: Logger, + canUseEvents: boolean, + body: T, eventName: string): void { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + defaultSend(host, byteLength, logger, canUseEvents, ev); + } + + export class DefaultMessageSender implements MessageSender { + constructor(protected host: ServerHost, + protected byteLength: (buf: string, encoding?: string) => number, + protected logger: Logger, + protected canUseEvents: boolean) { } + + public send = (msg: protocol.Message) => { + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + } + + public event = (body: T, eventName: string) => { + defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); + } + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -259,10 +311,9 @@ namespace ts.server { */ canUseEvents: boolean; /** - * An optional callback overriding the default behavior for sending events. - * if set, `canUseEvents` and `eventPort` are ignored. + * An optional callback overriding the default behavior for sending messages. */ - event?: Event; + messageSender?: MessageSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -271,9 +322,7 @@ namespace ts.server { allowLocalPluginLoads?: boolean; } - export class Session implements EventSender { - public readonly event: Event; - + export class Session implements MessageSender { private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -298,23 +347,13 @@ namespace ts.server { this.byteLength = opts.byteLength; this.hrtime = opts.hrtime; this.logger = opts.logger; - this.canUseEvents = opts.canUseEvents || !!opts.event; + this.canUseEvents = opts.canUseEvents; const { throttleWaitMilliseconds } = opts; - if (opts.event) { - this.event = opts.event; - } - else { - this.event = function (body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - this.send(ev); - }; + if (opts.messageSender) { + this.send = opts.messageSender.send; + this.event = opts.messageSender.event; } this.eventHandler = this.canUseEvents @@ -340,8 +379,7 @@ namespace ts.server { eventHandler: this.eventHandler, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, - allowLocalPluginLoads: opts.allowLocalPluginLoads, - eventSender: this + allowLocalPluginLoads: opts.allowLocalPluginLoads }; this.projectService = new ProjectService(settings); this.gcTimer = new GcTimer(this.host, /*delay*/ 7000, this.logger); @@ -413,13 +451,11 @@ namespace ts.server { } public send(msg: protocol.Message) { - if (msg.type === "event" && !this.canUseEvents) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; - } - this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + } + + public event(body: T, eventName: string): void { + defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); } // For backwards-compatibility only. diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 6fbf0939431..cde303bfd39 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -10,7 +10,7 @@ namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService, eventSender?: EventSender): void; + attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } From a7822c550d3d05fdab947d723999144a3806cce4 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:39:02 -0800 Subject: [PATCH 13/29] expose just eventSender --- src/server/server.ts | 62 +++++++++++++++++++++++++++---------------- src/server/session.ts | 62 ++++++++++--------------------------------- 2 files changed, 53 insertions(+), 71 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 2f5061bc715..caf70b9c94a 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -518,15 +518,31 @@ namespace ts.server { } } - class SocketEventSender extends DefaultMessageSender { + export class DefaultEventSender implements EventSender { + constructor(protected host: ServerHost, + protected byteLength: (buf: string, encoding?: string) => number, + protected logger: Logger, + protected canUseEvents: boolean) { } + + public event = (body: T, eventName: string) => { + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); + } + } + + class SocketEventSender implements EventSender { private eventSocket: NodeSocket | undefined; private socketEventQueue: { body: any, eventName: string }[] | undefined; - constructor(host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, + constructor(private host: ServerHost, + private byteLength: (buf: string, encoding?: string) => number, + private logger: Logger, private eventPort: number) { - super(host, byteLength, logger, /*canUseEvents*/ true); const s = net.connect({ port: this.eventPort }, () => { this.eventSocket = s; @@ -538,24 +554,24 @@ namespace ts.server { this.socketEventQueue = undefined; } }); - - this.event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - }; } + public event = (body: T, eventName: string) => { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + }; + private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, Buffer.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); } } @@ -563,11 +579,11 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const messageSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultMessageSender(host, Buffer.byteLength, logger, canUseEvents); + const eventSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultEventSender(host, Buffer.byteLength, logger, canUseEvents); const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? messageSender : undefined); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? eventSender : undefined); super({ host, @@ -579,7 +595,7 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - messageSender, + eventSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, allowLocalPluginLoads: options.allowLocalPluginLoads }); diff --git a/src/server/session.ts b/src/server/session.ts index ff41138ae29..e917b131bf1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -239,20 +239,12 @@ namespace ts.server { } } - export type Event = (body: T, eventName: string) => void; - export interface EventSender { - event: Event; + event: (body: T, eventName: string) => void; } - export type Send = (msg: protocol.Message) => void; - - export interface MessageSender extends EventSender { - send: Send; - event: Event; - } - - function defaultSend( + /** @internal */ + export function defaultSend( host: ServerHost, byteLength: (buf: string, encoding?: string) => number, logger: Logger, @@ -266,37 +258,6 @@ namespace ts.server { } host.write(formatMessage(msg, logger, byteLength, host.newLine)); } - - function defaultEvent( - host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, - canUseEvents: boolean, - body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - defaultSend(host, byteLength, logger, canUseEvents, ev); - } - - export class DefaultMessageSender implements MessageSender { - constructor(protected host: ServerHost, - protected byteLength: (buf: string, encoding?: string) => number, - protected logger: Logger, - protected canUseEvents: boolean) { } - - public send = (msg: protocol.Message) => { - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); - } - - public event = (body: T, eventName: string) => { - defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); - } - } - export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -313,7 +274,7 @@ namespace ts.server { /** * An optional callback overriding the default behavior for sending messages. */ - messageSender?: MessageSender; + eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -322,7 +283,7 @@ namespace ts.server { allowLocalPluginLoads?: boolean; } - export class Session implements MessageSender { + export class Session implements EventSender { private readonly gcTimer: GcTimer; protected projectService: ProjectService; private changeSeq = 0; @@ -351,9 +312,8 @@ namespace ts.server { const { throttleWaitMilliseconds } = opts; - if (opts.messageSender) { - this.send = opts.messageSender.send; - this.event = opts.messageSender.event; + if (opts.eventSender) { + this.event = opts.eventSender.event; } this.eventHandler = this.canUseEvents @@ -455,7 +415,13 @@ namespace ts.server { } public event(body: T, eventName: string): void { - defaultEvent(this.host, this.byteLength, this.logger, this.canUseEvents, body, eventName); + const ev: protocol.Event = { + seq: 0, + type: "event", + event: eventName, + body + }; + this.send(ev); } // For backwards-compatibility only. From 1fc113337770d77d9cffbaac72ddb7e12293cb77 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:39:26 -0800 Subject: [PATCH 14/29] baseline accept --- .../reference/api/tsserverlibrary.d.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 8ea02a6a5da..6b785153cc9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6909,9 +6909,8 @@ declare namespace ts.server { type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; - type Event = (body: T, eventName: string) => void; interface EventSender { - event: Event; + event: (body: T, eventName: string) => void; } interface SessionOptions { host: ServerHost; @@ -6927,15 +6926,9 @@ declare namespace ts.server { */ canUseEvents: boolean; /** - * If defined, specifies the socket to send events to the client. - * Otherwise, events are sent through the host. + * An optional callback overriding the default behavior for sending messages. */ - eventPort?: number; - /** - * An optional callback overriding the default behavior for sending events. - * if set, `canUseEvents` and `eventPort` are ignored. - */ - event?: Event; + eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6943,7 +6936,6 @@ declare namespace ts.server { allowLocalPluginLoads?: boolean; } class Session implements EventSender { - readonly event: Event; private readonly gcTimer; protected projectService: ProjectService; private changeSeq; @@ -6956,7 +6948,6 @@ declare namespace ts.server { private hrtime; protected logger: Logger; private canUseEvents; - private eventPort; private eventHandler; constructor(opts: SessionOptions); private sendRequestCompletedEvent(requestId); @@ -6964,6 +6955,7 @@ declare namespace ts.server { private projectsUpdatedInBackgroundEvent(openFiles); logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; + event(body: T, eventName: string): void; /** @deprecated */ output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; private doOutput(info, cmdName, reqSeq, success, message?); @@ -7122,7 +7114,7 @@ declare namespace ts.server { isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptionsWithProjectRootPath): Promise; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; - attach(projectService: ProjectService, eventSender?: EventSender): void; + attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; readonly globalTypingsCacheLocation: string; } @@ -7483,7 +7475,6 @@ declare namespace ts.server { pluginProbeLocations?: ReadonlyArray; allowLocalPluginLoads?: boolean; typesMapLocation?: string; - eventSender?: EventSender; } class ProjectService { readonly typingsCache: TypingsCache; From 5c2fea4d4e644302b03a61af5c5a2e3bf38646b2 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Nov 2017 19:49:34 -0800 Subject: [PATCH 15/29] make eventSender required for TypingsInstaller --- src/server/server.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index caf70b9c94a..669ca2869f6 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,10 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - /** - * If undefined, event-related work will be suppressed. - */ - private eventSender: EventSender | undefined) { + private eventSender: EventSender) { } isKnownTypesPackageName(name: string): boolean { @@ -583,7 +580,7 @@ namespace ts.server { const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents ? eventSender : undefined); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, eventSender); super({ host, From 49d92d844bc1163ad039366661d8dc2f7817c5f8 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 18:15:39 -0800 Subject: [PATCH 16/29] defer callback and remove handler object --- src/server/server.ts | 160 +++++++++++++++++++++++------------------- src/server/session.ts | 41 ++++------- 2 files changed, 102 insertions(+), 99 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 669ca2869f6..4a8ef0c1257 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,7 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private eventSender: EventSender) { + private event: Event) { } isKnownTypesPackageName(name: string): boolean { @@ -304,8 +304,8 @@ namespace ts.server { if (this.installerPidReported) { return; } - if (this.eventSender && this.installer) { - this.eventSender.event({ pid: this.installer.pid }, "typingsInstallerPid"); + if (this.event && this.installer) { + this.event({ pid: this.installer.pid }, "typingsInstallerPid"); this.installerPidReported = true; } } @@ -416,19 +416,19 @@ namespace ts.server { } case EventInitializationFailed: { - if (!this.eventSender) { + if (!this.event) { break; } const body: protocol.TypesInstallerInitializationFailedEventBody = { message: response.message }; const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case EventBeginInstallTypes: { - if (!this.eventSender) { + if (!this.event) { break; } const body: protocol.BeginInstallTypesEventBody = { @@ -436,12 +436,12 @@ namespace ts.server { packages: response.packagesToInstall, }; const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case EventEndInstallTypes: { - if (!this.eventSender) { + if (!this.event) { break; } if (this.telemetryEnabled) { @@ -454,7 +454,7 @@ namespace ts.server { } }; const eventName: protocol.TelemetryEventName = "telemetry"; - this.eventSender.event(body, eventName); + this.event(body, eventName); } const body: protocol.EndInstallTypesEventBody = { @@ -463,7 +463,7 @@ namespace ts.server { success: response.installSuccess, }; const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; - this.eventSender.event(body, eventName); + this.event(body, eventName); break; } case ActionInvalidate: @@ -495,8 +495,8 @@ namespace ts.server { this.projectService.updateTypingsForProject(response); - if (this.eventSender) { - this.eventSender.event(response, "setTypings"); + if (this.event) { + this.event(response, "setTypings"); } break; @@ -515,72 +515,49 @@ namespace ts.server { } } - export class DefaultEventSender implements EventSender { - constructor(protected host: ServerHost, - protected byteLength: (buf: string, encoding?: string) => number, - protected logger: Logger, - protected canUseEvents: boolean) { } + // export class DefaultEventSender implements EventSender { + // constructor(protected host: ServerHost, + // protected byteLength: (buf: string, encoding?: string) => number, + // protected logger: Logger, + // protected canUseEvents: boolean) { } - public event = (body: T, eventName: string) => { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); - } - } - - class SocketEventSender implements EventSender { - private eventSocket: NodeSocket | undefined; - private socketEventQueue: { body: any, eventName: string }[] | undefined; - - constructor(private host: ServerHost, - private byteLength: (buf: string, encoding?: string) => number, - private logger: Logger, - private eventPort: number) { - - const s = net.connect({ port: this.eventPort }, () => { - this.eventSocket = s; - if (this.socketEventQueue) { - // flush queue. - for (const event of this.socketEventQueue) { - this.writeToEventSocket(event.body, event.eventName); - } - this.socketEventQueue = undefined; - } - }); - } - - public event = (body: T, eventName: string) => { - if (!this.eventSocket) { - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); - } - (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); - return; - } - else { - Debug.assert(this.socketEventQueue === undefined); - this.writeToEventSocket(body, eventName); - } - }; - - private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); - } - } + // public event = (body: T, eventName: string) => { + // const ev: protocol.Event = { + // seq: 0, + // type: "event", + // event: eventName, + // body + // }; + // defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); + // } + // } class IOSession extends Session { + private eventPort: number; + private eventSocket: NodeSocket | undefined; + private socketEventQueue: { body: any, eventName: string }[] | undefined; + private constructed: boolean | undefined; + constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const eventSender = eventPort && canUseEvents ? new SocketEventSender(host, Buffer.byteLength, logger, eventPort) : new DefaultEventSender(host, Buffer.byteLength, logger, canUseEvents); + const event: Event | undefined = canUseEvents ? + (body: {}, eventName: string) => { + if (this.constructed) { + this.event(body, eventName); + } + else { + // It is unsafe to dereference `this` before initialization completes, + // so we defer until the next tick. + // + // Construction should finish before the next tick fires, so we do not need to do this recursively. + setImmediate(() => this.event(body, eventName)); + } + } : undefined; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined - : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, eventSender); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, event); super({ host, @@ -592,10 +569,51 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents, - eventSender, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, - allowLocalPluginLoads: options.allowLocalPluginLoads }); + allowLocalPluginLoads: options.allowLocalPluginLoads + }); + + this.eventPort = eventPort; + if (this.canUseEvents && this.eventPort) { + const s = net.connect({ port: this.eventPort }, () => { + this.eventSocket = s; + if (this.socketEventQueue) { + // flush queue. + for (const event of this.socketEventQueue) { + this.writeToEventSocket(event.body, event.eventName); + } + this.socketEventQueue = undefined; + } + }); + } + + this.constructed = true; + } + + event(body: T, eventName: string): void { + Debug.assert(this.constructed, "Should only call `IOSession.prototype.event` on an initialized IOSession"); + + if (this.canUseEvents && this.eventPort) { + if (!this.eventSocket) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`eventPort: event "${eventName}" queued, but socket not yet initialized`); + } + (this.socketEventQueue || (this.socketEventQueue = [])).push({ body, eventName }); + return; + } + else { + Debug.assert(this.socketEventQueue === undefined); + this.writeToEventSocket(body, eventName); + } + } + else { + super.event(body, eventName); + } + } + + private writeToEventSocket(body: any, eventName: string): void { + this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); } exit() { diff --git a/src/server/session.ts b/src/server/session.ts index e917b131bf1..d469e5bc8a2 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -239,25 +239,12 @@ namespace ts.server { } } + export type Event = (body: T, eventName: string) => void; + export interface EventSender { - event: (body: T, eventName: string) => void; + event: Event; } - /** @internal */ - export function defaultSend( - host: ServerHost, - byteLength: (buf: string, encoding?: string) => number, - logger: Logger, - canUseEvents: boolean, - msg: protocol.Message) { - if (msg.type === "event" && !canUseEvents) { - if (logger.hasLevel(LogLevel.verbose)) { - logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); - } - return; - } - host.write(formatMessage(msg, logger, byteLength, host.newLine)); - } export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -271,10 +258,6 @@ namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * An optional callback overriding the default behavior for sending messages. - */ - eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -291,14 +274,14 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private host: ServerHost; + protected host: ServerHost; private readonly cancellationToken: ServerCancellationToken; protected readonly typingsInstaller: ITypingsInstaller; - private byteLength: (buf: string, encoding?: string) => number; + protected byteLength: (buf: string, encoding?: string) => number; private hrtime: (start?: number[]) => number[]; protected logger: Logger; - private canUseEvents: boolean; + protected canUseEvents: boolean; private eventHandler: ProjectServiceEventHandler; constructor(opts: SessionOptions) { @@ -312,10 +295,6 @@ namespace ts.server { const { throttleWaitMilliseconds } = opts; - if (opts.eventSender) { - this.event = opts.eventSender.event; - } - this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; @@ -411,7 +390,13 @@ namespace ts.server { } public send(msg: protocol.Message) { - defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, msg); + if (msg.type === "event" && !this.canUseEvents) { + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Session does not support events: ignored event: ${JSON.stringify(msg)}`); + } + return; + } + this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } public event(body: T, eventName: string): void { From cda486ad85c611b448c2025171c40e13b53b1208 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 18:27:30 -0800 Subject: [PATCH 17/29] update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6b785153cc9..3ac976f2225 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6909,8 +6909,9 @@ declare namespace ts.server { type CommandNames = protocol.CommandTypes; const CommandNames: any; function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string; + type Event = (body: T, eventName: string) => void; interface EventSender { - event: (body: T, eventName: string) => void; + event: Event; } interface SessionOptions { host: ServerHost; @@ -6925,10 +6926,6 @@ declare namespace ts.server { * If falsy, all events are suppressed. */ canUseEvents: boolean; - /** - * An optional callback overriding the default behavior for sending messages. - */ - eventSender?: EventSender; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -6941,13 +6938,13 @@ declare namespace ts.server { private changeSeq; private currentRequestId; private errorCheck; - private host; + protected host: ServerHost; private readonly cancellationToken; protected readonly typingsInstaller: ITypingsInstaller; - private byteLength; + protected byteLength: (buf: string, encoding?: string) => number; private hrtime; protected logger: Logger; - private canUseEvents; + protected canUseEvents: boolean; private eventHandler; constructor(opts: SessionOptions); private sendRequestCompletedEvent(requestId); From c098a5cf83e7071dec2b5a3985d706fa43ad61b1 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:06:08 -0800 Subject: [PATCH 18/29] type `event` callback correctly --- src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.ts b/src/server/server.ts index 4a8ef0c1257..68bf4384f94 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -272,7 +272,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private event: Event) { + private event: Event | undefined) { } isKnownTypesPackageName(name: string): boolean { From 8b0d3ab31894dfdd8c69a6b4e47f27b8389a57f0 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:10:38 -0800 Subject: [PATCH 19/29] remove comments --- src/server/server.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index 68bf4384f94..b49a74b5d8b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -515,23 +515,6 @@ namespace ts.server { } } - // export class DefaultEventSender implements EventSender { - // constructor(protected host: ServerHost, - // protected byteLength: (buf: string, encoding?: string) => number, - // protected logger: Logger, - // protected canUseEvents: boolean) { } - - // public event = (body: T, eventName: string) => { - // const ev: protocol.Event = { - // seq: 0, - // type: "event", - // event: eventName, - // body - // }; - // defaultSend(this.host, this.byteLength, this.logger, this.canUseEvents, ev); - // } - // } - class IOSession extends Session { private eventPort: number; private eventSocket: NodeSocket | undefined; From 890820b52893d6d86483bb1647fb3b2d48191383 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Nov 2017 19:19:05 -0800 Subject: [PATCH 20/29] cleanup NodeTypingsInstaller --- src/server/server.ts | 177 +++++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 100 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index b49a74b5d8b..b69884c42c4 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -246,7 +246,6 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; - private installerPidReported = false; private projectService: ProjectService; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; @@ -272,7 +271,7 @@ namespace ts.server { readonly typingSafeListLocation: string, readonly typesMapLocation: string, private readonly npmLocation: string | undefined, - private event: Event | undefined) { + private event: Event) { } isKnownTypesPackageName(name: string): boolean { @@ -300,17 +299,6 @@ namespace ts.server { }); } - private reportInstallerProcessId() { - if (this.installerPidReported) { - return; - } - if (this.event && this.installer) { - this.event({ pid: this.installer.pid }, "typingsInstallerPid"); - this.installerPidReported = true; - } - } - - attach(projectService: ProjectService) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { @@ -350,7 +338,8 @@ namespace ts.server { this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv }); this.installer.on("message", m => this.handleMessage(m)); - this.reportInstallerProcessId(); + + this.event({ pid: this.installer.pid }, "typingsInstallerPid"); process.on("exit", () => { this.installer.kill(); @@ -415,92 +404,81 @@ namespace ts.server { break; } case EventInitializationFailed: - { - if (!this.event) { - break; - } - const body: protocol.TypesInstallerInitializationFailedEventBody = { - message: response.message - }; - const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; - this.event(body, eventName); - break; - } - case EventBeginInstallTypes: - { - if (!this.event) { - break; - } - const body: protocol.BeginInstallTypesEventBody = { - eventId: response.eventId, - packages: response.packagesToInstall, - }; - const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; - this.event(body, eventName); - break; - } - case EventEndInstallTypes: - { - if (!this.event) { - break; - } - if (this.telemetryEnabled) { - const body: protocol.TypingsInstalledTelemetryEventBody = { - telemetryEventName: "typingsInstalled", - payload: { - installedPackages: response.packagesToInstall.join(","), - installSuccess: response.installSuccess, - typingsInstallerVersion: response.typingsInstallerVersion - } + { + const body: protocol.TypesInstallerInitializationFailedEventBody = { + message: response.message }; - const eventName: protocol.TelemetryEventName = "telemetry"; + const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; this.event(body, eventName); + break; } + case EventBeginInstallTypes: + { + const body: protocol.BeginInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + }; + const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; + this.event(body, eventName); + break; + } + case EventEndInstallTypes: + { + if (this.telemetryEnabled) { + const body: protocol.TypingsInstalledTelemetryEventBody = { + telemetryEventName: "typingsInstalled", + payload: { + installedPackages: response.packagesToInstall.join(","), + installSuccess: response.installSuccess, + typingsInstallerVersion: response.typingsInstallerVersion + } + }; + const eventName: protocol.TelemetryEventName = "telemetry"; + this.event(body, eventName); + } - const body: protocol.EndInstallTypesEventBody = { - eventId: response.eventId, - packages: response.packagesToInstall, - success: response.installSuccess, - }; - const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; - this.event(body, eventName); - break; - } + const body: protocol.EndInstallTypesEventBody = { + eventId: response.eventId, + packages: response.packagesToInstall, + success: response.installSuccess, + }; + const eventName: protocol.EndInstallTypesEventName = "endInstallTypes"; + this.event(body, eventName); + break; + } case ActionInvalidate: - { - this.projectService.updateTypingsForProject(response); - break; - } + { + this.projectService.updateTypingsForProject(response); + break; + } case ActionSet: - { - if (this.activeRequestCount > 0) { - this.activeRequestCount--; - } - else { - Debug.fail("Received too many responses"); - } - - while (this.requestQueue.length > 0) { - const queuedRequest = this.requestQueue.shift(); - if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) { - this.requestMap.delete(queuedRequest.operationId); - this.scheduleRequest(queuedRequest); - break; + { + if (this.activeRequestCount > 0) { + this.activeRequestCount--; + } + else { + Debug.fail("Received too many responses"); } - if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`); + while (this.requestQueue.length > 0) { + const queuedRequest = this.requestQueue.shift(); + if (this.requestMap.get(queuedRequest.operationId) === queuedRequest) { + this.requestMap.delete(queuedRequest.operationId); + this.scheduleRequest(queuedRequest); + break; + } + + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`Skipping defunct request for: ${queuedRequest.operationId}`); + } } - } - this.projectService.updateTypingsForProject(response); + this.projectService.updateTypingsForProject(response); - if (this.event) { this.event(response, "setTypings"); - } - break; - } + break; + } default: assertTypeIsNever(response); } @@ -524,19 +502,18 @@ namespace ts.server { constructor(options: IoSessionOptions) { const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; - const event: Event | undefined = canUseEvents ? - (body: {}, eventName: string) => { - if (this.constructed) { - this.event(body, eventName); - } - else { - // It is unsafe to dereference `this` before initialization completes, - // so we defer until the next tick. - // - // Construction should finish before the next tick fires, so we do not need to do this recursively. - setImmediate(() => this.event(body, eventName)); - } - } : undefined; + const event: Event | undefined = (body: {}, eventName: string) => { + if (this.constructed) { + this.event(body, eventName); + } + else { + // It is unsafe to dereference `this` before initialization completes, + // so we defer until the next tick. + // + // Construction should finish before the next tick fires, so we do not need to do this recursively. + setImmediate(() => this.event(body, eventName)); + } + }; const typingsInstaller = disableAutomaticTypingAcquisition ? undefined From 0c0f4b81ae9f0bfa7383e0a62dee92d663dcfdd2 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 17 Nov 2017 10:19:35 -0800 Subject: [PATCH 21/29] Simplify documentHighlights (#20091) --- src/compiler/utilities.ts | 4 + src/services/documentHighlights.ts | 197 +++++++----------- .../reference/api/tsserverlibrary.d.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 1 + 4 files changed, 78 insertions(+), 125 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 48bb0a6dc11..4bc519c2b8e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4724,6 +4724,10 @@ namespace ts { return node.kind === SyntaxKind.BreakStatement; } + export function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement { + return node.kind === SyntaxKind.BreakStatement || node.kind === SyntaxKind.ContinueStatement; + } + export function isReturnStatement(node: Node): node is ReturnStatement { return node.kind === SyntaxKind.ReturnStatement; } diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 4f11e33c638..d9a9a03c38b 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -53,95 +53,48 @@ namespace ts.DocumentHighlights { return [{ fileName: sourceFile.fileName, highlightSpans }]; } - // returns true if 'node' is defined and has a matching 'kind'. - function hasKind(node: Node, kind: SyntaxKind) { - return node !== undefined && node.kind === kind; - } - - // Null-propagating 'parent' function. - function parent(node: Node): Node { - return node && node.parent; - } - - function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] { - if (!node) { - return undefined; - } - + function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] | undefined { switch (node.kind) { case SyntaxKind.IfKeyword: case SyntaxKind.ElseKeyword: - if (hasKind(node.parent, SyntaxKind.IfStatement)) { - return getIfElseOccurrences(node.parent, sourceFile); - } - break; + return isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; case SyntaxKind.ReturnKeyword: - if (hasKind(node.parent, SyntaxKind.ReturnStatement)) { - return highlightSpans(getReturnOccurrences(node.parent)); - } - break; + return useParent(node.parent, isReturnStatement, getReturnOccurrences); case SyntaxKind.ThrowKeyword: - if (hasKind(node.parent, SyntaxKind.ThrowStatement)) { - return highlightSpans(getThrowOccurrences(node.parent)); - } - break; + return useParent(node.parent, isThrowStatement, getThrowOccurrences); case SyntaxKind.TryKeyword: case SyntaxKind.CatchKeyword: case SyntaxKind.FinallyKeyword: - const tryStatement = node.kind === SyntaxKind.CatchKeyword ? parent(parent(node)) : parent(node); - if (hasKind(tryStatement, SyntaxKind.TryStatement)) { - return highlightSpans(getTryCatchFinallyOccurrences(tryStatement, sourceFile)); - } - break; + const tryStatement = node.kind === SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; + return useParent(tryStatement, isTryStatement, getTryCatchFinallyOccurrences); case SyntaxKind.SwitchKeyword: - if (hasKind(node.parent, SyntaxKind.SwitchStatement)) { - return highlightSpans(getSwitchCaseDefaultOccurrences(node.parent)); - } - break; + return useParent(node.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); case SyntaxKind.CaseKeyword: case SyntaxKind.DefaultKeyword: - if (hasKind(parent(parent(parent(node))), SyntaxKind.SwitchStatement)) { - return highlightSpans(getSwitchCaseDefaultOccurrences(node.parent.parent.parent)); - } - break; + return useParent(node.parent.parent.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); case SyntaxKind.BreakKeyword: case SyntaxKind.ContinueKeyword: - if (hasKind(node.parent, SyntaxKind.BreakStatement) || hasKind(node.parent, SyntaxKind.ContinueStatement)) { - return highlightSpans(getBreakOrContinueStatementOccurrences(node.parent)); - } - break; + return useParent(node.parent, isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); case SyntaxKind.ForKeyword: - if (hasKind(node.parent, SyntaxKind.ForStatement) || - hasKind(node.parent, SyntaxKind.ForInStatement) || - hasKind(node.parent, SyntaxKind.ForOfStatement)) { - return highlightSpans(getLoopBreakContinueOccurrences(node.parent)); - } - break; case SyntaxKind.WhileKeyword: case SyntaxKind.DoKeyword: - if (hasKind(node.parent, SyntaxKind.WhileStatement) || hasKind(node.parent, SyntaxKind.DoStatement)) { - return highlightSpans(getLoopBreakContinueOccurrences(node.parent)); - } - break; + return useParent(node.parent, (n): n is IterationStatement => isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); case SyntaxKind.ConstructorKeyword: - if (hasKind(node.parent, SyntaxKind.Constructor)) { - return highlightSpans(getConstructorOccurrences(node.parent)); - } - break; + return useParent(node.parent, isConstructorDeclaration, getConstructorOccurrences); case SyntaxKind.GetKeyword: case SyntaxKind.SetKeyword: - if (hasKind(node.parent, SyntaxKind.GetAccessor) || hasKind(node.parent, SyntaxKind.SetAccessor)) { - return highlightSpans(getGetAndSetOccurrences(node.parent)); - } - break; + return useParent(node.parent, isAccessor, getGetAndSetOccurrences); default: - if (isModifierKind(node.kind) && node.parent && - (isDeclaration(node.parent) || node.parent.kind === SyntaxKind.VariableStatement)) { - return highlightSpans(getModifierOccurrences(node.kind, node.parent)); - } + return isModifierKind(node.kind) && (isDeclaration(node.parent) || isVariableStatement(node.parent)) + ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) + : undefined; } - function highlightSpans(nodes: Node[]): HighlightSpan[] { + function useParent(node: Node, nodeTest: (node: Node) => node is T, getNodes: (node: T, sourceFile: SourceFile) => Node[] | undefined): HighlightSpan[] | undefined { + return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; + } + + function highlightSpans(nodes: Node[] | undefined): HighlightSpan[] | undefined { return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); } } @@ -156,23 +109,21 @@ namespace ts.DocumentHighlights { return statementAccumulator; function aggregate(node: Node): void { - if (node.kind === SyntaxKind.ThrowStatement) { - statementAccumulator.push(node); + if (isThrowStatement(node)) { + statementAccumulator.push(node); } - else if (node.kind === SyntaxKind.TryStatement) { - const tryStatement = node; - - if (tryStatement.catchClause) { - aggregate(tryStatement.catchClause); + else if (isTryStatement(node)) { + if (node.catchClause) { + aggregate(node.catchClause); } else { // Exceptions thrown within a try block lacking a catch clause // are "owned" in the current context. - aggregate(tryStatement.tryBlock); + aggregate(node.tryBlock); } - if (tryStatement.finallyBlock) { - aggregate(tryStatement.finallyBlock); + if (node.finallyBlock) { + aggregate(node.finallyBlock); } } // Do not cross function boundaries. @@ -236,11 +187,11 @@ namespace ts.DocumentHighlights { } function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node { - for (let node = statement.parent; node; node = node.parent) { + return findAncestor(statement, node => { switch (node.kind) { case SyntaxKind.SwitchStatement: if (statement.kind === SyntaxKind.ContinueStatement) { - continue; + return false; } // falls through case SyntaxKind.ForStatement: @@ -248,20 +199,13 @@ namespace ts.DocumentHighlights { case SyntaxKind.ForOfStatement: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: - if (!statement.label || isLabeledBy(node, statement.label.text)) { - return node; - } - break; + return !statement.label || isLabeledBy(node, statement.label.text); default: // Don't cross function boundaries. - if (isFunctionLike(node)) { - return undefined; - } - break; + // TODO: GH#20090 + return (isFunctionLike(node) && "quit") as false | "quit"; } - } - - return undefined; + }); } function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): Node[] { @@ -494,16 +438,14 @@ namespace ts.DocumentHighlights { return keywords; } - function getReturnOccurrences(returnStatement: ReturnStatement): Node[] { + function getReturnOccurrences(returnStatement: ReturnStatement): Node[] | undefined { const func = getContainingFunction(returnStatement); - - // If we didn't find a containing function with a block body, bail out. - if (!(func && hasKind(func.body, SyntaxKind.Block))) { + if (!func) { return undefined; } const keywords: Node[] = []; - forEachReturnStatement(func.body, returnStatement => { + forEachReturnStatement(cast(func.body, isBlock), returnStatement => { pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); }); @@ -516,32 +458,7 @@ namespace ts.DocumentHighlights { } function getIfElseOccurrences(ifStatement: IfStatement, sourceFile: SourceFile): HighlightSpan[] { - const keywords: Node[] = []; - - // Traverse upwards through all parent if-statements linked by their else-branches. - while (hasKind(ifStatement.parent, SyntaxKind.IfStatement) && (ifStatement.parent).elseStatement === ifStatement) { - ifStatement = ifStatement.parent; - } - - // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. - while (ifStatement) { - const children = ifStatement.getChildren(); - pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); - - // Generally the 'else' keyword is second-to-last, so we traverse backwards. - for (let i = children.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { - break; - } - } - - if (!hasKind(ifStatement.elseStatement, SyntaxKind.IfStatement)) { - break; - } - - ifStatement = ifStatement.elseStatement; - } - + const keywords = getIfElseKeywords(ifStatement, sourceFile); const result: HighlightSpan[] = []; // We'd like to highlight else/ifs together if they are only separated by whitespace @@ -551,17 +468,17 @@ namespace ts.DocumentHighlights { const elseKeyword = keywords[i]; const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. - let shouldCombindElseAndIf = true; + let shouldCombineElseAndIf = true; // Avoid recalculating getStart() by iterating backwards. - for (let j = ifKeyword.getStart() - 1; j >= elseKeyword.end; j--) { + for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { - shouldCombindElseAndIf = false; + shouldCombineElseAndIf = false; break; } } - if (shouldCombindElseAndIf) { + if (shouldCombineElseAndIf) { result.push({ fileName: sourceFile.fileName, textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), @@ -579,6 +496,36 @@ namespace ts.DocumentHighlights { return result; } + function getIfElseKeywords(ifStatement: IfStatement, sourceFile: SourceFile): Node[] { + const keywords: Node[] = []; + + // Traverse upwards through all parent if-statements linked by their else-branches. + while (isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { + ifStatement = ifStatement.parent; + } + + // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. + while (true) { + const children = ifStatement.getChildren(sourceFile); + pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); + + // Generally the 'else' keyword is second-to-last, so we traverse backwards. + for (let i = children.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { + break; + } + } + + if (!ifStatement.elseStatement || !isIfStatement(ifStatement.elseStatement)) { + break; + } + + ifStatement = ifStatement.elseStatement; + } + + return keywords; + } + /** * Whether or not a 'node' is preceded by a label of the given string. * Note: 'node' cannot be a SourceFile. diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 23520f42952..0b93c408be4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2992,6 +2992,7 @@ declare namespace ts { function isForOfStatement(node: Node): node is ForOfStatement; function isContinueStatement(node: Node): node is ContinueStatement; function isBreakStatement(node: Node): node is BreakStatement; + function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement; function isReturnStatement(node: Node): node is ReturnStatement; function isWithStatement(node: Node): node is WithStatement; function isSwitchStatement(node: Node): node is SwitchStatement; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 68c2ed14f2b..8a8f5218f3a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3045,6 +3045,7 @@ declare namespace ts { function isForOfStatement(node: Node): node is ForOfStatement; function isContinueStatement(node: Node): node is ContinueStatement; function isBreakStatement(node: Node): node is BreakStatement; + function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement; function isReturnStatement(node: Node): node is ReturnStatement; function isWithStatement(node: Node): node is WithStatement; function isSwitchStatement(node: Node): node is SwitchStatement; From e7adb1ce79bd46ee8507a7c92c321abcbd632f3e Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 17 Nov 2017 14:16:38 -0800 Subject: [PATCH 22/29] Bundle fileName with CodeActionCommand (#19881) * Bundle fileName with CodeActionCommand * Update test * Fix API tests * Add new overloads in services * Fix overload * Update API baselines --- src/compiler/core.ts | 4 ++++ src/server/protocol.ts | 2 +- src/server/session.ts | 17 ++++++++--------- src/services/codefixes/fixCannotFindModule.ts | 6 +++--- .../refactors/installTypesForPackage.ts | 2 +- src/services/services.ts | 13 ++++++++----- src/services/types.ts | 7 +++++++ .../reference/api/tsserverlibrary.d.ts | 8 +++++++- tests/baselines/reference/api/typescript.d.ts | 6 ++++++ .../cases/fourslash/codeFixCannotFindModule.ts | 1 + 10 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index caf6f553baf..8b0e3cfacd3 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1324,6 +1324,10 @@ namespace ts { return Array.isArray ? Array.isArray(value) : value instanceof Array; } + export function toArray(value: T | T[]): T[] { + return isArray(value) ? value : [value]; + } + /** * Tests whether a value is string */ diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3761049017d..6e96365a4ac 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -585,7 +585,7 @@ namespace ts.server.protocol { errorCodes?: number[]; } - export interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + export interface ApplyCodeActionCommandRequestArgs { /** May also be an array of commands. */ command: {}; } diff --git a/src/server/session.ts b/src/server/session.ts index f9a145d16fd..ea95f7a4f42 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1569,15 +1569,14 @@ namespace ts.server { } private applyCodeActionCommand(commandName: string, requestSeq: number, args: protocol.ApplyCodeActionCommandRequestArgs): void { - const { file, project } = this.getFileAndProject(args); - const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message); - const command = args.command as CodeActionCommand | CodeActionCommand[]; // They should be sending back the command we sent them. - - project.getLanguageService().applyCodeActionCommand(file, command).then( - result => { - output(/*success*/ true, isArray(result) ? result.map(res => res.successMessage).join(`${this.host.newLine}${this.host.newLine}`) : result.successMessage); - }, - error => { output(/*success*/ false, error); }); + const commands = args.command as CodeActionCommand | CodeActionCommand[]; // They should be sending back the command we sent them. + for (const command of toArray(commands)) { + const { project } = this.getFileAndProject(command); + const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message); + project.getLanguageService().applyCodeActionCommand(command).then( + result => { output(/*success*/ true, result.successMessage); }, + error => { output(/*success*/ false, error); }); + } } private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { diff --git a/src/services/codefixes/fixCannotFindModule.ts b/src/services/codefixes/fixCannotFindModule.ts index 29f2475f21d..9796bf2fa22 100644 --- a/src/services/codefixes/fixCannotFindModule.ts +++ b/src/services/codefixes/fixCannotFindModule.ts @@ -11,12 +11,12 @@ namespace ts.codefix { throw Debug.fail(); // These errors should only happen on the module name. } - const action = tryGetCodeActionForInstallPackageTypes(context.host, token.text); + const action = tryGetCodeActionForInstallPackageTypes(context.host, sourceFile.fileName, token.text); return action && [action]; }, }); - export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, moduleName: string): CodeAction | undefined { + export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, fileName: string, moduleName: string): CodeAction | undefined { const { packageName } = getPackageName(moduleName); if (!host.isKnownTypesPackageName(packageName)) { @@ -28,7 +28,7 @@ namespace ts.codefix { return { description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Install_0), [typesPackageName]), changes: [], - commands: [{ type: "install package", packageName: typesPackageName }], + commands: [{ type: "install package", file: fileName, packageName: typesPackageName }], }; } } diff --git a/src/services/refactors/installTypesForPackage.ts b/src/services/refactors/installTypesForPackage.ts index 996645fc15d..fbefbbcf4e8 100644 --- a/src/services/refactors/installTypesForPackage.ts +++ b/src/services/refactors/installTypesForPackage.ts @@ -47,7 +47,7 @@ namespace ts.refactor.installTypesForPackage { const { file, startPosition } = context; const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); if (isStringLiteral(node) && isModuleIdentifier(node) && getResolvedModule(file, node.text) === undefined) { - return codefix.tryGetCodeActionForInstallPackageTypes(context.host, node.text); + return codefix.tryGetCodeActionForInstallPackageTypes(context.host, file.fileName, node.text); } } diff --git a/src/services/services.ts b/src/services/services.ts index 8fcd7ef13b8..d62fc88a649 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1887,18 +1887,21 @@ namespace ts { }); } + function applyCodeActionCommand(action: CodeActionCommand): Promise; + function applyCodeActionCommand(action: CodeActionCommand[]): Promise; + function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise; function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise; - function applyCodeActionCommand(fileName: Path, action: CodeActionCommand | CodeActionCommand[]): Promise { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(path, a))) : applySingleCodeActionCommand(path, action); + function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrUndefined?: CodeActionCommand | CodeActionCommand[]): Promise { + const action = typeof fileName === "string" ? actionOrUndefined! : fileName as CodeActionCommand[]; + return isArray(action) ? Promise.all(action.map(applySingleCodeActionCommand)) : applySingleCodeActionCommand(action); } - function applySingleCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise { + function applySingleCodeActionCommand(action: CodeActionCommand): Promise { switch (action.type) { case "install package": return host.installPackage - ? host.installPackage({ fileName, packageName: action.packageName }) + ? host.installPackage({ fileName: toPath(action.file, currentDirectory, getCanonicalFileName), packageName: action.packageName }) : Promise.reject("Host does not implement `installPackage`"); default: Debug.fail(); diff --git a/src/services/types.ts b/src/services/types.ts index bcebc437892..28de0566a50 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -293,8 +293,14 @@ namespace ts { getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -406,6 +412,7 @@ namespace ts { export type CodeActionCommand = InstallPackageAction; export interface InstallPackageAction { + /* @internal */ file: string; /* @internal */ type: "install package"; /* @internal */ packageName: string; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 0b93c408be4..407e5dec707 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3975,8 +3975,14 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -5275,7 +5281,7 @@ declare namespace ts.server.protocol { */ errorCodes?: number[]; } - interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + interface ApplyCodeActionCommandRequestArgs { /** May also be an array of commands. */ command: {}; } diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 8a8f5218f3a..43b668fe2d1 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3975,8 +3975,14 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(action: CodeActionCommand): Promise; + applyCodeActionCommand(action: CodeActionCommand[]): Promise; + applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand[]): Promise; + /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; diff --git a/tests/cases/fourslash/codeFixCannotFindModule.ts b/tests/cases/fourslash/codeFixCannotFindModule.ts index d0ace09909e..10d4270892e 100644 --- a/tests/cases/fourslash/codeFixCannotFindModule.ts +++ b/tests/cases/fourslash/codeFixCannotFindModule.ts @@ -19,6 +19,7 @@ verify.codeFixAvailable([{ description: "Install '@types/abs'", commands: [{ type: "install package", + file: "/a.ts", packageName: "@types/abs", }], }]); From 97bb471e48c82cae3958785ab47d69d4688b9be2 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 17 Nov 2017 14:35:32 -0800 Subject: [PATCH 23/29] For import completion, if multiple re-exports exist, choose the one with the shortest path (#20049) * For import completion, if multiple re-exports exist, choose the one with the shortest path * Code review --- src/compiler/core.ts | 42 ++++++++++++ src/services/codefixes/importFixes.ts | 64 +++++++++++-------- src/services/completions.ts | 22 ++++++- ...mpletionsImport_ofAlias_preferShortPath.ts | 31 +++++++++ 4 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8b0e3cfacd3..d9c33f9aa3a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -394,6 +394,14 @@ namespace ts { return result; } + export function mapIterator(iter: Iterator, mapFn: (x: T) => U): Iterator { + return { next }; + function next(): { value: U, done: false } | { value: never, done: true } { + const iterRes = iter.next(); + return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false }; + } + } + // Maps from T to T and avoids allocation if all elements map to themselves export function sameMap(array: T[], f: (x: T, i: number) => T): T[]; export function sameMap(array: ReadonlyArray, f: (x: T, i: number) => T): ReadonlyArray; @@ -917,6 +925,36 @@ namespace ts { return array.slice().sort(comparer); } + export function best(iter: Iterator, isBetter: (a: T, b: T) => boolean): T | undefined { + const x = iter.next(); + if (x.done) { + return undefined; + } + let best = x.value; + while (true) { + const { value, done } = iter.next(); + if (done) { + return best; + } + if (isBetter(value, best)) { + best = value; + } + } + } + + export function arrayIterator(array: ReadonlyArray): Iterator { + let i = 0; + return { next: () => { + if (i === array.length) { + return { value: undefined as never, done: true }; + } + else { + i++; + return { value: array[i - 1], done: false }; + } + }}; + } + /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. */ @@ -1122,6 +1160,10 @@ namespace ts { return result; } + export function toArray(value: T | ReadonlyArray): ReadonlyArray { + return isArray(value) ? value : [value]; + } + /** * Calls `callback` for each entry in the map, returning the first truthy result. * Use `map.forEach` instead for normal iteration. diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index cd67d10330c..ec56a08656c 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -184,8 +184,10 @@ namespace ts.codefix { Equals } - export function getCodeActionForImport(moduleSymbol: Symbol, context: ImportCodeFixOptions): ImportCodeAction[] { - const declarations = getImportDeclarations(moduleSymbol, context.checker, context.sourceFile, context.cachedImportDeclarations); + export function getCodeActionForImport(moduleSymbols: Symbol | ReadonlyArray, context: ImportCodeFixOptions): ImportCodeAction[] { + moduleSymbols = toArray(moduleSymbols); + const declarations = flatMap(moduleSymbols, moduleSymbol => + getImportDeclarations(moduleSymbol, context.checker, context.sourceFile, context.cachedImportDeclarations)); const actions: ImportCodeAction[] = []; if (context.symbolToken) { // It is possible that multiple import statements with the same specifier exist in the file. @@ -207,7 +209,7 @@ namespace ts.codefix { } } } - actions.push(getCodeActionForAddImport(moduleSymbol, context, declarations)); + actions.push(getCodeActionForAddImport(moduleSymbols, context, declarations)); return actions; } @@ -313,16 +315,19 @@ namespace ts.codefix { } } - export function getModuleSpecifierForNewImport(sourceFile: SourceFile, moduleSymbol: Symbol, options: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost): string | undefined { - const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; - const sourceDirectory = getDirectoryPath(sourceFile.fileName); + export function getModuleSpecifierForNewImport(sourceFile: SourceFile, moduleSymbols: ReadonlyArray, options: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost): string | undefined { + const choices = mapIterator(arrayIterator(moduleSymbols), moduleSymbol => { + const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; + const sourceDirectory = getDirectoryPath(sourceFile.fileName); - return tryGetModuleNameFromAmbientModule(moduleSymbol) || - tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName) || - tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) || - tryGetModuleNameFromBaseUrl(options, moduleFileName, getCanonicalFileName) || - options.rootDirs && tryGetModuleNameFromRootDirs(options.rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) || - removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), options); + return tryGetModuleNameFromAmbientModule(moduleSymbol) || + tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName) || + tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) || + tryGetModuleNameFromBaseUrl(options, moduleFileName, getCanonicalFileName) || + options.rootDirs && tryGetModuleNameFromRootDirs(options.rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) || + removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), options); + }); + return best(choices, (a, b) => a.length < b.length); } function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined { @@ -543,7 +548,7 @@ namespace ts.codefix { } function getCodeActionForAddImport( - moduleSymbol: Symbol, + moduleSymbols: ReadonlyArray, ctx: ImportCodeFixOptions, declarations: ReadonlyArray): ImportCodeAction { const fromExistingImport = firstDefined(declarations, declaration => { @@ -565,7 +570,7 @@ namespace ts.codefix { } const moduleSpecifier = firstDefined(declarations, moduleSpecifierFromAnyImport) - || getModuleSpecifierForNewImport(ctx.sourceFile, moduleSymbol, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host); + || getModuleSpecifierForNewImport(ctx.sourceFile, moduleSymbols, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host); return getCodeActionForNewImport(ctx, moduleSpecifier); } @@ -659,24 +664,33 @@ namespace ts.codefix { symbolName = symbol.name; } else { - Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here"); + throw Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here"); } - const allowSyntheticDefaultImports = getAllowSyntheticDefaultImports(compilerOptions); - + return getCodeActionForImport(symbol, { ...context, symbolName, kind: getUmdImportKind(compilerOptions) }); + } + function getUmdImportKind(compilerOptions: CompilerOptions) { // Import a synthetic `default` if enabled. - if (allowSyntheticDefaultImports) { - return getCodeActionForImport(symbol, { ...context, symbolName, kind: ImportKind.Default }); + if (getAllowSyntheticDefaultImports(compilerOptions)) { + return ImportKind.Default; } - const moduleKind = getEmitModuleKind(compilerOptions); // When a synthetic `default` is unavailable, use `import..require` if the module kind supports it. - if (moduleKind === ModuleKind.AMD || moduleKind === ModuleKind.CommonJS || moduleKind === ModuleKind.UMD) { - return getCodeActionForImport(symbol, { ...context, symbolName, kind: ImportKind.Equals }); + const moduleKind = getEmitModuleKind(compilerOptions); + switch (moduleKind) { + case ModuleKind.AMD: + case ModuleKind.CommonJS: + case ModuleKind.UMD: + return ImportKind.Equals; + case ModuleKind.System: + case ModuleKind.ES2015: + case ModuleKind.ESNext: + case ModuleKind.None: + // Fall back to the `import * as ns` style import. + return ImportKind.Namespace; + default: + throw Debug.assertNever(moduleKind); } - - // Fall back to the `import * as ns` style import. - return getCodeActionForImport(symbol, { ...context, symbolName, kind: ImportKind.Namespace }); } function getActionsForNonUMDImport(context: ImportCodeFixContext, allSourceFiles: ReadonlyArray, cancellationToken: CancellationToken): ImportCodeAction[] { diff --git a/src/services/completions.ts b/src/services/completions.ts index f2771c0a7e1..6bbdfacac4c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -443,7 +443,7 @@ namespace ts.Completions { } case "symbol": { const { symbol, location, symbolToOriginInfoMap } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, typeChecker, host, compilerOptions, sourceFile, formatContext, getCanonicalFileName); + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, typeChecker, host, compilerOptions, sourceFile, formatContext, getCanonicalFileName, allSourceFiles); const kindModifiers = SymbolDisplay.getSymbolModifiers(symbol); const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All); return { name, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions, source: sourceDisplay }; @@ -476,6 +476,7 @@ namespace ts.Completions { sourceFile: SourceFile, formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, + allSourceFiles: ReadonlyArray, ): { codeActions: CodeAction[] | undefined, sourceDisplay: SymbolDisplayPart[] | undefined } { const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; if (!symbolOriginInfo) { @@ -483,9 +484,12 @@ namespace ts.Completions { } const { moduleSymbol, isDefaultExport } = symbolOriginInfo; + const exportedSymbol = skipAlias(symbol.exportSymbol || symbol, checker); + const moduleSymbols = getAllReExportingModules(exportedSymbol, checker, allSourceFiles); + Debug.assert(contains(moduleSymbols, moduleSymbol)); - const sourceDisplay = [textPart(codefix.getModuleSpecifierForNewImport(sourceFile, moduleSymbol, compilerOptions, getCanonicalFileName, host))]; - const codeActions = codefix.getCodeActionForImport(moduleSymbol, { + const sourceDisplay = [textPart(codefix.getModuleSpecifierForNewImport(sourceFile, moduleSymbols, compilerOptions, getCanonicalFileName, host))]; + const codeActions = codefix.getCodeActionForImport(moduleSymbols, { host, checker, newLineCharacter: host.getNewLine(), @@ -500,6 +504,18 @@ namespace ts.Completions { return { sourceDisplay, codeActions }; } + function getAllReExportingModules(exportedSymbol: Symbol, checker: TypeChecker, allSourceFiles: ReadonlyArray): ReadonlyArray { + const result: Symbol[] = []; + codefix.forEachExternalModule(checker, allSourceFiles, module => { + for (const exported of checker.getExportsOfModule(module)) { + if (skipAlias(exported, checker) === exportedSymbol) { + result.push(module); + } + } + }); + return result; + } + export function getCompletionEntrySymbol( typeChecker: TypeChecker, log: (message: string) => void, diff --git a/tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts b/tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts new file mode 100644 index 00000000000..a1eba5981db --- /dev/null +++ b/tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts @@ -0,0 +1,31 @@ +/// + +// Test that the completion is for the shortest path, even if that's a re-export. +// Note that `source` in completionEntries will still be the original exporting path, but we use the re-export in completionDetails. + +// @moduleResolution: node +// @module: commonJs + +// @Filename: /foo/index.ts +////export { foo } from "./lib/foo"; + +// @Filename: /foo/lib/foo.ts +////export const foo = 0; + +// @Filename: /user.ts +////fo/**/ + +goTo.marker(""); +const options = { includeExternalModuleExports: true, sourceDisplay: "./foo" }; +verify.completionListContains({ name: "foo", source: "/foo/lib/foo" }, "const foo: 0", "", "const", /*spanIndex*/ undefined, /*hasAction*/ true, options); +verify.not.completionListContains({ name: "foo", source: "/foo/index" }, undefined, undefined, undefined, undefined, undefined, options); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + source: "/foo/lib/foo", + description: `Import 'foo' from "./foo".`, + // TODO: GH#18445 + newFileContent: `import { foo } from "./foo";\r +\r +fo`, +}); From b244907074c2835feb38c4d074f15f8f85919ab7 Mon Sep 17 00:00:00 2001 From: Bill Ticehurst Date: Fri, 17 Nov 2017 12:49:56 -0800 Subject: [PATCH 24/29] Fixed internal safelist --- src/server/editorServices.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9ac0997f117..6ba10ba28a5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -155,10 +155,9 @@ namespace ts.server { exclude: [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it types: ["office"] // @types package to fetch instead }, - "Minified files": { - // e.g. /whatever/blah.min.js - match: /^(.+\.min\.js)$/i, - exclude: [["^", 1, "$"]] + "References": { + "match": /^(.*\/_references\.js)$/i, + "exclude": [["^", 1, "$"]] } }; From 805f191a57793aa407e4080a6223e010497a44a7 Mon Sep 17 00:00:00 2001 From: Bill Ticehurst Date: Fri, 17 Nov 2017 11:38:51 -0800 Subject: [PATCH 25/29] Moved minified file exclusion --- src/server/editorServices.ts | 8 +++++++- src/server/typesMap.json | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 6ba10ba28a5..d75b3581e54 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2284,7 +2284,13 @@ namespace ts.server { } } if (!exclude) { - filesToKeep.push(proj.rootFiles[i]); + // Exclude any minified files that get this far + if (/^.+[\.-]min\.js$/.test(normalizedNames[i])) { + excludedFiles.push(normalizedNames[i]); + } + else { + filesToKeep.push(proj.rootFiles[i]); + } } } } diff --git a/src/server/typesMap.json b/src/server/typesMap.json index c3d2b9af831..93e9c2502f8 100644 --- a/src/server/typesMap.json +++ b/src/server/typesMap.json @@ -19,8 +19,8 @@ "exclude": [["^", 1, "/.*"]], "types": ["office"] }, - "Minified files": { - "match": "^(.+\\.min\\.js)$", + "References": { + "match": "^(.*\\/_references\\.js)$", "exclude": [["^", 1, "$"]] } }, From 4037b5c55a631303b799f7d37f3645aba6b65ed6 Mon Sep 17 00:00:00 2001 From: csigs Date: Fri, 17 Nov 2017 23:11:05 +0000 Subject: [PATCH 26/29] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 161 +++++++++++++----- .../diagnosticMessages.generated.json.lcl | 161 +++++++++++++----- 2 files changed, 242 insertions(+), 80 deletions(-) diff --git a/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl index b44c64df54b..dfba55014e0 100644 --- a/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -141,48 +141,33 @@ - + - - - - + - + - - - - + - + - - - - + - + - - - - + - + - - - - + @@ -519,6 +504,18 @@ + + + + + + + + + + + + @@ -762,6 +759,12 @@ + + + + + + @@ -2568,6 +2571,12 @@ + + + + + + @@ -2916,12 +2925,9 @@ - + - - - - + @@ -2952,12 +2958,9 @@ - + - - - - + @@ -4110,6 +4113,15 @@ + + + + + + + + + @@ -4479,6 +4491,18 @@ + + + + + + + + + + + + @@ -5619,6 +5643,24 @@ + + + + + + + + + + + + + + + + + + @@ -5646,6 +5688,24 @@ + + + + + + + + + + + + + + + + + + @@ -6426,6 +6486,12 @@ + + + + + + @@ -6648,12 +6714,9 @@ - + - - - - + @@ -8703,6 +8766,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl index 6d200bf7e3b..ff50a731206 100644 --- a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -140,48 +140,33 @@ - + - - - - + - + - - - - + - + - - - - + - + - - - - + - + - - - - + @@ -518,6 +503,18 @@ + + + + + + + + + + + + @@ -764,6 +761,12 @@ + + + + + + @@ -2576,6 +2579,12 @@ + + + + + + @@ -2924,12 +2933,9 @@ - + - - - - + @@ -2960,12 +2966,9 @@ - + - - - - + @@ -4121,6 +4124,15 @@ + + + + + + + + + @@ -4490,6 +4502,18 @@ + + + + + + + + + + + + @@ -5633,6 +5657,24 @@ + + + + + + + + + + + + + + + + + + @@ -5660,6 +5702,24 @@ + + + + + + + + + + + + + + + + + + @@ -6446,6 +6506,12 @@ + + + + + + @@ -6668,12 +6734,9 @@ - + - - - - + @@ -8723,6 +8786,24 @@ + + + + + + + + + + + + + + + + + + From 82502ea0868297f3a9d7cc9560122c80c6fe18d6 Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 17 Nov 2017 15:29:09 -0800 Subject: [PATCH 27/29] Fix semantic merge conflict (#20119) --- src/compiler/core.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index d9c33f9aa3a..6084948ea77 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1160,10 +1160,6 @@ namespace ts { return result; } - export function toArray(value: T | ReadonlyArray): ReadonlyArray { - return isArray(value) ? value : [value]; - } - /** * Calls `callback` for each entry in the map, returning the first truthy result. * Use `map.forEach` instead for normal iteration. @@ -1366,6 +1362,8 @@ namespace ts { return Array.isArray ? Array.isArray(value) : value instanceof Array; } + export function toArray(value: T | ReadonlyArray): ReadonlyArray; + export function toArray(value: T | T[]): T[]; export function toArray(value: T | T[]): T[] { return isArray(value) ? value : [value]; } From 574ceccf0addeb62a5db395593b7be334e9b0ca9 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 17 Nov 2017 16:12:42 -0800 Subject: [PATCH 28/29] Update editorServices.ts Fix lint --- src/server/editorServices.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d75b3581e54..2793f2daee8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -156,8 +156,8 @@ namespace ts.server { types: ["office"] // @types package to fetch instead }, "References": { - "match": /^(.*\/_references\.js)$/i, - "exclude": [["^", 1, "$"]] + match: /^(.*\/_references\.js)$/i, + exclude: [["^", 1, "$"]] } }; From d2cc4f15bec6cacaf6cc297acd870aaa5807a4bc Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Fri, 17 Nov 2017 19:48:57 -0800 Subject: [PATCH 29/29] test overriding Session.event --- .../unittests/tsserverProjectSystem.ts | 122 +++++++++--------- src/server/server.ts | 2 +- src/server/session.ts | 18 ++- 3 files changed, 70 insertions(+), 72 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ea573726eb8..b112d46cec0 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -210,6 +210,8 @@ namespace ts.projectSystem { class TestSession extends server.Session { private seq = 0; + public events: protocol.Event[] = []; + public host: TestServerHost; getProjectService() { return this.projectService; @@ -229,6 +231,16 @@ namespace ts.projectSystem { request.type = "request"; return this.executeCommand(request); } + + public event(body: T, eventName: string) { + this.events.push(server.toEvent(eventName, body)); + super.event(body, eventName); + } + + public clearMessages() { + clear(this.events); + this.host.clearOutput(); + } } export function createSession(host: server.ServerHost, opts: Partial = {}) { @@ -436,48 +448,29 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: TestServerHost) { - assert.equal(actualOutput, server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, host.newLine)); + function checkErrorMessage(session: TestSession, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { + checkNthEvent(session, ts.server.toEvent(eventName, diagnostics), 0, /*isMostRecent*/ false); } - function checkErrorMessage(host: TestServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) { - const outputs = host.getOutput(); - assert.isTrue(outputs.length >= 1, outputs.toString()); - const event: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body: diagnostics - }; - assertEvent(outputs[0], event, host); + function checkCompleteEvent(session: TestSession, numberOfCurrentEvents: number, expectedSequenceId: number) { + checkNthEvent(session, ts.server.toEvent("requestCompleted", { request_seq: expectedSequenceId }), numberOfCurrentEvents - 1, /*isMostRecent*/ true); } - function checkCompleteEvent(host: TestServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) { - const outputs = host.getOutput(); - assert.equal(outputs.length, numberOfCurrentEvents, outputs.toString()); - const event: protocol.RequestCompletedEvent = { - seq: 0, - type: "event", - event: "requestCompleted", - body: { - request_seq: expectedSequenceId - } - }; - assertEvent(outputs[numberOfCurrentEvents - 1], event, host); + function checkProjectUpdatedInBackgroundEvent(session: TestSession, openFiles: string[]) { + checkNthEvent(session, ts.server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); } - function checkProjectUpdatedInBackgroundEvent(host: TestServerHost, openFiles: string[]) { - const outputs = host.getOutput(); - assert.equal(outputs.length, 1, outputs.toString()); - const event: protocol.ProjectsUpdatedInBackgroundEvent = { - seq: 0, - type: "event", - event: "projectsUpdatedInBackground", - body: { - openFiles - } - }; - assertEvent(outputs[0], event, host); + function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { + const events = session.events; + assert.deepEqual(events[index], expectedEvent); + + const outputs = session.host.getOutput(); + assert.equal(outputs[index], server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, session.host.newLine)); + + if (isMostRecent) { + assert.strictEqual(events.length, index + 1, JSON.stringify(events)); + assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs)); + } } describe("tsserverProjectSystem", () => { @@ -2887,14 +2880,14 @@ namespace ts.projectSystem { assert.isFalse(hasError); host.checkTimeoutQueueLength(2); - checkErrorMessage(host, "syntaxDiag", { file: untitledFile, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: untitledFile, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); assert.isFalse(hasError); - checkErrorMessage(host, "semanticDiag", { file: untitledFile, diagnostics: [] }); + checkErrorMessage(session, "semanticDiag", { file: untitledFile, diagnostics: [] }); - checkCompleteEvent(host, 2, expectedSequenceId); + checkCompleteEvent(session, 2, expectedSequenceId); } it("has projectRoot", () => { @@ -2938,7 +2931,7 @@ namespace ts.projectSystem { verifyErrorsInApp(); function verifyErrorsInApp() { - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -2948,13 +2941,13 @@ namespace ts.projectSystem { } }); host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(host, "syntaxDiag", { file: app.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: app.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); - checkErrorMessage(host, "semanticDiag", { file: app.path, diagnostics: [] }); - checkCompleteEvent(host, 2, expectedSequenceId); - host.clearOutput(); + checkErrorMessage(session, "semanticDiag", { file: app.path, diagnostics: [] }); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); } }); }); @@ -3679,7 +3672,7 @@ namespace ts.projectSystem { } }); checkNumberOfProjects(service, { inferredProjects: 1 }); - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -3690,23 +3683,24 @@ namespace ts.projectSystem { }); host.checkTimeoutQueueLengthAndRun(1); - checkErrorMessage(host, "syntaxDiag", { file: file1.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); const moduleNotFound = Diagnostics.Cannot_find_module_0; const startOffset = file1.content.indexOf('"') + 1; - checkErrorMessage(host, "semanticDiag", { + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [{ start: { line: 1, offset: startOffset }, end: { line: 1, offset: startOffset + '"pad"'.length }, text: formatStringFromArgs(moduleNotFound.message, ["pad"]), code: moduleNotFound.code, - category: DiagnosticCategory[moduleNotFound.category].toLowerCase() + category: DiagnosticCategory[moduleNotFound.category].toLowerCase(), + source: undefined }] }); - checkCompleteEvent(host, 2, expectedSequenceId); - host.clearOutput(); + checkCompleteEvent(session, 2, expectedSequenceId); + session.clearMessages(); const padIndex: FileOrFolder = { path: `${folderPath}/node_modules/@types/pad/index.d.ts`, @@ -3715,15 +3709,15 @@ namespace ts.projectSystem { files.push(padIndex); host.reloadFS(files, { ignoreWatchInvokedWithTriggerAsFileCreate: true }); host.runQueuedTimeoutCallbacks(); - checkProjectUpdatedInBackgroundEvent(host, [file1.path]); - host.clearOutput(); + checkProjectUpdatedInBackgroundEvent(session, [file1.path]); + session.clearMessages(); host.runQueuedTimeoutCallbacks(); - checkErrorMessage(host, "syntaxDiag", { file: file1.path, diagnostics: [] }); - host.clearOutput(); + checkErrorMessage(session, "syntaxDiag", { file: file1.path, diagnostics: [] }); + session.clearMessages(); host.runQueuedImmediateCallbacks(); - checkErrorMessage(host, "semanticDiag", { file: file1.path, diagnostics: [] }); + checkErrorMessage(session, "semanticDiag", { file: file1.path, diagnostics: [] }); }); }); @@ -4837,7 +4831,7 @@ namespace ts.projectSystem { command: "projectInfo", arguments: { file: f1.path } }); - host.clearOutput(); + session.clearMessages(); // cancel previously issued Geterr cancellationToken.setRequestToCancel(getErrId); @@ -4861,7 +4855,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); cancellationToken.setRequestToCancel(getErrId); host.runQueuedImmediateCallbacks(); @@ -4883,7 +4877,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); // the semanticDiag message host.runQueuedImmediateCallbacks(); @@ -4906,7 +4900,7 @@ namespace ts.projectSystem { assert.equal(host.getOutput().length, 1, "expect 1 message"); const e1 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); - host.clearOutput(); + session.clearMessages(); session.executeCommandSeq({ command: "geterr", @@ -4920,7 +4914,7 @@ namespace ts.projectSystem { const event = getMessage(n); assert.equal(event.event, "requestCompleted"); assert.equal(event.body.request_seq, expectedSeq, "expectedSeq"); - host.clearOutput(); + session.clearMessages(); } function getMessage(n: number) { @@ -6423,7 +6417,7 @@ namespace ts.projectSystem { }); // Verified the events, reset them - host.clearOutput(); + session.clearMessages(); } } }); diff --git a/src/server/server.ts b/src/server/server.ts index b69884c42c4..fb0b3b781af 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -573,7 +573,7 @@ namespace ts.server { } private writeToEventSocket(body: any, eventName: string): void { - this.eventSocket.write(formatMessage({ seq: 0, type: "event", event: eventName, body }, this.logger, this.byteLength, this.host.newLine), "utf8"); + this.eventSocket.write(formatMessage(toEvent(body, eventName), this.logger, this.byteLength, this.host.newLine), "utf8"); } exit() { diff --git a/src/server/session.ts b/src/server/session.ts index d469e5bc8a2..ab1bedc7b6e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -245,6 +245,16 @@ namespace ts.server { event: Event; } + /** @internal */ + export function toEvent(eventName: string, body: {}): protocol.Event { + return { + seq: 0, + type: "event", + event: eventName, + body + }; + } + export interface SessionOptions { host: ServerHost; cancellationToken: ServerCancellationToken; @@ -400,13 +410,7 @@ namespace ts.server { } public event(body: T, eventName: string): void { - const ev: protocol.Event = { - seq: 0, - type: "event", - event: eventName, - body - }; - this.send(ev); + this.send(toEvent(eventName, body)); } // For backwards-compatibility only.