Merge branch 'tsserverVS-WIP' into tsserverVS-Types2.0

# Conflicts:
#	src/server/typingsCache.ts
This commit is contained in:
Jason Ramsay 2016-08-26 16:41:50 -07:00
commit 7132fe5112
23 changed files with 1668 additions and 191 deletions

View File

@ -5,12 +5,15 @@
/// <reference path="scanner.ts"/>
namespace ts {
/* @internal */
export const compileOnSaveCommandLineOption: CommandLineOption = { name: "compileOnSave", type: "boolean" };
/* @internal */
export const optionDeclarations: CommandLineOption[] = [
{
name: "charset",
type: "string",
},
compileOnSaveCommandLineOption,
{
name: "declaration",
shortName: "d",
@ -808,6 +811,7 @@ namespace ts {
options.configFilePath = configFileName;
const { fileNames, wildcardDirectories } = getFileNames(errors);
const compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors);
return {
options,
@ -815,7 +819,8 @@ namespace ts {
typingOptions,
raw: json,
errors,
wildcardDirectories
wildcardDirectories,
compileOnSave
};
function getFileNames(errors: Diagnostic[]): ExpandResult {
@ -870,13 +875,24 @@ namespace ts {
}
}
export function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Diagnostic[]): boolean {
if (!hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) {
return false;
}
const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption["compileOnSave"], basePath, errors);
if (typeof result === "boolean" && result) {
return result;
}
return false;
}
export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } {
const errors: Diagnostic[] = [];
const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName);
return { options, errors };
}
export function convertTypingOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } {
export function convertTypingOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: TypingOptions, errors: Diagnostic[] } {
const errors: Diagnostic[] = [];
const options = convertTypingOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName);
return { options, errors };

View File

