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:
parent
d736db3b01
commit
a082857ae8
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/// <reference path="utilities.ts"/>
|
||||
/// <reference path="utilities.ts"/>
|
||||
/// <reference path="scanner.ts"/>
|
||||
|
||||
namespace ts {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
368
src/server/builder.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/// <reference types="node"/>
|
||||
/// <reference types="node" />
|
||||
|
||||
|
||||
// TODO: extract services types
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
1203
src/server/editorServices.ts.orig
Normal file
1203
src/server/editorServices.ts.orig
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
681
src/server/project.ts.orig
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
21
src/server/protocol.d.ts
vendored
21
src/server/protocol.d.ts
vendored
|
@ -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
|
||||
|
|
1488
src/server/protocol.d.ts.orig
Normal file
1488
src/server/protocol.d.ts.orig
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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));
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue