Add APIs for enabling CompileOnSave on tsserver (#9837)

* Add API to get only the emited declarations output

* Add nonModuleBuilder

* Add basic tests for CompileOnSaveAffectedFileList API

* Add API for compile single file

* Avoid invoking project.languageService directly

* Add API to query if compileOnSave is enabled for a project

* Seperate check and emit signatures

* Use Path type for internal file name matching and simplifying builder logic

* Always return cascaded affected list

* Correct the tsconfig file in compileOnSave tests
Also move the CompileOnSave option out of compilerOptions

* Reduce string to path conversion
This commit is contained in:
Zhengbo Li 2016-08-23 16:11:52 -07:00 committed by GitHub
parent d736db3b01
commit a082857ae8
19 changed files with 4390 additions and 71 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,6 +875,17 @@ 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);

View file

@ -1,4 +1,4 @@
/// <reference path="types.ts"/>
/// <reference path="types.ts"/>
/// <reference path="performance.ts" />
@ -47,6 +47,7 @@ namespace ts {
contains,
remove,
forEachValue: forEachValueInMap,
getKeys,
clear,
};
@ -56,6 +57,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 +320,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

@ -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,14 +8364,16 @@ const _super = (function (geti, seti) {
}
}
function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath}: { jsFilePath: string, sourceMapFilePath: string, declarationFilePath: string },
function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath }: EmitFileNames,
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;
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) {
@ -8379,9 +8381,11 @@ const _super = (function (geti, seti) {
}
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 {
@ -2234,7 +2232,8 @@ namespace ts {
export function forEachExpectedEmitFile(host: EmitHost,
action: (emitFileNames: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean) => void,
targetSourceFile?: SourceFile) {
targetSourceFile?: SourceFile,
emitOnlyDtsFiles?: boolean) {
const options = host.getCompilerOptions();
// Emit on each source file
if (options.outFile || options.out) {
@ -2267,10 +2266,11 @@ 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);
}

View file

@ -1,4 +1,4 @@
/// <reference path="..\harness.ts" />
/// <reference path="..\harness.ts" />
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
namespace ts {
@ -415,7 +415,7 @@ namespace ts {
setImmediate(callback: TimeOutCallback, time: number, ...args: any[]) {
return this.immediateCallbacks.register(callback, args);
};
}
clearImmediate(timeoutId: any): void {
this.immediateCallbacks.unregister(timeoutId);
@ -454,6 +454,23 @@ namespace ts {
readonly exit = () => notImplemented();
}
function makeSessionRequest<T>(command: string, args: T) {
const newRequest: server.protocol.Request = {
seq: 0,
type: "request",
command,
arguments: args
};
return newRequest;
}
function openFilesForSession(files: FileOrFolder[], session: server.Session) {
for (const file of files) {
const request = makeSessionRequest<server.protocol.OpenRequestArgs>(server.CommandNames.Open, { file: file.path });
session.executeCommand(request);
}
}
describe("tsserver-project-system", () => {
const commonFile1: FileOrFolder = {
path: "/a/b/commonFile1.ts",
@ -801,7 +818,7 @@ namespace ts {
content: `{
"compilerOptions": {
"target": "es6"
},
},
"files": [ "main.ts" ]
}`
};
@ -844,7 +861,7 @@ namespace ts {
content: `{
"compilerOptions": {
"target": "es6"
},
},
"files": [ "main.ts" ]
}`
};
@ -1483,6 +1500,381 @@ namespace ts {
});
});
describe("CompileOnSave affected list", () => {
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: FileOrFolder[]) {
const actualResult = session.executeCommand(request).response;
const expectedFileNameList = expectedFileList.length > 0 ? ts.map(expectedFileList, f => f.path).sort() : [];
const actualFileNameList = actualResult.sort();
assert.isTrue(arrayIsEqualTo(actualFileNameList, expectedFileNameList), `Actual result is ${actualFileNameList}, while expected ${expectedFileNameList}`);
}
describe("for configured projects", () => {
let moduleFile1: FileOrFolder;
let file1Consumer1: FileOrFolder;
let file1Consumer2: FileOrFolder;
let moduleFile2: FileOrFolder;
let globalFile3: FileOrFolder;
let configFile: FileOrFolder;
let changeModuleFile1ShapeRequest1: server.protocol.Request;
let changeModuleFile1InternalRequest1: server.protocol.Request;
let changeModuleFile1ShapeRequest2: server.protocol.Request;
// A compile on save affected file request using file1
let moduleFile1FileListRequest: server.protocol.Request;
let host: TestServerHost;
let typingsInstaller: server.ITypingsInstaller;
let session: server.Session;
beforeEach(() => {
moduleFile1 = {
path: "/a/b/moduleFile1.ts",
content: "export function Foo() { };"
};
file1Consumer1 = {
path: "/a/b/file1Consumer1.ts",
content: `import {Foo} from "./moduleFile1"; export var y = 10;`
};
file1Consumer2 = {
path: "/a/b/file1Consumer2.ts",
content: `import {Foo} from "./moduleFile1"; let z = 10;`
};
moduleFile2 = {
path: "/a/b/moduleFile2.ts",
content: `export var Foo4 = 10;`
};
globalFile3 = {
path: "/a/b/globalFile3.ts",
content: `interface GlobalFoo { age: number }`
};
configFile = {
path: "/a/b/tsconfig.json",
content: `{
"compileOnSave": true
}`
};
// Change the content of file1 to `export var T: number;export function Foo() { };`
changeModuleFile1ShapeRequest1 = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `export var T: number;`
});
// Change the content of file1 to `export var T: number;export function Foo() { };`
changeModuleFile1InternalRequest1 = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `var T1: number;`
});
// Change the content of file1 to `export var T: number;export function Foo() { };`
changeModuleFile1ShapeRequest2 = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `export var T2: number;`
});
moduleFile1FileListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(server.CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path });
host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
});
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
openFilesForSession([moduleFile1, file1Consumer1], session);
// Send an initial compileOnSave request
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
session.executeCommand(changeModuleFile1ShapeRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
// Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };`
const changeFile1InternalRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 46,
endLine: 1,
endOffset: 46,
insertString: `console.log('hi');`
});
session.executeCommand(changeFile1InternalRequest);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
});
it("should be up-to-date with the reference map changes", () => {
openFilesForSession([moduleFile1, file1Consumer1], session);
// Send an initial compileOnSave request
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
// Change file2 content to `let y = Foo();`
const removeFile1Consumer1ImportRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: file1Consumer1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 28,
insertString: ""
});
session.executeCommand(removeFile1Consumer1ImportRequest);
session.executeCommand(changeModuleFile1ShapeRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer2]);
// Add the import statements back to file2
const addFile2ImportRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: file1Consumer1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `import {Foo} from "./moduleFile1";`
});
session.executeCommand(addFile2ImportRequest);
// Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };`
const changeModuleFile1ShapeRequest2 = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `export var T2: string;`
});
session.executeCommand(changeModuleFile1ShapeRequest2);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
});
it("should be up-to-date with changes made in non-open files", () => {
openFilesForSession([moduleFile1], session);
// Send an initial compileOnSave request
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
file1Consumer1.content = `let y = 10;`;
host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false);
session.executeCommand(changeModuleFile1ShapeRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer2]);
});
it("should be up-to-date with deleted files", () => {
openFilesForSession([moduleFile1], session);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
session.executeCommand(changeModuleFile1ShapeRequest1);
// Delete file1Consumer2
host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]);
host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
});
it("should be up-to-date with newly created files", () => {
openFilesForSession([moduleFile1], session);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
const file1Consumer3: FileOrFolder = {
path: "/a/b/file1Consumer3.ts",
content: `import {Foo} from "./moduleFile1"; let y = Foo();`
};
host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]);
host.triggerDirectoryWatcherCallback(ts.getDirectoryPath(file1Consumer3.path), file1Consumer3.path);
host.runQueuedTimeoutCallbacks();
session.executeCommand(changeModuleFile1ShapeRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3]);
});
it("should detect changes in non-root files", () => {
moduleFile1 = {
path: "/a/b/moduleFile1.ts",
content: "export function Foo() { };"
};
file1Consumer1 = {
path: "/a/b/file1Consumer1.ts",
content: `import {Foo} from "./moduleFile1"; let y = Foo();`
};
configFile = {
path: "/a/b/tsconfig.json",
content: `{
"compileOnSave": true,
"files": ["${file1Consumer1.path}"]
}`
};
host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([moduleFile1, file1Consumer1], session);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
// change file1 shape now, and verify both files are affected
session.executeCommand(changeModuleFile1ShapeRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
// change file1 internal, and verify only file1 is affected
session.executeCommand(changeModuleFile1InternalRequest1);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
});
it("should return all files if a global file changed shape", () => {
openFilesForSession([globalFile3], session);
const changeGlobalFile3ShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: globalFile3.path,
line: 1,
offset: 1,
endLine: 1,
endOffset: 1,
insertString: `var T2: string;`
});
// check after file1 shape changes
session.executeCommand(changeGlobalFile3ShapeRequest);
const globalFile3FileListRequest = makeSessionRequest<server.protocol.FileRequestArgs>(server.CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path });
sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2]);
});
it("should return empty array if CompileOnSave is not enabled", () => {
configFile = {
path: "/a/b/tsconfig.json",
content: `{}`
};
host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([moduleFile1], session);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []);
});
it("should always return the file itself if '--isolatedModules' is specified", () => {
configFile = {
path: "/a/b/tsconfig.json",
content: `{
"compileOnSave": true,
"compilerOptions": {
"isolatedModules": true
}
}`
};
host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([moduleFile1], session);
const file1ChangeShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 27,
endLine: 1,
endOffset: 27,
insertString: `Point,`
});
session.executeCommand(file1ChangeShapeRequest);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
});
it("should always return the file itself if '--out' or '--outFile' is specified", () => {
configFile = {
path: "/a/b/tsconfig.json",
content: `{
"compileOnSave": true,
"compilerOptions": {
"module": "system",
"outFile": "/a/b/out.js"
}
}`
};
host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([moduleFile1], session);
const file1ChangeShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: moduleFile1.path,
line: 1,
offset: 27,
endLine: 1,
endOffset: 27,
insertString: `Point,`
});
session.executeCommand(file1ChangeShapeRequest);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
});
it("should return cascaded affected file list", () => {
const file1Consumer1Consumer1: FileOrFolder = {
path: "/a/b/file1Consumer1Consumer1.ts",
content: `import {y} from "./file1Consumer1";`
};
host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]);
typingsInstaller = new TestTypingsInstaller("/a/data/", host);
session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([moduleFile1, file1Consumer1], session);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer1Consumer1]);
const changeFile1Consumer1ShapeRequest = makeSessionRequest<server.protocol.ChangeRequestArgs>(server.CommandNames.Change, {
file: file1Consumer1.path,
line: 2,
offset: 1,
endLine: 2,
endOffset: 1,
insertString: `export var T: number;`
});
session.executeCommand(changeModuleFile1ShapeRequest1);
session.executeCommand(changeFile1Consumer1ShapeRequest);
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer1Consumer1]);
});
});
});
describe("EmitFile test", () => {
it("should emit specified file", () => {
const file1 = {
path: "/a/b/f1.ts",
content: `export function Foo() { return 10; }`
};
const file2 = {
path: "/a/b/f2.ts",
content: `import {Foo} from "./f1"; let y = Foo();`
};
const config = {
path: "/a/b/tsconfig.json",
content: `{}`
};
const host = createServerHost([file1, file2, config, libFile]);
const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
openFilesForSession([file1, file2], session);
const compileFileRequest = makeSessionRequest<server.protocol.CompileOnSaveEmitFileRequestArgs>(server.CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: config.path });
session.executeCommand(compileFileRequest);
const expectedEmittedFileName = "/a/b/f1.js";
assert.isTrue(host.fileExists(expectedEmittedFileName));
assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`);
});
});
describe("typings installer", () => {
it("configured projects (tsd installed) 1", () => {
const file1 = {

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.getFileInfo(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

@ -681,7 +681,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 };
}
@ -704,14 +705,15 @@ namespace ts.server {
return false;
}
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], compilerOptions: CompilerOptions, typingOptions: TypingOptions) {
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typingOptions: TypingOptions) {
const project = new ExternalProject(
projectFileName,
this,
this.documentRegistry,
compilerOptions,
options,
typingOptions,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files, externalFilePropertyReader));
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(options, files, externalFilePropertyReader),
!!options.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined);
this.externalProjects.push(project);
@ -728,7 +730,8 @@ namespace ts.server {
projectOptions.compilerOptions,
projectOptions.typingOptions,
projectOptions.wildcardDirectories,
/*languageServiceEnabled*/ !sizeLimitExceeded);
/*languageServiceEnabled*/ !sizeLimitExceeded,
/*compileOnSaveEnabled*/ !!projectOptions.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName);
@ -775,7 +778,7 @@ namespace ts.server {
return { success: true, project, errors };
}
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypingOptions: TypingOptions) {
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypingOptions: TypingOptions, compileOnSave: boolean) {
const oldRootScriptInfos = project.getRootScriptInfos();
const newRootScriptInfos: ScriptInfo[] = [];
const newRootScriptInfoMap: NormalizedPathMap<ScriptInfo> = createNormalizedPathMap<ScriptInfo>();
@ -836,6 +839,7 @@ namespace ts.server {
project.setCompilerOptions(newOptions);
(<ExternalProject | ConfiguredProject>project).setTypingOptions(newTypingOptions);
project.compileOnSaveEnabled = !!compileOnSave;
project.updateGraph();
}
@ -865,7 +869,7 @@ namespace ts.server {
project.enableLanguageService();
}
this.watchConfigDirectoryForProject(project, projectOptions);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions, projectOptions.compileOnSave);
}
}
@ -1135,7 +1139,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, proj.typingOptions);
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave);
return;
}

File diff suppressed because it is too large Load diff

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 {
@ -32,6 +33,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.
*/
@ -61,7 +63,8 @@ namespace ts.server {
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();
@ -79,6 +82,8 @@ namespace ts.server {
else {
this.disableLanguageService();
}
this.builder = createBuilder(this);
this.markAsDirty();
}
@ -89,6 +94,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();
}
@ -111,6 +124,13 @@ 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) {
// if we have a program - release all files that are enlisted in program
@ -164,6 +184,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 [];
@ -184,6 +215,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);
}
@ -276,6 +315,7 @@ namespace ts.server {
}
}
}
this.builder.onProjectUpdateGraph();
return hasChanges;
}
@ -377,6 +417,59 @@ namespace ts.server {
}
}
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)) {
@ -404,7 +497,8 @@ namespace ts.server {
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
compilerOptions);
compilerOptions,
/*compileOnSaveEnabled*/ false);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
@ -445,8 +539,9 @@ namespace ts.server {
compilerOptions: CompilerOptions,
private typingOptions: TypingOptions,
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions);
languageServiceEnabled: boolean,
public compileOnSaveEnabled = false) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
setTypingOptions(newTypingOptions: TypingOptions): void {
@ -533,8 +628,9 @@ namespace ts.server {
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
typingOptions: TypingOptions,
languageServiceEnabled: boolean) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions);
languageServiceEnabled: boolean,
public compileOnSaveEnabled = true) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
this.setTypingOptions(typingOptions);
}

681
src/server/project.ts.orig Normal file
View file

@ -0,0 +1,681 @@
/// <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 {
export enum ProjectKind {
Inferred,
Configured,
External
}
function remove<T>(items: T[], item: T) {
const index = items.indexOf(item);
if (index >= 0) {
items.splice(index, 1);
}
}
const jsOrDts = [".js", ".d.ts"];
export function allFilesAreJsOrDts(project: Project): boolean {
return project.getFileNames().every(f => fileExtensionIsAny(f, jsOrDts));
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
private program: ts.Program;
private languageService: LanguageService;
builder: Builder;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
*/
private lastReportedFileNames: Map<string>;
/**
* Last version that was reported.
*/
private lastReportedVersion = 0;
/**
* Current project structure version.
* This property is changed in 'updateGraph' based on the set of files in program
*/
private projectStructureVersion = 0;
/**
* Current version of the project state. It is changed when:
* - new root file was added/removed
* - edit happen in some file that is currently included in the project.
* This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project
*/
private projectStateVersion = 0;
private typingFiles: TypingsArray;
constructor(
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean) {
if (!this.compilerOptions) {
this.compilerOptions = ts.getDefaultCompilerOptions();
this.compilerOptions.allowNonTsExtensions = true;
this.compilerOptions.allowJs = true;
}
else if (hasExplicitListOfFiles) {
// If files are listed explicitly, allow all extensions
this.compilerOptions.allowNonTsExtensions = true;
}
if (languageServiceEnabled) {
this.enableLanguageService();
}
else {
this.disableLanguageService();
}
this.builder = createBuilder(this);
this.markAsDirty();
}
getLanguageService(ensureSynchronized = true): LanguageService {
if (ensureSynchronized) {
this.updateGraph();
}
return this.languageService;
}
getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
if (!this.languageServiceEnabled) {
return [];
}
this.updateGraph();
return this.builder.getFilesAffectedBy(scriptInfo);
}
getProjectVersion() {
return this.projectStateVersion.toString();
}
enableLanguageService() {
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
this.lsHost = lsHost;
this.languageServiceEnabled = true;
}
disableLanguageService() {
this.languageService = nullLanguageService;
this.lsHost = nullLanguageServiceHost;
this.languageServiceEnabled = false;
}
abstract getProjectName(): string;
abstract getTypingOptions(): TypingOptions;
<<<<<<< HEAD
getSourceFile(path: Path) {
if (!this.program) {
return undefined;
}
return this.program.getSourceFileByPath(path);
}
=======
>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
close() {
if (this.program) {
// if we have a program - release all files that are enlisted in program
for (const f of this.program.getSourceFiles()) {
const info = this.projectService.getScriptInfo(f.fileName);
info.detachFromProject(this);
}
}
else {
// release all root files
for (const root of this.rootFiles) {
root.detachFromProject(this);
}
}
this.rootFiles = undefined;
this.rootFilesMap = undefined;
this.program = undefined;
// signal language service to release source files acquired from document registry
this.languageService.dispose();
}
getCompilerOptions() {
return this.compilerOptions;
}
hasRoots() {
return this.rootFiles && this.rootFiles.length > 0;
}
getRootFiles() {
return this.rootFiles && this.rootFiles.map(info => info.fileName);
}
getRootFilesLSHost() {
const result: string[] = [];
if (this.rootFiles) {
for (const f of this.rootFiles) {
result.push(f.fileName);
}
if (this.typingFiles) {
for (const f of this.typingFiles) {
result.push(f);
}
}
}
return result;
}
getRootScriptInfos() {
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 [];
}
if (!this.languageServiceEnabled) {
// if language service is disabled assume that all files in program are root files + default library
let rootFiles = this.getRootFiles();
if (this.compilerOptions) {
const defaultLibrary = getDefaultLibFilePath(this.compilerOptions);
if (defaultLibrary) {
(rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary));
}
}
return rootFiles;
}
const sourceFiles = this.program.getSourceFiles();
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);
}
containsFile(filename: NormalizedPath, requireOpen?: boolean) {
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
if (info && (info.isOpen || !requireOpen)) {
return this.containsScriptInfo(info);
}
}
isRoot(info: ScriptInfo) {
return this.rootFilesMap && this.rootFilesMap.contains(info.path);
}
// add a root file to project
addRoot(info: ScriptInfo) {
if (!this.isRoot(info)) {
this.rootFiles.push(info);
this.rootFilesMap.set(info.path, info);
info.attachToProject(this);
this.markAsDirty();
}
}
removeFile(info: ScriptInfo, detachFromProject = true) {
this.removeRootFileIfNecessary(info);
this.lsHost.notifyFileRemoved(info);
if (detachFromProject) {
info.detachFromProject(this);
}
this.markAsDirty();
}
markAsDirty() {
this.projectStateVersion++;
}
/**
* Updates set of files that contribute to this project
* @returns: true if set of files in the project stays the same and false - otherwise.
*/
updateGraph(): boolean {
if (!this.languageServiceEnabled) {
return true;
}
let hasChanges = this.updateGraphWorker();
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this);
if (this.setTypings(cachedTypings)) {
hasChanges = this.updateGraphWorker() || hasChanges;
}
if (hasChanges) {
this.projectStructureVersion++;
}
return !hasChanges;
}
private setTypings(typings: TypingsArray): boolean {
if (arrayIsEqualTo(this.typingFiles, typings)) {
return false;
}
this.typingFiles = typings;
this.markAsDirty();
return true;
}
private updateGraphWorker() {
const oldProgram = this.program;
this.program = this.languageService.getProgram();
let hasChanges = false;
// bump up the version if
// - oldProgram is not set - this is a first time updateGraph is called
// - newProgram is different from the old program and structure of the old program was not reused.
if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) {
hasChanges = true;
if (oldProgram) {
for (const f of oldProgram.getSourceFiles()) {
if (this.program.getSourceFileByPath(f.path)) {
continue;
}
// new program does not contain this file - detach it from the project
const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
if (scriptInfoToDetach) {
scriptInfoToDetach.detachFromProject(this);
}
}
}
}
this.builder.onProjectUpdateGraph();
return hasChanges;
}
getScriptInfoLSHost(fileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
if (scriptInfo) {
scriptInfo.attachToProject(this);
}
return scriptInfo;
}
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
Debug.assert(!scriptInfo || scriptInfo.isAttached(this));
return scriptInfo;
}
getScriptInfo(uncheckedFileName: string) {
return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
}
filesToString() {
if (!this.program) {
return "";
}
let strBuilder = "";
for (const file of this.program.getSourceFiles()) {
strBuilder += `${file.fileName}\n`;
}
return strBuilder;
}
setCompilerOptions(compilerOptions: CompilerOptions) {
if (compilerOptions) {
if (this.projectKind === ProjectKind.Inferred) {
compilerOptions.allowJs = true;
}
compilerOptions.allowNonTsExtensions = true;
this.compilerOptions = compilerOptions;
this.lsHost.setCompilationSettings(compilerOptions);
this.markAsDirty();
}
}
reloadScript(filename: NormalizedPath): boolean {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
if (script) {
Debug.assert(script.isAttached(this));
script.reloadFromFile();
return true;
}
return false;
}
getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles {
this.updateGraph();
const info = {
projectName: this.getProjectName(),
version: this.projectStructureVersion,
isInferred: this.projectKind === ProjectKind.Inferred,
options: this.getCompilerOptions()
};
// check if requested version is the same that we have reported last time
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 };
}
// compute and return the difference
const lastReportedFileNames = this.lastReportedFileNames;
const currentFiles = arrayToMap(this.getFileNames(), x => x);
const added: string[] = [];
const removed: string[] = [];
for (const id in currentFiles) {
if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) {
added.push(id);
}
}
for (const id in lastReportedFileNames) {
if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) {
removed.push(id);
}
}
this.lastReportedFileNames = currentFiles;
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.projectStructureVersion;
return { info, changes: { added, removed } };
}
else {
// unknown version - return everything
const projectFileNames = this.getFileNames();
this.lastReportedFileNames = arrayToMap(projectFileNames, x => x);
this.lastReportedVersion = this.projectStructureVersion;
return { info, files: projectFileNames };
}
}
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)) {
remove(this.rootFiles, info);
this.rootFilesMap.remove(info.path);
}
}
}
export class InferredProject extends Project {
private static NextId = 1;
/**
* Unique name that identifies this particular inferred project
*/
private readonly inferredProjectName: string;
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions) {
super(ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
compilerOptions,
/*compileOnSaveEnabled*/ false);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
}
getProjectName() {
return this.inferredProjectName;
}
close() {
super.close();
for (const directory of this.directoriesWatchedForTsconfig) {
this.projectService.stopWatchingDirectory(directory);
}
}
getTypingOptions(): TypingOptions {
return {
enableAutoDiscovery: allFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
}
export class ConfiguredProject extends Project {
private projectFileWatcher: FileWatcher;
private directoryWatcher: FileWatcher;
private directoriesWatchedForWildcards: Map<FileWatcher>;
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(readonly configFileName: NormalizedPath,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
compilerOptions: CompilerOptions,
private typingOptions: TypingOptions,
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean,
public compileOnSaveEnabled = false) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
setTypingOptions(newTypingOptions: TypingOptions): void {
this.typingOptions = newTypingOptions;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
this.typingOptions = newTypingOptions;
}
getTypingOptions() {
return this.typingOptions;
}
getProjectName() {
return this.configFileName;
}
watchConfigFile(callback: (project: ConfiguredProject) => void) {
this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this));
}
watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) {
if (this.directoryWatcher) {
return;
}
const directoryToWatch = getDirectoryPath(this.configFileName);
this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`);
this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true);
}
watchWildcards(callback: (project: ConfiguredProject, path: string) => void) {
if (!this.wildcardDirectories) {
return;
}
const configDirectoryPath = getDirectoryPath(this.configFileName);
this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => {
if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
this.projectService.logger.info(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`);
watchers[directory] = this.projectService.host.watchDirectory(
directory,
path => callback(this, path),
recursive
);
}
return watchers;
}, <Map<FileWatcher>>{});
}
stopWatchingDirectory() {
if (this.directoryWatcher) {
this.directoryWatcher.close();
this.directoryWatcher = undefined;
}
}
close() {
super.close();
if (this.projectFileWatcher) {
this.projectFileWatcher.close();
}
for (const id in this.directoriesWatchedForWildcards) {
this.directoriesWatchedForWildcards[id].close();
}
this.directoriesWatchedForWildcards = undefined;
this.stopWatchingDirectory();
}
addOpenRef() {
this.openRefCount++;
}
deleteOpenRef() {
this.openRefCount--;
return this.openRefCount;
}
}
export class ExternalProject extends Project {
private typingOptions: TypingOptions;
constructor(readonly externalProjectName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
typingOptions: TypingOptions,
<<<<<<< HEAD
languageServiceEnabled: boolean,
public compileOnSaveEnabled = true) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
=======
languageServiceEnabled: boolean) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions);
>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
this.setTypingOptions(typingOptions);
}
getTypingOptions() {
return this.typingOptions;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
if (!newTypingOptions) {
// set default typings options
newTypingOptions = {
enableAutoDiscovery: allFilesAreJsOrDts(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 = allFilesAreJsOrDts(this);
}
if (!newTypingOptions.include) {
newTypingOptions.include = [];
}
if (!newTypingOptions.exclude) {
newTypingOptions.exclude = [];
}
}
this.typingOptions = newTypingOptions;
}
getProjectName() {
return this.externalProjectName;
}
}
}

View file

@ -498,10 +498,18 @@ 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 {
projectName: string;
isInferred: boolean;
@ -721,6 +729,17 @@ declare namespace ts.server.protocol {
export interface CloseRequest extends FileRequest {
}
export interface CompileOnSaveAffectedFileListRequest extends FileRequest {
}
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

File diff suppressed because it is too large Load diff

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";
@ -939,6 +941,26 @@ namespace ts.server {
}, []);
}
private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs) {
const info = this.projectService.getScriptInfo(args.file);
let result: string[] = [];
for (const project of info.containingProjects) {
if (project.compileOnSaveEnabled) {
result = concatenate(result, 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);
@ -1340,6 +1362,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

@ -1,4 +1,4 @@
/// <reference path="types.d.ts" />
/// <reference path="types.d.ts" />
namespace ts.server {
export enum LogLevel {
@ -220,6 +220,7 @@ namespace ts.server {
wildcardDirectories?: Map<WatchDirectoryFlags>;
compilerOptions?: CompilerOptions;
typingOptions?: TypingOptions;
compileOnSave?: boolean;
}
export function isInferredProjectName(name: string) {

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