@ -1,5 +1,6 @@
/// <reference path="types.ts"/>
/// <reference path="types.ts"/>
/// <reference path="performance.ts" />
/// <reference path="diagnosticInformationMap.generated.ts" />
/* @internal */
@ -47,6 +48,7 @@ namespace ts {
contains,
remove,
forEachValue: forEachValueInMap,
getKeys,
clear,
};
@ -56,6 +58,14 @@ namespace ts {
}
}
function getKeys() {
const keys: Path[] = [];
for (const key in files) {
keys.push(<Path>key);
}
return keys;
}
// path should already be well-formed so it does not need to be normalized
function get(path: Path): T {
return files[toKey(path)];
@ -311,18 +321,25 @@ namespace ts {
* @param array A sorted array whose first element must be no larger than number
* @param number The value to be searched for in the array.
*/
export function binarySearch(array: number[], value: number): number {
export function binarySearch<T>(array: T[], value: T, comparer?: (v1: T, v2: T) => number): number {
if (!array || array.length === 0) {
return -1;
}
let low = 0;
let high = array.length - 1;
comparer = comparer !== undefined
? comparer
: (v1, v2) => (v1 < v2 ? -1 : (v1 > v2 ? 1 : 0));
while (low <= high) {
const middle = low + ((high - low) >> 1);
const midValue = array[middle];
if (midValue === value) {
if (comparer(midValue, value) === 0) {
return middle;
}
else if (midValue > value) {
else if (comparer(midValue, value) > 0) {
high = middle - 1;
}
else {

View File

@ -36,12 +36,12 @@ namespace ts {
return declarationDiagnostics.getDiagnostics(targetSourceFile ? targetSourceFile.fileName : undefined);
function getDeclarationDiagnosticsFromFile({ declarationFilePath }: EmitFileNames, sources: SourceFile[], isBundledEmit: boolean) {
emitDeclarations(host, resolver, declarationDiagnostics, declarationFilePath, sources, isBundledEmit);
emitDeclarations(host, resolver, declarationDiagnostics, declarationFilePath, sources, isBundledEmit, /*emitOnlyDtsFiles*/ false);
}
}
function emitDeclarations(host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, declarationFilePath: string,
sourceFiles: SourceFile[], isBundledEmit: boolean): DeclarationEmit {
sourceFiles: SourceFile[], isBundledEmit: boolean, emitOnlyDtsFiles: boolean): DeclarationEmit {
const newLine = host.getNewLine();
const compilerOptions = host.getCompilerOptions();
@ -98,7 +98,7 @@ namespace ts {
// global file reference is added only
// - if it is not bundled emit (because otherwise it would be self reference)
// - and it is not already added
if (writeReferencePath(referencedFile, !isBundledEmit && !addedGlobalFileReference)) {
if (writeReferencePath(referencedFile, !isBundledEmit && !addedGlobalFileReference, emitOnlyDtsFiles)) {
addedGlobalFileReference = true;
}
emittedReferencedFiles.push(referencedFile);
@ -1713,7 +1713,7 @@ namespace ts {
* @param referencedFile
* @param addBundledFileReference Determines if global file reference corresponding to bundled file should be emitted or not
*/
function writeReferencePath(referencedFile: SourceFile, addBundledFileReference: boolean): boolean {
function writeReferencePath(referencedFile: SourceFile, addBundledFileReference: boolean, emitOnlyDtsFiles: boolean): boolean {
let declFileName: string;
let addedBundledEmitReference = false;
if (isDeclarationFile(referencedFile)) {
@ -1722,7 +1722,7 @@ namespace ts {
}
else {
// Get the declaration file path
forEachExpectedEmitFile(host, getDeclFileName, referencedFile);
forEachExpectedEmitFile(host, getDeclFileName, referencedFile, emitOnlyDtsFiles);
}
if (declFileName) {
@ -1751,8 +1751,8 @@ namespace ts {
}
/* @internal */
export function writeDeclarationFile(declarationFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean, host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection) {
const emitDeclarationResult = emitDeclarations(host, resolver, emitterDiagnostics, declarationFilePath, sourceFiles, isBundledEmit);
export function writeDeclarationFile(declarationFilePath: string, sourceFiles: SourceFile[], isBundledEmit: boolean, host: EmitHost, resolver: EmitResolver, emitterDiagnostics: DiagnosticCollection, emitOnlyDtsFiles: boolean) {
const emitDeclarationResult = emitDeclarations(host, resolver, emitterDiagnostics, declarationFilePath, sourceFiles, isBundledEmit, emitOnlyDtsFiles);
const emitSkipped = emitDeclarationResult.reportedDeclarationError || host.isEmitBlocked(declarationFilePath) || host.getCompilerOptions().noEmit;
if (!emitSkipped) {
const declarationOutput = emitDeclarationResult.referencesOutput

View File

@ -1,4 +1,4 @@
/// <reference path="checker.ts"/>
/// <reference path="checker.ts"/>
/// <reference path="sourcemap.ts" />
/// <reference path="declarationEmitter.ts"/>
@ -336,7 +336,7 @@ namespace ts {
}
// targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature
export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile): EmitResult {
export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile, emitOnlyDtsFiles?: boolean): EmitResult {
// emit output for the __extends helper function
const extendsHelper = `
var __extends = (this && this.__extends) || function (d, b) {
@ -396,7 +396,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
const newLine = host.getNewLine();
const emitJavaScript = createFileEmitter();
forEachExpectedEmitFile(host, emitFile, targetSourceFile);
forEachExpectedEmitFile(host, emitFile, targetSourceFile, emitOnlyDtsFiles);
return {
emitSkipped,
@ -1615,7 +1615,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
else if (declaration.kind === SyntaxKind.ImportSpecifier) {
// Identifier references named import
write(getGeneratedNameForNode(<ImportDeclaration>declaration.parent.parent.parent));
const name = (<ImportSpecifier>declaration).propertyName || (<ImportSpecifier>declaration).name;
const name = (<ImportSpecifier>declaration).propertyName || (<ImportSpecifier>declaration).name;
const identifier = getTextOfNodeFromSourceText(currentText, name);
if (languageVersion === ScriptTarget.ES3 && identifier === "default") {
write('["default"]');
@ -3254,19 +3254,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
write("var ");
let seen: Map<string>;
for (const id of convertedLoopState.hoistedLocalVariables) {
// Don't initialize seen unless we have at least one element.
// Emit a comma to separate for all but the first element.
if (!seen) {
// Don't initialize seen unless we have at least one element.
// Emit a comma to separate for all but the first element.
if (!seen) {
seen = createMap<string>();
}
else {
write(", ");
}
}
else {
write(", ");
}
if (!(id.text in seen)) {
emit(id);
seen[id.text] = id.text;
}
emit(id);
seen[id.text] = id.text;
}
}
write(";");
writeLine();
@ -7415,7 +7415,7 @@ const _super = (function (geti, seti) {
// - import equals declarations that import external modules are not emitted
continue;
}
// fall-though for import declarations that import internal modules
// fall-though for import declarations that import internal modules
default:
writeLine();
emit(statement);
@ -8364,24 +8364,28 @@ const _super = (function (geti, seti) {
}
}
function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath}: { jsFilePath: string, sourceMapFilePath: string, declarationFilePath: string },
sourceFiles: SourceFile[], isBundledEmit: boolean) {
// Make sure not to write js File and source map file if any of them cannot be written
if (!host.isEmitBlocked(jsFilePath) && !compilerOptions.noEmit) {
emitJavaScript(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit);
}
else {
emitSkipped = true;
function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath }: EmitFileNames,
sourceFiles: SourceFile[], isBundledEmit: boolean, emitOnlyDtsFiles: boolean) {
if (!emitOnlyDtsFiles) {
// Make sure not to write js File and source map file if any of them cannot be written
if (!host.isEmitBlocked(jsFilePath) && !compilerOptions.noEmit) {
emitJavaScript(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit);
}
else {
emitSkipped = true;
}
}
if (declarationFilePath) {
emitSkipped = writeDeclarationFile(declarationFilePath, sourceFiles, isBundledEmit, host, resolver, emitterDiagnostics) || emitSkipped;
emitSkipped = writeDeclarationFile(declarationFilePath, sourceFiles, isBundledEmit, host, resolver, emitterDiagnostics, emitOnlyDtsFiles) || emitSkipped;
}
if (!emitSkipped && emittedFilesList) {
emittedFilesList.push(jsFilePath);
if (sourceMapFilePath) {
emittedFilesList.push(sourceMapFilePath);
if (!emitOnlyDtsFiles) {
emittedFilesList.push(jsFilePath);
if (sourceMapFilePath) {
emittedFilesList.push(sourceMapFilePath);
}
}
if (declarationFilePath) {
emittedFilesList.push(declarationFilePath);

View File

@ -1,4 +1,4 @@
/// <reference path="utilities.ts"/>
/// <reference path="utilities.ts"/>
/// <reference path="scanner.ts"/>
namespace ts {

View File

@ -774,15 +774,15 @@ namespace ts {
return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false));
}
function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult {
return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken));
function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult {
return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles));
}
function isEmitBlocked(emitFileName: string): boolean {
return hasEmitBlockingDiagnostics.contains(toPath(emitFileName, currentDirectory, getCanonicalFileName));
}
function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken): EmitResult {
function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult {
let declarationDiagnostics: Diagnostic[] = [];
if (options.noEmit) {
@ -827,7 +827,8 @@ namespace ts {
const emitResult = emitFiles(
emitResolver,
getEmitHost(writeFileCallback),
sourceFile);
sourceFile,
emitOnlyDtsFiles);
performance.mark("afterEmit");
performance.measure("Emit", "beforeEmit", "afterEmit");

View File

@ -19,6 +19,7 @@ namespace ts {
remove(fileName: Path): void;
forEachValue(f: (key: Path, v: T) => void): void;
getKeys(): Path[];
clear(): void;
}
@ -1755,7 +1756,7 @@ namespace ts {
* used for writing the JavaScript and declaration files. Otherwise, the writeFile parameter
* will be invoked when writing the JavaScript and declaration files.
*/
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult;
getOptionsDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
getGlobalDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
@ -2736,6 +2737,7 @@ namespace ts {
raw?: any;
errors: Diagnostic[];
wildcardDirectories?: MapLike<WatchDirectoryFlags>;
compileOnSave?: boolean;
}
export const enum WatchDirectoryFlags {

View File

@ -1,4 +1,4 @@
/// <reference path="sys.ts" />
/// <reference path="sys.ts" />
/* @internal */
namespace ts {
@ -2218,12 +2218,10 @@ namespace ts {
const options = host.getCompilerOptions();
const outputDir = options.declarationDir || options.outDir; // Prefer declaration folder if specified
if (options.declaration) {
const path = outputDir
? getSourceFilePathInNewDir(sourceFile, host, outputDir)
: sourceFile.fileName;
return removeFileExtension(path) + ".d.ts";
}
const path = outputDir
? getSourceFilePathInNewDir(sourceFile, host, outputDir)
: sourceFile.fileName;
return removeFileExtension(path) + ".d.ts";
}
export interface EmitFileNames {
@ -2233,8 +2231,9 @@ namespace ts {
}
export function forEachExpectedEmitFile(host: EmitHost,
action: (emitFileNames: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean) => void,
targetSourceFile?: SourceFile) {
action: (emitFileNames: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean, emitOnlyDtsFiles: boolean) => void,
targetSourceFile?: SourceFile,
emitOnlyDtsFiles?: boolean) {
const options = host.getCompilerOptions();
// Emit on each source file
if (options.outFile || options.out) {
@ -2267,12 +2266,13 @@ namespace ts {
}
}
const jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, extension);
const declarationFilePath = !isSourceFileJavaScript(sourceFile) && (emitOnlyDtsFiles || options.declaration) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined;
const emitFileNames: EmitFileNames = {
jsFilePath,
sourceMapFilePath: getSourceMapFilePath(jsFilePath, options),
declarationFilePath: !isSourceFileJavaScript(sourceFile) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined
declarationFilePath
};
action(emitFileNames, [sourceFile], /*isBundledEmit*/false);
action(emitFileNames, [sourceFile], /*isBundledEmit*/false, emitOnlyDtsFiles);
}
function onBundledEmit(host: EmitHost) {
@ -2290,7 +2290,7 @@ namespace ts {
sourceMapFilePath: getSourceMapFilePath(jsFilePath, options),
declarationFilePath: options.declaration ? removeFileExtension(jsFilePath) + ".d.ts" : undefined
};
action(emitFileNames, bundledSources, /*isBundledEmit*/true);
action(emitFileNames, bundledSources, /*isBundledEmit*/true, emitOnlyDtsFiles);
}
}

View File

@ -698,7 +698,8 @@ namespace Harness.LanguageService {
/*useOneInferredProject*/ false,
/*typingsInstaller*/ undefined,
Utils.byteLength,
process.hrtime, serverHost);
process.hrtime, serverHost,
/*canUseEvents*/ true);
// Fake the connection between the client and the server
serverHost.writeMessage = client.onMessage.bind(client);

View File

@ -44,7 +44,7 @@ namespace ts.server {
let lastSent: protocol.Message;
beforeEach(() => {
session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger);
session = new Session(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger, /*canUseEvents*/ true);
session.send = (msg: protocol.Message) => {
lastSent = msg;
};
@ -269,7 +269,7 @@ namespace ts.server {
lastSent: protocol.Message;
customHandler = "testhandler";
constructor() {
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger);
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger, /*canUseEvents*/ true);
this.addProtocolHandler(this.customHandler, () => {
return { response: undefined, responseRequired: true };
});
@ -327,7 +327,7 @@ namespace ts.server {
class InProcSession extends Session {
private queue: protocol.Request[] = [];
constructor(private client: InProcClient) {
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger);
super(mockHost, nullCancellationToken, /*useOneInferredProject*/ false, /*typingsInstaller*/ undefined, Utils.byteLength, process.hrtime, mockLogger, /*canUseEvents*/ true);
this.addProtocolHandler("echo", (req: protocol.Request) => ({
response: req.arguments,
responseRequired: true

File diff suppressed because it is too large Load Diff

368
src/server/builder.ts Normal file
View File

@ -0,0 +1,368 @@
/// <reference path="..\compiler\commandLineParser.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="protocol.d.ts" />
/// <reference path="session.ts" />
/// <reference types="node" />
namespace ts.server {
interface Hash {
update(data: any, input_encoding?: string): Hash;
digest(encoding: string): any;
}
const crypto: {
createHash(algorithm: string): Hash
} = require("crypto");
/**
* An abstract file info that maintains a shape signature.
*/
export class BuilderFileInfo {
private lastCheckedShapeSignature: string;
constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) {
}
public isExternalModuleOrHasOnlyAmbientExternalModules() {
const sourceFile = this.getSourceFile();
return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile);
}
/**
* For script files that contains only ambient external modules, although they are not actually external module files,
* they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
* there are no point to rebuild all script files if these special files have changed. However, if any statement
* in the file is not ambient external module, we treat it as a regular script file.
*/
private containsOnlyAmbientModules(sourceFile: SourceFile) {
for (const statement of sourceFile.statements) {
if (statement.kind !== SyntaxKind.ModuleDeclaration || (<ModuleDeclaration>statement).name.kind !== SyntaxKind.StringLiteral) {
return false;
}
}
return true;
}
private computeHash(text: string): string {
return crypto.createHash("md5")
.update(text)
.digest("base64");
}
private getSourceFile(): SourceFile {
return this.project.getSourceFile(this.scriptInfo.path);
}
/**
* @return {boolean} indicates if the shape signature has changed since last update.
*/
public updateShapeSignature() {
const sourceFile = this.getSourceFile();
if (!sourceFile) {
return true;
}
const lastSignature = this.lastCheckedShapeSignature;
if (sourceFile.isDeclarationFile) {
this.lastCheckedShapeSignature = this.computeHash(sourceFile.text);
}
else {
const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true);
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text);
}
}
return !lastSignature || this.lastCheckedShapeSignature !== lastSignature;
}
}
export interface Builder {
readonly project: Project;
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
onProjectUpdateGraph(): void;
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
}
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
private fileInfos = createFileMap<T>();
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
}
protected getFileInfo(path: Path): T {
return this.fileInfos.get(path);
}
protected getOrCreateFileInfo(path: Path): T {
let fileInfo = this.getFileInfo(path);
if (!fileInfo) {
const scriptInfo = this.project.getScriptInfo(path);
fileInfo = new this.ctor(scriptInfo, this.project);
this.setFileInfo(path, fileInfo);
}
return fileInfo;
}
protected getFileInfoPaths(): Path[] {
return this.fileInfos.getKeys();
}
protected setFileInfo(path: Path, info: T) {
this.fileInfos.set(path, info);
}
protected removeFileInfo(path: Path) {
this.fileInfos.remove(path);
}
protected forEachFileInfo(action: (fileInfo: T) => any) {
this.fileInfos.forEachValue((path: Path, value: T) => action(value));
}
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
abstract onProjectUpdateGraph(): void;
/**
* @returns {boolean} whether the emit was conducted or not
*/
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo) {
return false;
}
const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false);
if (!emitSkipped) {
for (const outputFile of outputFiles) {
writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark);
}
}
return !emitSkipped;
}
}
class NonModuleBuilder extends AbstractBuilder<BuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, BuilderFileInfo);
}
onProjectUpdateGraph() {
}
/**
* Note: didn't use path as parameter because the returned file names will be directly
* consumed by the API user, which will use it to interact with file systems. Path
* should only be used internally, because the case sensitivity is not trustable.
*/
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
const info = this.getOrCreateFileInfo(scriptInfo.path);
if (info.updateShapeSignature()) {
const options = this.project.getCompilerOptions();
// If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
// so returning the file itself is good enough.
if (options && (options.out || options.outFile)) {
return [scriptInfo.fileName];
}
return this.project.getFileNamesWithoutDefaultLib();
}
return [scriptInfo.fileName];
}
}
class ModuleBuilderFileInfo extends BuilderFileInfo {
references: ModuleBuilderFileInfo[] = [];
referencedBy: ModuleBuilderFileInfo[] = [];
scriptVersionForReferences: string;
static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): number {
const l = lf.scriptInfo.fileName;
const r = rf.scriptInfo.fileName;
return (l < r ? -1 : (l > r ? 1 : 0));
};
static addToReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
if (array.length === 0) {
array.push(fileInfo);
return;
}
const insertIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
if (insertIndex < 0) {
array.splice(~insertIndex, 0, fileInfo);
}
}
static removeFromReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
if (!array || array.length === 0) {
return;
}
if (array[0] === fileInfo) {
array.splice(0, 1);
return;
}
const removeIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
if (removeIndex >= 0) {
array.splice(removeIndex, 1);
}
}
addReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
ModuleBuilderFileInfo.addToReferenceList(this.referencedBy, fileInfo);
}
removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
ModuleBuilderFileInfo.removeFromReferenceList(this.referencedBy, fileInfo);
}
removeFileReferences() {
for (const reference of this.references) {
reference.removeReferencedBy(this);
}
this.references = [];
}
}
class ModuleBuilder extends AbstractBuilder<ModuleBuilderFileInfo> {
constructor(public readonly project: Project) {
super(project, ModuleBuilderFileInfo);
}
private projectVersionForDependencyGraph: string;
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] {
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return [];
}
const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path);
if (referencedFilePaths.length > 0) {
return map(referencedFilePaths, f => this.getOrCreateFileInfo(f)).sort(ModuleBuilderFileInfo.compareFileInfos);
}
return [];
}
onProjectUpdateGraph() {
this.ensureProjectDependencyGraphUpToDate();
}
private ensureProjectDependencyGraphUpToDate() {
if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) {
const currentScriptInfos = this.project.getScriptInfos();
for (const scriptInfo of currentScriptInfos) {
const fileInfo = this.getOrCreateFileInfo(scriptInfo.path);
this.updateFileReferences(fileInfo);
}
this.forEachFileInfo(fileInfo => {
if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
// This file was deleted from this project
fileInfo.removeFileReferences();
this.removeFileInfo(fileInfo.scriptInfo.path);
}
});
this.projectVersionForDependencyGraph = this.project.getProjectVersion();
}
}
private updateFileReferences(fileInfo: ModuleBuilderFileInfo) {
// Only need to update if the content of the file changed.
if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) {
return;
}
const newReferences = this.getReferencedFileInfos(fileInfo);
const oldReferences = fileInfo.references;
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldReferences.length && newIndex < newReferences.length) {
const oldReference = oldReferences[oldIndex];
const newReference = newReferences[newIndex];
const compare = ModuleBuilderFileInfo.compareFileInfos(oldReference, newReference);
if (compare < 0) {
// New reference is greater then current reference. That means
// the current reference doesn't exist anymore after parsing. So delete
// references.
oldReference.removeReferencedBy(fileInfo);
oldIndex++;
}
else if (compare > 0) {
// A new reference info. Add it.
newReference.addReferencedBy(fileInfo);
newIndex++;
}
else {
// Equal. Go to next
oldIndex++;
newIndex++;
}
}
// Clean old references
for (let i = oldIndex; i < oldReferences.length; i++) {
oldReferences[i].removeReferencedBy(fileInfo);
}
// Update new references
for (let i = newIndex; i < newReferences.length; i++) {
newReferences[i].addReferencedBy(fileInfo);
}
fileInfo.references = newReferences;
fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion();
}
getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
this.ensureProjectDependencyGraphUpToDate();
const fileInfo = this.getFileInfo(scriptInfo.path);
if (!fileInfo || !fileInfo.updateShapeSignature()) {
return [scriptInfo.fileName];
}
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return this.project.getFileNamesWithoutDefaultLib();
}
const options = this.project.getCompilerOptions();
if (options && (options.isolatedModules || options.out || options.outFile)) {
return [scriptInfo.fileName];
}
// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
// Use slice to clone the array to avoid manipulating in place
const queue = fileInfo.referencedBy.slice(0);
const fileNameSet = createMap<boolean>();
fileNameSet[scriptInfo.fileName] = true;
while (queue.length > 0) {
const processingFileInfo = queue.pop();
if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) {
for (const potentialFileInfo of processingFileInfo.referencedBy) {
if (!fileNameSet[potentialFileInfo.scriptInfo.fileName]) {
queue.push(potentialFileInfo);
}
}
}
fileNameSet[processingFileInfo.scriptInfo.fileName] = true;
}
return Object.keys(fileNameSet);
}
}
export function createBuilder(project: Project): Builder {
const moduleKind = project.getCompilerOptions().module;
switch (moduleKind) {
case ModuleKind.None:
return new NonModuleBuilder(project);
default:
return new ModuleBuilder(project);
}
}
}

View File

@ -1,4 +1,4 @@
/// <reference types="node"/>
/// <reference types="node" />
// TODO: extract services types

View File

@ -30,7 +30,7 @@ namespace ts.server {
interface ConfigFileConversionResult {
success: boolean;
errors?: Diagnostic[];
configFileErrors?: Diagnostic[];
projectOptions?: ProjectOptions;
}
@ -73,6 +73,10 @@ namespace ts.server {
}
}
function createFileNotFoundDiagnostic(fileName: string) {
return createCompilerDiagnostic(Diagnostics.File_0_not_found, fileName);
}
/**
* TODO: enforce invariants:
* - script info can be never migrate to state - root file in inferred project, this is only a starting point
@ -656,7 +660,7 @@ namespace ts.server {
const configObj = parseConfigFileTextToJson(configFilename, this.host.readFile(configFilename));
if (configObj.error) {
return { success: false, errors: [configObj.error] };
return { success: false, configFileErrors: [configObj.error] };
}
const parsedCommandLine = parseJsonConfigFileContent(
@ -669,12 +673,12 @@ namespace ts.server {
Debug.assert(!!parsedCommandLine.fileNames);
if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
return { success: false, errors: parsedCommandLine.errors };
return { success: false, configFileErrors: parsedCommandLine.errors };
}
if (parsedCommandLine.fileNames.length === 0) {
const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
return { success: false, errors: [error] };
return { success: false, configFileErrors: [error] };
}
const projectOptions: ProjectOptions = {
@ -682,7 +686,8 @@ namespace ts.server {
compilerOptions: parsedCommandLine.options,
configHasFilesProperty: configObj.config["files"] !== undefined,
wildcardDirectories: createMap(parsedCommandLine.wildcardDirectories),
typingOptions: parsedCommandLine.typingOptions
typingOptions: parsedCommandLine.typingOptions,
compileOnSave: parsedCommandLine.compileOnSave
};
return { success: true, projectOptions };
}
@ -705,17 +710,18 @@ namespace ts.server {
return false;
}
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], compilerOptions: CompilerOptions) {
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typingOptions: TypingOptions) {
const project = new ExternalProject(
projectFileName,
this,
this.documentRegistry,
compilerOptions,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files, externalFilePropertyReader));
options,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(options, files, externalFilePropertyReader),
options.compileOnSave === undefined ? true : options.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined);
this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typingOptions);
this.externalProjects.push(project);
return { project, errors };
return project;
}
private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) {
@ -726,11 +732,11 @@ namespace ts.server {
this.documentRegistry,
projectOptions.configHasFilesProperty,
projectOptions.compilerOptions,
projectOptions.typingOptions,
projectOptions.wildcardDirectories,
/*languageServiceEnabled*/ !sizeLimitExceeded);
/*languageServiceEnabled*/ !sizeLimitExceeded,
projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName);
this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typingOptions);
project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project));
if (!sizeLimitExceeded) {
@ -739,7 +745,7 @@ namespace ts.server {
project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path));
this.configuredProjects.push(project);
return { project, errors };
return project;
}
private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void {
@ -748,7 +754,7 @@ namespace ts.server {
}
}
private addFilesToProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName: string): Diagnostic[] {
private addFilesToProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName: string, typingOptions: TypingOptions): void {
let errors: Diagnostic[];
for (const f of files) {
const rootFilename = propertyReader.getFileName(f);
@ -759,31 +765,37 @@ namespace ts.server {
project.addRoot(info);
}
else {
(errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename));
(errors || (errors = [])).push(createFileNotFoundDiagnostic(rootFilename));
}
}
project.setProjectErrors(errors);
project.setTypingOptions(typingOptions);
project.updateGraph();
return errors;
}
private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult {
const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName);
if (!conversionResult.success) {
return { success: false, errors: conversionResult.errors };
// open project with no files and set errors on the project
const project = this.createAndAddConfiguredProject(configFileName, { files: [], compilerOptions: {} }, clientFileName);
project.setProjectErrors(conversionResult.configFileErrors);
return { success: false, errors: conversionResult.configFileErrors };
}
const { project, errors } = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName);
return { success: true, project, errors };
const project = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName);
return { success: true, project, errors: project.getProjectErrors() };
}
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions) {
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypingOptions: TypingOptions, compileOnSave: boolean, configFileErrors: Diagnostic[]) {
const oldRootScriptInfos = project.getRootScriptInfos();
const newRootScriptInfos: ScriptInfo[] = [];
const newRootScriptInfoMap: NormalizedPathMap<ScriptInfo> = createNormalizedPathMap<ScriptInfo>();
let projectErrors: Diagnostic[];
let rootFilesChanged = false;
for (const f of newUncheckedFiles) {
const newRootFile = propertyReader.getFileName(f);
if (!this.host.fileExists(newRootFile)) {
(projectErrors || (projectErrors = [])).push(createFileNotFoundDiagnostic(newRootFile));
continue;
}
const normalizedPath = toNormalizedPath(newRootFile);
@ -835,6 +847,10 @@ namespace ts.server {
}
project.setCompilerOptions(newOptions);
(<ExternalProject | ConfiguredProject>project).setTypingOptions(newTypingOptions);
project.compileOnSaveEnabled = !!compileOnSave;
project.setProjectErrors(concatenate(configFileErrors, projectErrors));
project.updateGraph();
}
@ -845,9 +861,11 @@ namespace ts.server {
return;
}
const { success, projectOptions, errors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
if (!success) {
return errors;
// reset project settings to default
this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/false, configFileErrors);
return configFileErrors;
}
if (this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) {
@ -864,7 +882,7 @@ namespace ts.server {
project.enableLanguageService();
}
this.watchConfigDirectoryForProject(project, projectOptions);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions, projectOptions.compileOnSave, configFileErrors);
}
}
@ -872,7 +890,7 @@ namespace ts.server {
const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length;
const project = useExistingProject
? this.inferredProjects[0]
: new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true, this.compilerOptionsForInferredProjects);
: new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true, this.compilerOptionsForInferredProjects, /*compileOnSaveEnabled*/ false);
project.addRoot(root);
@ -1047,15 +1065,15 @@ namespace ts.server {
this.printProjects();
}
private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void {
private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: ProjectFilesWithTSDiagnostics[]): void {
for (const proj of currentProjects) {
const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p);
result.push(proj.getChangesSinceVersion(knownProject && knownProject.version));
}
}
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] {
const files: protocol.ProjectFiles[] = [];
synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): ProjectFilesWithTSDiagnostics[] {
const files: ProjectFilesWithTSDiagnostics[] = [];
this.collectChanges(knownProjects, this.externalProjects, files);
this.collectChanges(knownProjects, this.configuredProjects, files);
this.collectChanges(knownProjects, this.inferredProjects, files);
@ -1134,7 +1152,7 @@ namespace ts.server {
openExternalProject(proj: protocol.ExternalProject): void {
const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
if (externalProject) {
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options);
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined);
return;
}
@ -1166,7 +1184,7 @@ namespace ts.server {
}
}
else {
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options);
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typingOptions);
}
}
}

View File

@ -76,6 +76,10 @@ namespace ts.server {
return this.compilationSettings;
}
useCaseSensitiveFileNames() {
return this.host.useCaseSensitiveFileNames;
}
getCancellationToken() {
return this.cancellationToken;
}

View File

@ -1,8 +1,9 @@
/// <reference path="..\services\services.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts"/>
/// <reference path="scriptInfo.ts"/>
/// <reference path="lsHost.ts"/>
/// <reference path="typingsCache.ts"/>
/// <reference path="builder.ts"/>
namespace ts.server {
@ -19,6 +20,16 @@ namespace ts.server {
}
}
const jsOrDts = [".js", ".d.ts"];
export function allRootFilesAreJsOrDts(project: Project): boolean {
return project.getRootScriptInfos().every(f => fileExtensionIsAny(f.fileName, jsOrDts));
}
export interface ProjectFilesWithTSDiagnostics extends protocol.ProjectFiles {
projectErrors: Diagnostic[];
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
@ -26,6 +37,7 @@ namespace ts.server {
private program: ts.Program;
private languageService: LanguageService;
builder: Builder;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
*/
@ -49,13 +61,16 @@ namespace ts.server {
private typingFiles: TypingsArray;
protected projectErrors: Diagnostic[];
constructor(
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions) {
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean) {
if (!this.compilerOptions) {
this.compilerOptions = ts.getDefaultCompilerOptions();
@ -73,9 +88,15 @@ namespace ts.server {
else {
this.disableLanguageService();
}
this.builder = createBuilder(this);
this.markAsDirty();
}
getProjectErrors() {
return this.projectErrors;
}
getLanguageService(ensureSynchronized = true): LanguageService {
if (ensureSynchronized) {
this.updateGraph();
@ -83,6 +104,14 @@ namespace ts.server {
return this.languageService;
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
if (!this.languageServiceEnabled) {
return [];
}
this.updateGraph();
return this.builder.getFilesAffectedBy(scriptInfo);
}
getProjectVersion() {
return this.projectStateVersion.toString();
}
@ -103,6 +132,14 @@ namespace ts.server {
}
abstract getProjectName(): string;
abstract getTypingOptions(): TypingOptions;
getSourceFile(path: Path) {
if (!this.program) {
return undefined;
}
return this.program.getSourceFileByPath(path);
}
close() {
if (this.program) {
@ -157,6 +194,17 @@ namespace ts.server {
return this.rootFiles;
}
getScriptInfos() {
return map(this.program.getSourceFiles(), sourceFile => this.getScriptInfoLSHost(sourceFile.path));
}
getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
if (!this.languageServiceEnabled) {
return undefined;
}
return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
}
getFileNames() {
if (!this.program) {
return [];
@ -177,6 +225,14 @@ namespace ts.server {
return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
}
getFileNamesWithoutDefaultLib() {
if (!this.languageServiceEnabled) {
return this.getRootFiles();
}
const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
return filter(this.getFileNames(), file => getBaseFileName(file) !== defaultLibraryFileName);
}
containsScriptInfo(info: ScriptInfo): boolean {
return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
}
@ -269,6 +325,7 @@ namespace ts.server {
}
}
}
this.builder.onProjectUpdateGraph();
return hasChanges;
}
@ -282,7 +339,9 @@ namespace ts.server {
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
Debug.assert(!scriptInfo || scriptInfo.isAttached(this));
if (scriptInfo && !scriptInfo.isAttached(this)) {
return Errors.ThrowProjectDoesNotContainDocument(fileName, this);
}
return scriptInfo;
}
@ -324,7 +383,7 @@ namespace ts.server {
return false;
}
getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles {
getChangesSinceVersion(lastKnownVersion?: number): ProjectFilesWithTSDiagnostics {
this.updateGraph();
const info = {
@ -337,7 +396,7 @@ namespace ts.server {
if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
// if current structure version is the same - return info witout any changes
if (this.projectStructureVersion == this.lastReportedVersion) {
return { info };
return { info, projectErrors: this.projectErrors };
}
// compute and return the difference
const lastReportedFileNames = this.lastReportedFileNames;
@ -359,17 +418,70 @@ namespace ts.server {
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.projectStructureVersion;
return { info, changes: { added, removed } };
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
}
else {
// unknown version - return everything
const projectFileNames = this.getFileNames();
this.lastReportedFileNames = arrayToMap(projectFileNames, x => x);
this.lastReportedVersion = this.projectStructureVersion;
return { info, files: projectFileNames };
return { info, files: projectFileNames, projectErrors: this.projectErrors };
}
}
getReferencedFiles(path: Path): Path[] {
if (!this.languageServiceEnabled) {
return [];
}
const sourceFile = this.getSourceFile(path);
if (!sourceFile) {
return [];
}
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
const referencedFiles = createMap<boolean>();
if (sourceFile.imports) {
const checker: TypeChecker = this.program.getTypeChecker();
for (const importName of sourceFile.imports) {
const symbol = checker.getSymbolAtLocation(importName);
if (symbol && symbol.declarations && symbol.declarations[0]) {
const declarationSourceFile = symbol.declarations[0].getSourceFile();
if (declarationSourceFile) {
referencedFiles[declarationSourceFile.path] = true;
}
}
}
}
const currentDirectory = getDirectoryPath(path);
const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames);
// Handle triple slash references
if (sourceFile.referencedFiles) {
for (const referencedFile of sourceFile.referencedFiles) {
const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName);
referencedFiles[referencedPath] = true;
}
}
// Handle type reference directives
if (sourceFile.resolvedTypeReferenceDirectiveNames) {
for (const typeName in sourceFile.resolvedTypeReferenceDirectiveNames) {
const resolvedTypeReferenceDirective = sourceFile.resolvedTypeReferenceDirectiveNames[typeName];
if (!resolvedTypeReferenceDirective) {
continue;
}
const fileName = resolvedTypeReferenceDirective.resolvedFileName;
const typeFilePath = toPath(fileName, currentDirectory, getCanonicalFileName);
referencedFiles[typeFilePath] = true;
}
}
return map(Object.keys(referencedFiles), key => <Path>key);
}
// remove a root file from project
private removeRootFileIfNecessary(info: ScriptInfo): void {
if (this.isRoot(info)) {
@ -391,13 +503,14 @@ namespace ts.server {
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions) {
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean) {
super(ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
compilerOptions);
compilerOptions,
compileOnSaveEnabled);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
@ -414,9 +527,18 @@ namespace ts.server {
this.projectService.stopWatchingDirectory(directory);
}
}
getTypingOptions(): TypingOptions {
return {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
}
export class ConfiguredProject extends Project {
private typingOptions: TypingOptions;
private projectFileWatcher: FileWatcher;
private directoryWatcher: FileWatcher;
private directoriesWatchedForWildcards: Map<FileWatcher>;
@ -428,10 +550,18 @@ namespace ts.server {
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
compilerOptions: CompilerOptions,
private typingOptions: TypingOptions,
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions);
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
this.typingOptions = newTypingOptions;
}
getTypingOptions() {
@ -508,12 +638,46 @@ namespace ts.server {
}
export class ExternalProject extends Project {
private typingOptions: TypingOptions;
constructor(readonly externalProjectName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
languageServiceEnabled: boolean) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions);
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
getTypingOptions() {
return this.typingOptions;
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
if (!newTypingOptions) {
// set default typings options
newTypingOptions = {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
else {
if (newTypingOptions.enableAutoDiscovery === undefined) {
// if autoDiscovery was not specified by the caller - set it based on the content of the project
newTypingOptions.enableAutoDiscovery = allRootFilesAreJsOrDts(this);
}
if (!newTypingOptions.include) {
newTypingOptions.include = [];
}
if (!newTypingOptions.exclude) {
newTypingOptions.exclude = [];
}
}
this.typingOptions = newTypingOptions;
}
getProjectName() {

View File

@ -498,7 +498,16 @@ declare namespace ts.server.protocol {
export interface ExternalProject {
projectFileName: string;
rootFiles: ExternalFile[];
options: CompilerOptions;
options: ExternalProjectCompilerOptions;
typingOptions?: TypingOptions;
}
/**
* For external projects, some of the project settings are sent together with
* compiler settings.
*/
export interface ExternalProjectCompilerOptions extends CompilerOptions {
compileOnSave?: boolean;
}
export interface ProjectVersionInfo {
@ -526,6 +535,10 @@ declare namespace ts.server.protocol {
changes?: ProjectChanges;
}
export interface ProjectFilesWithDiagnostics extends ProjectFiles {
projectErrors: DiagnosticWithLinePosition[];
}
export interface ChangedOpenFile {
fileName: string;
changes: ts.TextChange[];
@ -720,6 +733,26 @@ declare namespace ts.server.protocol {
export interface CloseRequest extends FileRequest {
}
export interface CompileOnSaveAffectedFileListRequest extends FileRequest {
}
export interface CompileOnSaveAffectedFileListSingleProject {
projectFileName: string;
fileNames: string[];
}
export interface CompileOnSaveAffectedFileListResponse extends Response {
body: CompileOnSaveAffectedFileListSingleProject[];
}
export interface CompileOnSaveEmitFileRequest extends FileRequest {
args: CompileOnSaveEmitFileRequestArgs;
}
export interface CompileOnSaveEmitFileRequestArgs extends FileRequestArgs {
forced?: boolean;
}
/**
* Quickinfo request; value of command field is
* "quickinfo". Return response giving a quick type and

View File

@ -232,8 +232,22 @@ namespace ts.server {
}
class IOSession extends Session {
constructor(host: ServerHost, cancellationToken: HostCancellationToken, eventPort: number, useSingleInferredProject: boolean, logger: server.Logger) {
super(host, cancellationToken, useSingleInferredProject, new NodeTypingsInstaller(logger, eventPort, host.newLine), Buffer.byteLength, process.hrtime, logger);
constructor(
host: ServerHost,
cancellationToken: HostCancellationToken,
installerEventPort: number,
canUseEvents: boolean,
useSingleInferredProject: boolean,
logger: server.Logger) {
super(
host,
cancellationToken,
useSingleInferredProject,
new NodeTypingsInstaller(logger, installerEventPort, host.newLine),
Buffer.byteLength,
process.hrtime,
logger,
canUseEvents);
}
exit() {
@ -477,7 +491,7 @@ namespace ts.server {
}
const useSingleInferredProject = sys.args.indexOf("--useSingleInferredProject") >= 0;
const ioSession = new IOSession(sys, cancellationToken, eventPort, useSingleInferredProject, logger);
const ioSession = new IOSession(sys, cancellationToken, eventPort, /*canUseEvents*/ eventPort === undefined, useSingleInferredProject, logger);
process.on("uncaughtException", function (err: Error) {
ioSession.logError(err, "unknown");
});

View File

@ -1,4 +1,4 @@
/// <reference path="..\compiler\commandLineParser.ts" />
/// <reference path="..\compiler\commandLineParser.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="protocol.d.ts" />
/// <reference path="editorServices.ts" />
@ -79,6 +79,8 @@ namespace ts.server {
export const Completions = "completions";
export const CompletionsFull = "completions-full";
export const CompletionDetails = "completionEntryDetails";
export const CompileOnSaveAffectedFileList = "compileOnSaveAffectedFileList";
export const CompileOnSaveEmitFile = "compileOnSaveEmitFile";
export const Configure = "configure";
export const Definition = "definition";
export const DefinitionFull = "definition-full";
@ -158,11 +160,14 @@ namespace ts.server {
protected readonly typingsInstaller: ITypingsInstaller,
private byteLength: (buf: string, encoding?: string) => number,
private hrtime: (start?: number[]) => number[],
protected logger: Logger) {
this.projectService =
new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, (eventName, project, fileName) => {
this.handleEvent(eventName, project, fileName);
});
protected logger: Logger,
protected readonly canUseEvents: boolean) {
const eventHandler: ProjectServiceEventHandler = canUseEvents
? (eventName, project, fileName) => this.handleEvent(eventName, project, fileName)
: undefined;
this.projectService = new ProjectService(host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, eventHandler);
this.gcTimer = new GcTimer(host, /*delay*/ 15000, logger);
}
@ -186,6 +191,12 @@ 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));
}
@ -930,6 +941,32 @@ namespace ts.server {
}, []);
}
private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): protocol.CompileOnSaveAffectedFileListSingleProject[] {
const info = this.projectService.getScriptInfo(args.file);
const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = [];
// if specified a project, we only return affected file list in this project
const projectsToSearch = args.projectFileName ? [this.projectService.findProject(args.projectFileName)] : info.containingProjects;
for (const project of projectsToSearch) {
if (project.compileOnSaveEnabled) {
result.push({
projectFileName: project.getProjectName(),
fileNames: project.getCompileOnSaveAffectedFileList(info)
});
}
}
return result;
}
private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs) {
const { file, project } = this.getFileAndProject(args);
if (!project) {
Errors.ThrowNoProject();
}
const scriptInfo = project.getScriptInfo(file);
return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
}
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems {
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
@ -1224,7 +1261,21 @@ namespace ts.server {
},
[CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => {
const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects);
return this.requiredResponse(result);
if (!result.some(p => p.projectErrors && p.projectErrors.length !== 0)) {
return this.requiredResponse(result);
}
const converted = map(result, p => {
if (!p.projectErrors || p.projectErrors.length === 0) {
return p;
}
return {
info: p.info,
changes: p.changes,
files: p.files,
projectErrors: this.convertToDiagnosticsWithLinePosition(p.projectErrors, /*scriptInfo*/ undefined)
};
});
return this.requiredResponse(converted);
},
[CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => {
this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles);
@ -1331,6 +1382,12 @@ namespace ts.server {
[CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => {
return this.requiredResponse(this.getCompletionEntryDetails(request.arguments));
},
[CommandNames.CompileOnSaveAffectedFileList]: (request: protocol.CompileOnSaveAffectedFileListRequest) => {
return this.requiredResponse(this.getCompileOnSaveAffectedFileList(request.arguments));
},
[CommandNames.CompileOnSaveEmitFile]: (request: protocol.CompileOnSaveEmitFileRequest) => {
return this.requiredResponse(this.emitFile(request.arguments));
},
[CommandNames.SignatureHelp]: (request: protocol.SignatureHelpRequest) => {
return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ true));
},

View File

@ -20,20 +20,6 @@ namespace ts.server {
poisoned: boolean;
}
const emptyArray: any[] = [];
const jsOrDts = [".js", ".jsx", ".d.ts"];
function getTypingOptionsForProjects(proj: Project): TypingOptions {
if (proj.projectKind === ProjectKind.Configured) {
return (<ConfiguredProject>proj).getTypingOptions();
}
const enableAutoDiscovery = proj.getFileNames().every(f => fileExtensionIsAny(f, jsOrDts));
// TODO: add .d.ts files to excludes
return { enableAutoDiscovery, include: emptyArray, exclude: emptyArray };
}
function setIsEqualTo(arr1: string[], arr2: string[]): boolean {
if (arr1 === arr2) {
return true;
@ -89,9 +75,9 @@ namespace ts.server {
}
getTypingsForProject(project: Project): TypingsArray {
const typingOptions = getTypingOptionsForProjects(project);
const typingOptions = project.getTypingOptions();
if (!typingOptions.enableAutoDiscovery) {
if (!typingOptions || !typingOptions.enableAutoDiscovery) {
return <any>emptyArray;
}
@ -113,7 +99,7 @@ namespace ts.server {
}
invalidateCachedTypingsForProject(project: Project) {
const typingOptions = getTypingOptionsForProjects(project);
const typingOptions = project.getTypingOptions();
if (!typingOptions.enableAutoDiscovery) {
return;
}

View File

@ -1,4 +1,4 @@
/// <reference path="types.d.ts" />
/// <reference path="types.d.ts" />
namespace ts.server {
export enum LogLevel {
@ -8,6 +8,8 @@ namespace ts.server {
verbose
}
export const emptyArray: ReadonlyArray<any> = [];
export interface Logger {
close(): void;
hasLevel(level: LogLevel): boolean;
@ -62,6 +64,9 @@ namespace ts.server {
export function ThrowProjectLanguageServiceDisabled(): never {
throw new Error("The project's language service is disabled.");
}
export function ThrowProjectDoesNotContainDocument(fileName: string, project: Project): never {
throw new Error(`Project '${project.getProjectName()}' does not contain document '${fileName}'`);
}
}
export function getDefaultFormatCodeSettings(host: ServerHost): FormatCodeSettings {
@ -218,6 +223,7 @@ namespace ts.server {
wildcardDirectories?: Map<WatchDirectoryFlags>;
compilerOptions?: CompilerOptions;
typingOptions?: TypingOptions;
compileOnSave?: boolean;
}
export function isInferredProjectName(name: string) {

View File

@ -29,6 +29,8 @@ namespace ts.JsTyping {
// that we are confident require typings
let safeList: Map<string>;
const EmptySafeList: Map<string> = createMap<string>();
/**
* @param host is the object providing I/O related operations.
* @param fileNames are the file names that belong to the same project
@ -63,7 +65,7 @@ namespace ts.JsTyping {
if (!safeList) {
const result = readConfigFile(safeListPath, (path: string) => host.readFile(path));
safeList = createMap<string>(result.config);
safeList = result.config ? createMap<string>(result.config) : EmptySafeList;
}
const filesToWatch: string[] = [];
@ -163,10 +165,8 @@ namespace ts.JsTyping {
const jsFileNames = filter(fileNames, hasJavaScriptFileExtension);
const inferredTypingNames = map(jsFileNames, f => removeFileExtension(getBaseFileName(f.toLowerCase())));
const cleanedTypingNames = map(inferredTypingNames, f => f.replace(/((?:\.|-)min(?=\.|$))|((?:-|\.)\d+)/g, ""));
if (safeList === undefined) {
mergeTypings(cleanedTypingNames);
}
else {
if (safeList !== EmptySafeList) {
mergeTypings(filter(cleanedTypingNames, f => f in safeList));
}

View File

@ -1253,7 +1253,7 @@ namespace ts {
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
getEmitOutput(fileName: string): EmitOutput;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
getProgram(): Program;
@ -3053,7 +3053,7 @@ namespace ts {
let program: Program;
let lastProjectVersion: string;
const useCaseSensitivefileNames = false;
const useCaseSensitivefileNames = host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames();
const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken());
const currentDirectory = host.getCurrentDirectory();
@ -7069,7 +7069,7 @@ namespace ts {
return ts.NavigateTo.getNavigateToItems(program, checker, cancellationToken, searchValue, maxResultCount);
}
function getEmitOutput(fileName: string): EmitOutput {
function getEmitOutput(fileName: string, emitDeclarationsOnly?: boolean): EmitOutput {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
@ -7083,7 +7083,7 @@ namespace ts {
});
}
const emitOutput = program.emit(sourceFile, writeFile, cancellationToken);
const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitDeclarationsOnly);
return {
outputFiles,