diff --git a/src/compiler/core.ts b/src/compiler/core.ts index caf6f553ba..6084948ea7 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. */ @@ -1324,6 +1362,12 @@ 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]; + } + /** * Tests whether a value is string */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 48bb0a6dc1..4bc519c2b8 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/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f5a71aa4d8..2ca7d4e21d 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", () => { @@ -2891,14 +2884,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", () => { @@ -2942,7 +2935,7 @@ namespace ts.projectSystem { verifyErrorsInApp(); function verifyErrorsInApp() { - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -2952,13 +2945,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(); } }); }); @@ -3683,7 +3676,7 @@ namespace ts.projectSystem { } }); checkNumberOfProjects(service, { inferredProjects: 1 }); - host.clearOutput(); + session.clearMessages(); const expectedSequenceId = session.getNextSeq(); session.executeCommandSeq({ command: server.CommandNames.Geterr, @@ -3694,23 +3687,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`, @@ -3719,15 +3713,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: [] }); }); }); @@ -4841,7 +4835,7 @@ namespace ts.projectSystem { command: "projectInfo", arguments: { file: f1.path } }); - host.clearOutput(); + session.clearMessages(); // cancel previously issued Geterr cancellationToken.setRequestToCancel(getErrId); @@ -4865,7 +4859,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(); @@ -4887,7 +4881,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(); @@ -4910,7 +4904,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", @@ -4924,7 +4918,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) { @@ -6427,7 +6421,7 @@ namespace ts.projectSystem { }); // Verified the events, reset them - host.clearOutput(); + session.clearMessages(); } } }); diff --git a/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl index b44c64df54..dfba55014e 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 6d200bf7e3..ff50a73120 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 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9ac0997f11..bda540d6b3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -155,9 +155,8 @@ 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, + "References": { + match: /^(.*\/_references\.js)$/i, exclude: [["^", 1, "$"]] } }; @@ -1447,7 +1446,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), @@ -2285,7 +2286,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/protocol.ts b/src/server/protocol.ts index 3761049017..6e96365a4a 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/server.ts b/src/server/server.ts index f4faa0d3c7..fb0b3b781a 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,4 +1,3 @@ -/// /// /// @@ -7,7 +6,11 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; canUseEvents: boolean; - installerEventPort: number; + /** + * 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; disableAutomaticTypingAcquisition: boolean; @@ -22,10 +25,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; @@ -36,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": { @@ -83,10 +90,6 @@ namespace ts.server { pid: number; } - interface NodeSocket { - write(data: string, encoding: string): boolean; - } - interface ReadLineOptions { input: NodeJS.ReadableStream; output?: NodeJS.WritableStream; @@ -243,10 +246,7 @@ namespace ts.server { class NodeTypingsInstaller implements ITypingsInstaller { private installer: NodeChildProcess; - private installerPidReported = false; - private socket: NodeSocket; private projectService: ProjectService; - private eventSender: EventSender; private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID @@ -267,18 +267,11 @@ 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 event: Event) { } isKnownTypesPackageName(name: string): boolean { @@ -306,24 +299,6 @@ namespace ts.server { }); } - private reportInstallerProcessId() { - if (this.installerPidReported) { - return; - } - if (this.socket && this.installer) { - this.sendEvent(0, "typingsInstallerPid", { pid: this.installer.pid }); - 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) { this.projectService = projectService; if (this.logger.hasLevel(LogLevel.requestTime)) { @@ -363,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(); @@ -428,92 +404,81 @@ namespace ts.server { break; } case EventInitializationFailed: - { - if (!this.eventSender) { - break; - } - const body: protocol.TypesInstallerInitializationFailedEventBody = { - message: response.message - }; - const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed"; - this.eventSender.event(body, eventName); - break; - } - case EventBeginInstallTypes: - { - if (!this.eventSender) { - break; - } - const body: protocol.BeginInstallTypesEventBody = { - eventId: response.eventId, - packages: response.packagesToInstall, - }; - const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes"; - this.eventSender.event(body, eventName); - break; - } - case EventEndInstallTypes: - { - if (!this.eventSender) { - 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"; - this.eventSender.event(body, eventName); + 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.eventSender.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.event(response, "setTypings"); + + break; } - - this.projectService.updateTypingsForProject(response); - - if (this.socket) { - this.sendEvent(0, "setTypings", response); - } - - break; - } default: assertTypeIsNever(response); } @@ -529,11 +494,30 @@ namespace ts.server { } 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, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + const { host, eventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, canUseEvents } = options; + + 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 - : new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, host.newLine); + : new NodeTypingsInstaller(telemetryEnabled, logger, host, globalTypingsCacheLocation, typingSafeListLocation, typesMapLocation, npmLocation, event); super({ host, @@ -547,11 +531,49 @@ namespace ts.server { canUseEvents, globalPlugins: options.globalPlugins, pluginProbeLocations: options.pluginProbeLocations, - allowLocalPluginLoads: options.allowLocalPluginLoads }); + allowLocalPluginLoads: options.allowLocalPluginLoads + }); - if (telemetryEnabled && typingsInstaller) { - typingsInstaller.setTelemetrySender(this); + 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(toEvent(body, eventName), this.logger, this.byteLength, this.host.newLine), "utf8"); } exit() { @@ -896,7 +918,7 @@ namespace ts.server { cancellationToken = nullCancellationToken; } - let eventPort: number; + let eventPort: number | undefined; { const str = findArgument("--eventPort"); const v = str && parseInt(str); @@ -936,8 +958,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 f9a145d16f..9e6378ebeb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -105,10 +105,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) { @@ -243,6 +239,22 @@ namespace ts.server { } } + export type Event = (body: T, eventName: string) => void; + + export interface EventSender { + 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; @@ -252,6 +264,9 @@ namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -269,15 +284,15 @@ namespace ts.server { private currentRequestId: number; private errorCheck: MultistepOperation; - private eventHandler: ProjectServiceEventHandler; - - 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) { this.host = opts.host; @@ -293,7 +308,6 @@ namespace ts.server { this.eventHandler = this.canUseEvents ? opts.eventHandler || (event => this.defaultEventHandler(event)) : undefined; - const multistepOperationHost: MultistepOperationHost = { executeWithRequestId: (requestId, action) => this.executeWithRequestId(requestId, action), getCurrentRequestId: () => this.currentRequestId, @@ -321,13 +335,7 @@ namespace ts.server { } 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) { @@ -401,17 +409,12 @@ namespace ts.server { 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); + public event(body: T, eventName: string): void { + this.send(toEvent(eventName, body)); } // For backwards-compatibility only. + /** @deprecated */ public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg); } @@ -1569,15 +1572,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/server/typesMap.json b/src/server/typesMap.json index c3d2b9af83..93e9c2502f 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, "$"]] } }, diff --git a/src/services/codefixes/fixCannotFindModule.ts b/src/services/codefixes/fixCannotFindModule.ts index 29f2475f21..9796bf2fa2 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/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index cd67d10330..ec56a08656 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 f2771c0a7e..6bbdfacac4 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/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 4f11e33c63..d9a9a03c38 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/src/services/refactors/installTypesForPackage.ts b/src/services/refactors/installTypesForPackage.ts index 996645fc15..fbefbbcf4e 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 8fcd7ef13b..d62fc88a64 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 bcebc43789..28de0566a5 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 23520f4295..d3a97c7569 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; @@ -3974,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; @@ -5274,7 +5281,7 @@ declare namespace ts.server.protocol { */ errorCodes?: number[]; } - interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + interface ApplyCodeActionCommandRequestArgs { /** May also be an array of commands. */ command: {}; } @@ -6915,12 +6922,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; @@ -6930,6 +6938,9 @@ declare namespace ts.server { byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; logger: Logger; + /** + * If falsy, all events are suppressed. + */ canUseEvents: boolean; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -6943,21 +6954,22 @@ declare namespace ts.server { private changeSeq; private currentRequestId; private errorCheck; - private eventHandler; - 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); private defaultEventHandler(event); private projectsUpdatedInBackgroundEvent(openFiles); logError(err: Error, cmd: string): void; send(msg: protocol.Message): void; - event(info: T, eventName: string): 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?); private semanticCheck(file, project); diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 68c2ed14f2..43b668fe2d 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; @@ -3974,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 d0ace09909..10d4270892 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", }], }]); diff --git a/tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts b/tests/cases/fourslash/completionsImport_ofAlias_preferShortPath.ts new file mode 100644 index 0000000000..a1eba5981d --- /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`, +});