mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Merge pull request #17820 from Microsoft/tsserverEventChangedFiles
Event to notify project changes
This commit is contained in:
+124
-96
@@ -18,6 +18,13 @@ namespace ts {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ChangedProgramFiles {
|
||||
/** Minimal set of list of files that require emit */
|
||||
readonly filesToEmit: ReadonlyArray<string>;
|
||||
/** File paths of source files changed/added/removed or affected by changed files */
|
||||
readonly changedFiles: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface Builder {
|
||||
/**
|
||||
* This is the callback when file infos in the builder are updated
|
||||
@@ -25,8 +32,15 @@ namespace ts {
|
||||
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
|
||||
getFilesAffectedBy(program: Program, path: Path): string[];
|
||||
emitFile(program: Program, path: Path): EmitOutput;
|
||||
|
||||
/** Emit the changed files and clear the cache of the changed files */
|
||||
emitChangedFiles(program: Program): EmitOutputDetailed[];
|
||||
/** Get the changed files since last query and then clear the cache of changed files */
|
||||
getChangedProgramFiles(program: Program): ChangedProgramFiles;
|
||||
/** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */
|
||||
getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[];
|
||||
|
||||
/** Called to reset the status of the builder */
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
@@ -73,16 +87,18 @@ namespace ts {
|
||||
): Builder {
|
||||
let isModuleEmit: boolean | undefined;
|
||||
// Last checked shape signature for the file info
|
||||
type FileInfo = { version: string; signature: string; };
|
||||
let fileInfos: Map<FileInfo>;
|
||||
type FileInfo = { fileName: string; version: string; signature: string; };
|
||||
const fileInfos = createMap<FileInfo>();
|
||||
const semanticDiagnosticsPerFile = createMap<Diagnostic[]>();
|
||||
let changedFilesSinceLastEmit: Map<true>;
|
||||
/** The map has key by source file's path that has been changed */
|
||||
const changedFileNames = createMap<string>();
|
||||
let emitHandler: EmitHandler;
|
||||
return {
|
||||
onProgramUpdateGraph,
|
||||
getFilesAffectedBy,
|
||||
emitFile,
|
||||
emitChangedFiles,
|
||||
getChangedProgramFiles,
|
||||
getSemanticDiagnostics,
|
||||
clear
|
||||
};
|
||||
@@ -92,13 +108,11 @@ namespace ts {
|
||||
if (isModuleEmit !== currentIsModuleEmit) {
|
||||
isModuleEmit = currentIsModuleEmit;
|
||||
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
|
||||
fileInfos = undefined;
|
||||
fileInfos.clear();
|
||||
semanticDiagnosticsPerFile.clear();
|
||||
}
|
||||
|
||||
changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap<true>();
|
||||
mutateMap(
|
||||
fileInfos || (fileInfos = createMap()),
|
||||
fileInfos,
|
||||
arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
|
||||
{
|
||||
// Add new file info
|
||||
@@ -111,27 +125,26 @@ namespace ts {
|
||||
);
|
||||
}
|
||||
|
||||
function registerChangedFile(path: Path) {
|
||||
changedFilesSinceLastEmit.set(path, true);
|
||||
function registerChangedFile(path: Path, fileName: string) {
|
||||
changedFileNames.set(path, fileName);
|
||||
// All changed files need to re-evaluate its semantic diagnostics
|
||||
semanticDiagnosticsPerFile.delete(path);
|
||||
}
|
||||
|
||||
function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo {
|
||||
registerChangedFile(sourceFile.path);
|
||||
registerChangedFile(sourceFile.path, sourceFile.fileName);
|
||||
emitHandler.addScriptInfo(program, sourceFile);
|
||||
return { version: sourceFile.version, signature: undefined };
|
||||
return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined };
|
||||
}
|
||||
|
||||
function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) {
|
||||
registerChangedFile(path);
|
||||
function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) {
|
||||
registerChangedFile(path, existingFileInfo.fileName);
|
||||
emitHandler.removeScriptInfo(path);
|
||||
}
|
||||
|
||||
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
|
||||
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
|
||||
registerChangedFile(sourceFile.path);
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
registerChangedFile(sourceFile.path, sourceFile.fileName);
|
||||
existingInfo.version = sourceFile.version;
|
||||
emitHandler.updateScriptInfo(program, sourceFile);
|
||||
}
|
||||
@@ -154,7 +167,7 @@ namespace ts {
|
||||
|
||||
const sourceFile = program.getSourceFile(path);
|
||||
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
|
||||
const info = fileInfos && fileInfos.get(path);
|
||||
const info = fileInfos.get(path);
|
||||
if (!info || !updateShapeSignature(program, sourceFile, info)) {
|
||||
return singleFileResult;
|
||||
}
|
||||
@@ -165,44 +178,61 @@ namespace ts {
|
||||
|
||||
function emitFile(program: Program, path: Path) {
|
||||
ensureProgramGraph(program);
|
||||
if (!fileInfos || !fileInfos.has(path)) {
|
||||
if (!fileInfos.has(path)) {
|
||||
return { outputFiles: [], emitSkipped: true };
|
||||
}
|
||||
|
||||
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false);
|
||||
}
|
||||
|
||||
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
|
||||
ensureProgramGraph(program);
|
||||
const result: EmitOutputDetailed[] = [];
|
||||
if (changedFilesSinceLastEmit) {
|
||||
const seenFiles = createMap<SourceFile>();
|
||||
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
|
||||
const affectedFiles = getFilesAffectedBy(program, path);
|
||||
for (const file of affectedFiles) {
|
||||
if (!seenFiles.has(file)) {
|
||||
const sourceFile = program.getSourceFile(file);
|
||||
seenFiles.set(file, sourceFile);
|
||||
if (sourceFile) {
|
||||
// Any affected file shouldnt have the cached diagnostics
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
function enumerateChangedFilesSet(
|
||||
program: Program,
|
||||
onChangedFile: (fileName: string, path: Path) => void,
|
||||
onAffectedFile: (fileName: string, sourceFile: SourceFile) => void
|
||||
) {
|
||||
changedFileNames.forEach((fileName, path) => {
|
||||
onChangedFile(fileName, path as Path);
|
||||
const affectedFiles = getFilesAffectedBy(program, path as Path);
|
||||
for (const file of affectedFiles) {
|
||||
onAffectedFile(file, program.getSourceFile(file));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed;
|
||||
result.push(emitOutput);
|
||||
function enumerateChangedFilesEmitOutput(
|
||||
program: Program,
|
||||
emitOnlyDtsFiles: boolean,
|
||||
onChangedFile: (fileName: string, path: Path) => void,
|
||||
onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void
|
||||
) {
|
||||
const seenFiles = createMap<SourceFile>();
|
||||
enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => {
|
||||
if (!seenFiles.has(fileName)) {
|
||||
seenFiles.set(fileName, sourceFile);
|
||||
if (sourceFile) {
|
||||
// Any affected file shouldnt have the cached diagnostics
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
|
||||
// mark all the emitted source files as seen
|
||||
if (emitOutput.emittedSourceFiles) {
|
||||
for (const file of emitOutput.emittedSourceFiles) {
|
||||
seenFiles.set(file.fileName, file);
|
||||
}
|
||||
}
|
||||
const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed;
|
||||
onEmitOutput(emitOutput, sourceFile);
|
||||
|
||||
// mark all the emitted source files as seen
|
||||
if (emitOutput.emittedSourceFiles) {
|
||||
for (const file of emitOutput.emittedSourceFiles) {
|
||||
seenFiles.set(file.fileName, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
changedFilesSinceLastEmit = undefined;
|
||||
}
|
||||
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
|
||||
ensureProgramGraph(program);
|
||||
const result: EmitOutputDetailed[] = [];
|
||||
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false,
|
||||
/*onChangedFile*/ noop, emitOutput => result.push(emitOutput));
|
||||
changedFileNames.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -210,17 +240,11 @@ namespace ts {
|
||||
ensureProgramGraph(program);
|
||||
|
||||
// Ensure that changed files have cleared their respective
|
||||
if (changedFilesSinceLastEmit) {
|
||||
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
|
||||
const affectedFiles = getFilesAffectedBy(program, path);
|
||||
for (const file of affectedFiles) {
|
||||
const sourceFile = program.getSourceFile(file);
|
||||
if (sourceFile) {
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => {
|
||||
if (sourceFile) {
|
||||
semanticDiagnosticsPerFile.delete(sourceFile.path);
|
||||
}
|
||||
});
|
||||
|
||||
let diagnostics: Diagnostic[];
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
@@ -240,11 +264,36 @@ namespace ts {
|
||||
return diagnostics || emptyArray;
|
||||
}
|
||||
|
||||
function getChangedProgramFiles(program: Program): ChangedProgramFiles {
|
||||
ensureProgramGraph(program);
|
||||
|
||||
let filesToEmit: string[];
|
||||
const changedFiles = createMap<string>();
|
||||
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true,
|
||||
// All the changed files are required to get diagnostics
|
||||
(changedFileName, changedFilePath) => addFileForDiagnostics(changedFileName, changedFilePath),
|
||||
// Emitted file is for emit as well as diagnostic
|
||||
(_emitOutput, sourceFile) => {
|
||||
(filesToEmit || (filesToEmit = [])).push(sourceFile.fileName);
|
||||
addFileForDiagnostics(sourceFile.fileName, sourceFile.path);
|
||||
});
|
||||
changedFileNames.clear();
|
||||
return {
|
||||
filesToEmit: filesToEmit || emptyArray,
|
||||
changedFiles: arrayFrom(changedFiles.values())
|
||||
};
|
||||
|
||||
function addFileForDiagnostics(fileName: string, path: Path) {
|
||||
changedFiles.set(path, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
isModuleEmit = undefined;
|
||||
emitHandler = undefined;
|
||||
fileInfos = undefined;
|
||||
fileInfos.clear();
|
||||
semanticDiagnosticsPerFile.clear();
|
||||
changedFileNames.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,9 +336,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the referenced files for a file from the program
|
||||
* @param program
|
||||
* @param path
|
||||
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
|
||||
*/
|
||||
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
|
||||
const referencedFiles = createMap<true>();
|
||||
@@ -371,55 +418,36 @@ namespace ts {
|
||||
|
||||
function getModuleEmitHandler(): EmitHandler {
|
||||
const references = createMap<Map<true>>();
|
||||
const referencedBy = createMultiMap<Path>();
|
||||
return {
|
||||
addScriptInfo: (program, sourceFile) => {
|
||||
const refs = createMap<true>();
|
||||
references.set(sourceFile.path, refs);
|
||||
setReferences(program, sourceFile, refs);
|
||||
},
|
||||
addScriptInfo: setReferences,
|
||||
removeScriptInfo,
|
||||
updateScriptInfo: (program, sourceFile) => setReferences(program, sourceFile, references.get(sourceFile.path)),
|
||||
updateScriptInfo: setReferences,
|
||||
getFilesAffectedByUpdatedShape
|
||||
};
|
||||
|
||||
function setReferences(program: Program, sourceFile: SourceFile, existingReferences: Map<true>) {
|
||||
const path = sourceFile.path;
|
||||
mutateMap(
|
||||
// Existing references
|
||||
existingReferences,
|
||||
// Updated references
|
||||
getReferencedFiles(program, sourceFile),
|
||||
{
|
||||
// Creating new Reference: as sourceFile references file with path 'key'
|
||||
// in other words source file (path) is referenced by 'key'
|
||||
createNewValue: (key): true => { referencedBy.add(key, path); return true; },
|
||||
// Remove existing reference by entry: source file doesnt reference file 'key' any more
|
||||
// in other words source file (path) is not referenced by 'key'
|
||||
onDeleteValue: (key, _existingValue) => { referencedBy.remove(key, path); }
|
||||
}
|
||||
);
|
||||
function setReferences(program: Program, sourceFile: SourceFile) {
|
||||
references.set(sourceFile.path, getReferencedFiles(program, sourceFile));
|
||||
}
|
||||
|
||||
function removeScriptInfo(path: Path) {
|
||||
function removeScriptInfo(removedFilePath: Path) {
|
||||
// Remove existing references
|
||||
references.forEach((_value, key) => {
|
||||
referencedBy.remove(key, path);
|
||||
});
|
||||
references.delete(path);
|
||||
|
||||
// Delete the entry and add files referencing this file, as chagned files too
|
||||
const referencedByPaths = referencedBy.get(path);
|
||||
if (referencedByPaths) {
|
||||
for (const path of referencedByPaths) {
|
||||
registerChangedFile(path);
|
||||
references.forEach((referencesInFile, filePath) => {
|
||||
if (referencesInFile.has(removedFilePath)) {
|
||||
// add files referencing the removedFilePath, as changed files too
|
||||
const referencedByInfo = fileInfos.get(filePath);
|
||||
if (referencedByInfo) {
|
||||
registerChangedFile(filePath as Path, referencedByInfo.fileName);
|
||||
}
|
||||
}
|
||||
referencedBy.delete(path);
|
||||
}
|
||||
});
|
||||
// Delete the entry for the removed file path
|
||||
references.delete(removedFilePath);
|
||||
}
|
||||
|
||||
function getReferencedByPaths(path: Path) {
|
||||
return referencedBy.get(path) || [];
|
||||
function getReferencedByPaths(referencedFilePath: Path) {
|
||||
return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) =>
|
||||
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
|
||||
);
|
||||
}
|
||||
|
||||
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
|
||||
@@ -444,7 +472,7 @@ namespace ts {
|
||||
// Start with the paths this file was referenced by
|
||||
const path = sourceFile.path;
|
||||
setSeenFileName(path, sourceFile);
|
||||
const queue = getReferencedByPaths(path).slice();
|
||||
const queue = getReferencedByPaths(path);
|
||||
while (queue.length > 0) {
|
||||
const currentPath = queue.pop();
|
||||
if (!seenFileNamesMap.has(currentPath)) {
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace ts.projectSystem {
|
||||
this.events.push(event);
|
||||
}
|
||||
|
||||
checkEventCountOfType(eventType: "context" | "configFileDiag", expectedCount: number) {
|
||||
checkEventCountOfType(eventType: "configFileDiag", expectedCount: number) {
|
||||
const eventsOfType = filter(this.events, e => e.eventName === eventType);
|
||||
assert.equal(eventsOfType.length, expectedCount, `The actual event counts of type ${eventType} is ${eventsOfType.length}, while expected ${expectedCount}`);
|
||||
}
|
||||
@@ -2001,7 +2001,7 @@ namespace ts.projectSystem {
|
||||
const session = createSession(host, {
|
||||
canUseEvents: true,
|
||||
eventHandler: e => {
|
||||
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
|
||||
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectChangedEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
|
||||
return;
|
||||
}
|
||||
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);
|
||||
@@ -4467,4 +4467,537 @@ namespace ts.projectSystem {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectChangedEvent", () => {
|
||||
function verifyFiles(caption: string, actual: ReadonlyArray<string>, expected: ReadonlyArray<string>) {
|
||||
assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`);
|
||||
const seen = createMap<true>();
|
||||
forEach(actual, f => {
|
||||
assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`);
|
||||
seen.set(f, true);
|
||||
assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`);
|
||||
});
|
||||
}
|
||||
|
||||
function createVerifyInitialOpen(session: TestSession, verifyProjectChangedEventHandler: (events: server.ProjectChangedEvent[]) => void) {
|
||||
return (file: FileOrFolder) => {
|
||||
session.executeCommandSeq(<protocol.OpenRequest>{
|
||||
command: server.CommandNames.Open,
|
||||
arguments: {
|
||||
file: file.path
|
||||
}
|
||||
});
|
||||
verifyProjectChangedEventHandler([]);
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectChangeEventVerifier {
|
||||
session: TestSession;
|
||||
verifyProjectChangedEventHandler(events: server.ProjectChangedEvent[]): void;
|
||||
verifyInitialOpen(file: FileOrFolder): void;
|
||||
}
|
||||
|
||||
function verifyProjectChangedEvent(createSession: (host: TestServerHost) => ProjectChangeEventVerifier) {
|
||||
it("when adding new file", () => {
|
||||
const commonFile1: FileOrFolder = {
|
||||
path: "/a/b/file1.ts",
|
||||
content: "export var x = 10;"
|
||||
};
|
||||
const commonFile2: FileOrFolder = {
|
||||
path: "/a/b/file2.ts",
|
||||
content: "export var y = 10;"
|
||||
};
|
||||
const commonFile3: FileOrFolder = {
|
||||
path: "/a/b/file3.ts",
|
||||
content: "export var z = 10;"
|
||||
};
|
||||
const configFile: FileOrFolder = {
|
||||
path: "/a/b/tsconfig.json",
|
||||
content: `{}`
|
||||
};
|
||||
const host = createServerHost([commonFile1, libFile, configFile]);
|
||||
const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host, );
|
||||
const projectService = session.getProjectService();
|
||||
verifyInitialOpen(commonFile1);
|
||||
|
||||
host.reloadFS([commonFile1, libFile, configFile, commonFile2]);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
// Since this is first event
|
||||
const project = projectService.configuredProjects.get(configFile.path);
|
||||
verifyProjectChangedEventHandler([{
|
||||
eventName: server.ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
changedFiles: [libFile.path, commonFile1.path, commonFile2.path],
|
||||
filesToEmit: [commonFile1.path, commonFile2.path]
|
||||
}
|
||||
}]);
|
||||
|
||||
host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
verifyProjectChangedEventHandler([{
|
||||
eventName: server.ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
changedFiles: [commonFile3.path],
|
||||
filesToEmit: [commonFile3.path]
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
describe("with --out or --outFile setting", () => {
|
||||
function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) {
|
||||
const config: FileOrFolder = {
|
||||
path: "/a/tsconfig.json",
|
||||
content: JSON.stringify({
|
||||
compilerOptions
|
||||
})
|
||||
};
|
||||
|
||||
const f1: FileOrFolder = {
|
||||
path: "/a/a.ts",
|
||||
content: "export let x = 1"
|
||||
};
|
||||
const f2: FileOrFolder = {
|
||||
path: "/a/b.ts",
|
||||
content: "export let y = 1"
|
||||
};
|
||||
|
||||
const files = [f1, config, libFile];
|
||||
const host = createServerHost(files);
|
||||
const { session, verifyInitialOpen, verifyProjectChangedEventHandler } = createSession(host);
|
||||
const projectService = session.getProjectService();
|
||||
verifyInitialOpen(f1);
|
||||
|
||||
files.push(f2);
|
||||
host.reloadFS(files);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
|
||||
// Since this is first event
|
||||
const project = projectService.configuredProjects.get(config.path);
|
||||
verifyProjectChangedEventHandler([{
|
||||
eventName: server.ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
changedFiles: [libFile.path, f1.path, f2.path],
|
||||
filesToEmit: [f1.path, f2.path]
|
||||
}
|
||||
}]);
|
||||
|
||||
f2.content = "export let x = 11";
|
||||
host.reloadFS(files);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
verifyProjectChangedEventHandler([{
|
||||
eventName: server.ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
changedFiles: [f2.path],
|
||||
filesToEmit: [f2.path]
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
it("when both options are not set", () => {
|
||||
verifyEventWithOutSettings();
|
||||
});
|
||||
|
||||
it("when --out is set", () => {
|
||||
const outJs = "/a/out.js";
|
||||
verifyEventWithOutSettings({ out: outJs });
|
||||
});
|
||||
|
||||
it("when --outFile is set", () => {
|
||||
const outJs = "/a/out.js";
|
||||
verifyEventWithOutSettings({ outFile: outJs });
|
||||
});
|
||||
});
|
||||
|
||||
describe("with modules and configured project", () => {
|
||||
const file1Consumer1Path = "/a/b/file1Consumer1.ts";
|
||||
const moduleFile1Path = "/a/b/moduleFile1.ts";
|
||||
const configFilePath = "/a/b/tsconfig.json";
|
||||
type InitialStateParams = {
|
||||
/** custom config file options */
|
||||
configObj?: any;
|
||||
/** list of files emitted/changed on first update graph */
|
||||
firstCompilationEmitFiles?: string[];
|
||||
/** Additional files and folders to add */
|
||||
getAdditionalFileOrFolder?(): FileOrFolder[];
|
||||
/** initial list of files to reload in fs and first file in this list being the file to open */
|
||||
firstReloadFileList?: string[];
|
||||
};
|
||||
function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList, firstCompilationEmitFiles }: InitialStateParams = {}) {
|
||||
const moduleFile1: FileOrFolder = {
|
||||
path: moduleFile1Path,
|
||||
content: "export function Foo() { };",
|
||||
};
|
||||
|
||||
const file1Consumer1: FileOrFolder = {
|
||||
path: file1Consumer1Path,
|
||||
content: `import {Foo} from "./moduleFile1"; export var y = 10;`,
|
||||
};
|
||||
|
||||
const file1Consumer2: FileOrFolder = {
|
||||
path: "/a/b/file1Consumer2.ts",
|
||||
content: `import {Foo} from "./moduleFile1"; let z = 10;`,
|
||||
};
|
||||
|
||||
const moduleFile2: FileOrFolder = {
|
||||
path: "/a/b/moduleFile2.ts",
|
||||
content: `export var Foo4 = 10;`,
|
||||
};
|
||||
|
||||
const globalFile3: FileOrFolder = {
|
||||
path: "/a/b/globalFile3.ts",
|
||||
content: `interface GlobalFoo { age: number }`
|
||||
};
|
||||
|
||||
const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : [];
|
||||
const configFile = {
|
||||
path: configFilePath,
|
||||
content: JSON.stringify(configObj || { compilerOptions: {} })
|
||||
};
|
||||
|
||||
const files = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile];
|
||||
|
||||
const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files;
|
||||
const host = createServerHost([filesToReload[0], configFile]);
|
||||
|
||||
// Initial project creation
|
||||
const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host);
|
||||
const projectService = session.getProjectService();
|
||||
verifyInitialOpen(filesToReload[0]);
|
||||
|
||||
// Since this is first event, it will have all the files
|
||||
const firstFilesExpected = firstCompilationEmitFiles && getFiles(firstCompilationEmitFiles) || filesToReload;
|
||||
verifyProjectChangedEvent(firstFilesExpected, filesToReload);
|
||||
|
||||
return {
|
||||
moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile,
|
||||
files,
|
||||
updateContentOfOpenFile,
|
||||
verifyProjectChangedEvent,
|
||||
verifyAffectedAllFiles,
|
||||
};
|
||||
|
||||
function getFiles(filelist: string[]) {
|
||||
return map(filelist, getFile);
|
||||
}
|
||||
|
||||
function getFile(fileName: string) {
|
||||
return find(files, file => file.path === fileName);
|
||||
}
|
||||
|
||||
function verifyAffectedAllFiles() {
|
||||
verifyProjectChangedEvent(filter(files, f => f !== libFile));
|
||||
}
|
||||
|
||||
function verifyProjectChangedEvent(filesToEmit: FileOrFolder[], filesToReload?: FileOrFolder[], additionalChangedFiles?: FileOrFolder[]) {
|
||||
const changedFiles = mapDefined(additionalChangedFiles ? filesToEmit.concat(additionalChangedFiles) : filesToEmit, f => f !== configFile ? f.path : undefined);
|
||||
host.reloadFS(filesToReload || files);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
const project = projectService.configuredProjects.get(configFile.path);
|
||||
verifyProjectChangedEventHandler([{
|
||||
eventName: server.ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
changedFiles,
|
||||
filesToEmit: mapDefined(filesToEmit, f => f !== libFile && f !== configFile ? f.path : undefined)
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
function updateContentOfOpenFile(file: FileOrFolder, newContent: string) {
|
||||
session.executeCommandSeq<protocol.ChangeRequest>({
|
||||
command: server.CommandNames.Change,
|
||||
arguments: {
|
||||
file: file.path,
|
||||
insertString: newContent,
|
||||
endLine: 1,
|
||||
endOffset: file.content.length,
|
||||
line: 1,
|
||||
offset: 1
|
||||
}
|
||||
});
|
||||
file.content = newContent;
|
||||
}
|
||||
}
|
||||
|
||||
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
|
||||
const { moduleFile1, file1Consumer1, file1Consumer2, verifyProjectChangedEvent } = getInitialState();
|
||||
|
||||
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]);
|
||||
|
||||
// Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`
|
||||
moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`;
|
||||
verifyProjectChangedEvent([moduleFile1]);
|
||||
});
|
||||
|
||||
it("should be up-to-date with the reference map changes", () => {
|
||||
const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState();
|
||||
|
||||
// Change file1Consumer1 content to `export let y = Foo();`
|
||||
updateContentOfOpenFile(file1Consumer1, "export let y = Foo();");
|
||||
verifyProjectChangedEvent([file1Consumer1]);
|
||||
|
||||
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer2]);
|
||||
|
||||
// Add the import statements back to file1Consumer1
|
||||
updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`);
|
||||
verifyProjectChangedEvent([file1Consumer1]);
|
||||
|
||||
// Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`
|
||||
moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer2, file1Consumer1]);
|
||||
|
||||
// Multiple file edits in one go:
|
||||
|
||||
// Change file1Consumer1 content to `export let y = Foo();`
|
||||
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
|
||||
updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`);
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]);
|
||||
});
|
||||
|
||||
it("should be up-to-date with deleted files", () => {
|
||||
const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent } = getInitialState();
|
||||
|
||||
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
|
||||
// Delete file1Consumer2
|
||||
const filesToLoad = filter(files, file => file !== file1Consumer2);
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1], filesToLoad, [file1Consumer2]);
|
||||
});
|
||||
|
||||
it("should be up-to-date with newly created files", () => {
|
||||
const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent, } = getInitialState();
|
||||
|
||||
const file1Consumer3: FileOrFolder = {
|
||||
path: "/a/b/file1Consumer3.ts",
|
||||
content: `import {Foo} from "./moduleFile1"; let y = Foo();`
|
||||
};
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3));
|
||||
});
|
||||
|
||||
it("should detect changes in non-root files", () => {
|
||||
const { moduleFile1, file1Consumer1, verifyProjectChangedEvent } = getInitialState({
|
||||
configObj: { files: [file1Consumer1Path] },
|
||||
firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path, libFile.path]
|
||||
});
|
||||
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1]);
|
||||
|
||||
// change file1 internal, and verify only file1 is affected
|
||||
moduleFile1.content += "var T1: number;";
|
||||
verifyProjectChangedEvent([moduleFile1]);
|
||||
});
|
||||
|
||||
it("should return all files if a global file changed shape", () => {
|
||||
const { globalFile3, verifyAffectedAllFiles } = getInitialState();
|
||||
|
||||
globalFile3.content += "var T2: string;";
|
||||
verifyAffectedAllFiles();
|
||||
});
|
||||
|
||||
it("should always return the file itself if '--isolatedModules' is specified", () => {
|
||||
const { moduleFile1, verifyProjectChangedEvent } = getInitialState({
|
||||
configObj: { compilerOptions: { isolatedModules: true } }
|
||||
});
|
||||
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1]);
|
||||
});
|
||||
|
||||
it("should always return the file itself if '--out' or '--outFile' is specified", () => {
|
||||
const outFilePath = "/a/b/out.js";
|
||||
const { moduleFile1, verifyProjectChangedEvent } = getInitialState({
|
||||
configObj: { compilerOptions: { module: "system", outFile: outFilePath } }
|
||||
});
|
||||
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1]);
|
||||
});
|
||||
|
||||
it("should return cascaded affected file list", () => {
|
||||
const file1Consumer1Consumer1: FileOrFolder = {
|
||||
path: "/a/b/file1Consumer1Consumer1.ts",
|
||||
content: `import {y} from "./file1Consumer1";`
|
||||
};
|
||||
const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({
|
||||
getAdditionalFileOrFolder: () => [file1Consumer1Consumer1]
|
||||
});
|
||||
|
||||
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;");
|
||||
verifyProjectChangedEvent([file1Consumer1, file1Consumer1Consumer1]);
|
||||
|
||||
// Doesnt change the shape of file1Consumer1
|
||||
moduleFile1.content = `export var T: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]);
|
||||
|
||||
// Change both files before the timeout
|
||||
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;");
|
||||
moduleFile1.content = `export var T2: number;export function Foo() { };`;
|
||||
verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1]);
|
||||
});
|
||||
|
||||
it("should work fine for files with circular references", () => {
|
||||
const file1: FileOrFolder = {
|
||||
path: "/a/b/file1.ts",
|
||||
content: `
|
||||
/// <reference path="./file2.ts" />
|
||||
export var t1 = 10;`
|
||||
};
|
||||
const file2: FileOrFolder = {
|
||||
path: "/a/b/file2.ts",
|
||||
content: `
|
||||
/// <reference path="./file1.ts" />
|
||||
export var t2 = 10;`
|
||||
};
|
||||
const { configFile, verifyProjectChangedEvent, updateContentOfOpenFile } = getInitialState({
|
||||
getAdditionalFileOrFolder: () => [file1, file2],
|
||||
firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath]
|
||||
});
|
||||
|
||||
updateContentOfOpenFile(file1, file1.content + "export var t3 = 10;");
|
||||
verifyProjectChangedEvent([file1, file2], [file1, file2, libFile, configFile]);
|
||||
});
|
||||
|
||||
it("should detect removed code file", () => {
|
||||
const referenceFile1: FileOrFolder = {
|
||||
path: "/a/b/referenceFile1.ts",
|
||||
content: `
|
||||
/// <reference path="./moduleFile1.ts" />
|
||||
export var x = Foo();`
|
||||
};
|
||||
const { configFile, verifyProjectChangedEvent, moduleFile1 } = getInitialState({
|
||||
getAdditionalFileOrFolder: () => [referenceFile1],
|
||||
firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath]
|
||||
});
|
||||
|
||||
verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile], [moduleFile1]);
|
||||
});
|
||||
|
||||
it("should detect non-existing code file", () => {
|
||||
const referenceFile1: FileOrFolder = {
|
||||
path: "/a/b/referenceFile1.ts",
|
||||
content: `
|
||||
/// <reference path="./moduleFile2.ts" />
|
||||
export var x = Foo();`
|
||||
};
|
||||
const { configFile, moduleFile2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({
|
||||
getAdditionalFileOrFolder: () => [referenceFile1],
|
||||
firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath]
|
||||
});
|
||||
|
||||
updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();");
|
||||
verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile]);
|
||||
|
||||
// Create module File2 and see both files are saved
|
||||
verifyProjectChangedEvent([referenceFile1, moduleFile2], [libFile, moduleFile2, referenceFile1, configFile]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("when event handler is set in the session", () => {
|
||||
verifyProjectChangedEvent(createSessionWithProjectChangedEventHandler);
|
||||
|
||||
function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectChangeEventVerifier {
|
||||
const projectChangedEvents: server.ProjectChangedEvent[] = [];
|
||||
const session = createSession(host, {
|
||||
eventHandler: e => {
|
||||
if (e.eventName === server.ProjectChangedEvent) {
|
||||
projectChangedEvents.push(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
session,
|
||||
verifyProjectChangedEventHandler,
|
||||
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler)
|
||||
};
|
||||
|
||||
function eventToString(event: server.ProjectChangedEvent) {
|
||||
const eventToModify = event && {
|
||||
eventName: event.eventName,
|
||||
data: {
|
||||
project: event.data.project.getProjectName(),
|
||||
changedFiles: event.data.changedFiles,
|
||||
filesToEmit: event.data.filesToEmit
|
||||
}
|
||||
};
|
||||
return JSON.stringify(eventToModify);
|
||||
}
|
||||
|
||||
function eventsToString(events: ReadonlyArray<server.ProjectChangedEvent>) {
|
||||
return "[" + map(events, eventToString).join(",") + "]";
|
||||
}
|
||||
|
||||
function verifyProjectChangedEventHandler(expectedEvents: ReadonlyArray<server.ProjectChangedEvent>) {
|
||||
assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`);
|
||||
forEach(projectChangedEvents, (actualEvent, i) => {
|
||||
const expectedEvent = expectedEvents[i];
|
||||
assert.strictEqual(actualEvent.eventName, expectedEvent.eventName);
|
||||
assert.strictEqual(actualEvent.data.project, expectedEvent.data.project);
|
||||
verifyFiles("changedFiles", actualEvent.data.changedFiles, expectedEvent.data.changedFiles);
|
||||
verifyFiles("filesToEmit", actualEvent.data.filesToEmit, expectedEvent.data.filesToEmit);
|
||||
});
|
||||
|
||||
// Verified the events, reset them
|
||||
projectChangedEvents.length = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("when event handler is not set but session is created with canUseEvents = true", () => {
|
||||
verifyProjectChangedEvent(createSessionThatUsesEvents);
|
||||
|
||||
function createSessionThatUsesEvents(host: TestServerHost): ProjectChangeEventVerifier {
|
||||
const session = createSession(host, { canUseEvents: true });
|
||||
|
||||
return {
|
||||
session,
|
||||
verifyProjectChangedEventHandler,
|
||||
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler)
|
||||
};
|
||||
|
||||
function verifyProjectChangedEventHandler(expected: ReadonlyArray<server.ProjectChangedEvent>) {
|
||||
const expectedEvents: protocol.ProjectChangedEventBody[] = map(expected, e => {
|
||||
return {
|
||||
projectName: e.data.project.getProjectName(),
|
||||
changedFiles: e.data.changedFiles,
|
||||
fileNamesToEmit: e.data.filesToEmit
|
||||
};
|
||||
});
|
||||
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
|
||||
const events: protocol.ProjectStructureChangedEvent[] = filter(
|
||||
map(
|
||||
host.getOutput(), s => convertToObject(
|
||||
ts.parseJsonText("json.json", s.replace(outputEventRegex, "")),
|
||||
[]
|
||||
)
|
||||
),
|
||||
e => e.event === server.ProjectChangedEvent
|
||||
);
|
||||
assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`);
|
||||
forEach(events, (actualEvent, i) => {
|
||||
const expectedEvent = expectedEvents[i];
|
||||
assert.strictEqual(actualEvent.body.projectName, expectedEvent.projectName);
|
||||
verifyFiles("changedFiles", actualEvent.body.changedFiles, expectedEvent.changedFiles);
|
||||
verifyFiles("fileNamesToEmit", actualEvent.body.fileNamesToEmit, expectedEvent.fileNamesToEmit);
|
||||
});
|
||||
|
||||
// Verified the events, reset them
|
||||
host.clearOutput();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
namespace ts.server {
|
||||
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
|
||||
|
||||
export const ContextEvent = "context";
|
||||
export const ProjectChangedEvent = "projectChanged";
|
||||
export const ConfigFileDiagEvent = "configFileDiag";
|
||||
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
|
||||
export const ProjectInfoTelemetryEvent = "projectInfo";
|
||||
|
||||
export interface ContextEvent {
|
||||
eventName: typeof ContextEvent;
|
||||
data: { project: Project; fileName: NormalizedPath };
|
||||
export interface ProjectChangedEvent {
|
||||
eventName: typeof ProjectChangedEvent;
|
||||
data: { project: Project; filesToEmit: string[]; changedFiles: string[] };
|
||||
}
|
||||
|
||||
export interface ConfigFileDiagEvent {
|
||||
@@ -77,7 +77,7 @@ namespace ts.server {
|
||||
readonly dts: number;
|
||||
}
|
||||
|
||||
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
|
||||
export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent;
|
||||
|
||||
export interface ProjectServiceEventHandler {
|
||||
(event: ProjectServiceEvent): void;
|
||||
@@ -492,9 +492,32 @@ namespace ts.server {
|
||||
if (this.pendingProjectUpdates.delete(projectName)) {
|
||||
project.updateGraph();
|
||||
}
|
||||
// Send the update event to notify about the project changes
|
||||
this.sendProjectChangedEvent(project);
|
||||
});
|
||||
}
|
||||
|
||||
private sendProjectChangedEvent(project: Project) {
|
||||
if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filesToEmit, changedFiles } = project.getChangedFiles();
|
||||
if (changedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event: ProjectChangedEvent = {
|
||||
eventName: ProjectChangedEvent,
|
||||
data: {
|
||||
project,
|
||||
filesToEmit: filesToEmit as string[],
|
||||
changedFiles: changedFiles as string[]
|
||||
}
|
||||
};
|
||||
this.eventHandler(event);
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) {
|
||||
this.delayUpdateProjectGraph(project);
|
||||
@@ -678,19 +701,6 @@ namespace ts.server {
|
||||
|
||||
// update projects to make sure that set of referenced files is correct
|
||||
this.delayUpdateProjectGraphs(containingProjects);
|
||||
|
||||
// TODO: (sheetalkamat) Someway to send this event so that error checks are updated?
|
||||
// if (!this.eventHandler) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (const openFile of this.openFiles) {
|
||||
// const event: ContextEvent = {
|
||||
// eventName: ContextEvent,
|
||||
// data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
|
||||
// };
|
||||
// this.eventHandler(event);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -133,7 +133,7 @@ namespace ts.server {
|
||||
/*@internal*/
|
||||
lsHost: LSHost;
|
||||
|
||||
builder: Builder;
|
||||
private builder: Builder;
|
||||
/**
|
||||
* Set of files names that were updated since the last call to getChangesSinceVersion.
|
||||
*/
|
||||
@@ -307,6 +307,12 @@ namespace ts.server {
|
||||
return !emitSkipped;
|
||||
}
|
||||
|
||||
getChangedFiles() {
|
||||
Debug.assert(this.languageServiceEnabled);
|
||||
this.ensureBuilder();
|
||||
return this.builder.getChangedProgramFiles(this.program);
|
||||
}
|
||||
|
||||
getProjectVersion() {
|
||||
return this.projectStateVersion.toString();
|
||||
}
|
||||
@@ -392,6 +398,10 @@ namespace ts.server {
|
||||
this.languageService = undefined;
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
return this.lsHost === undefined;
|
||||
}
|
||||
|
||||
getCompilerOptions() {
|
||||
return this.compilerOptions;
|
||||
}
|
||||
@@ -635,8 +645,9 @@ namespace ts.server {
|
||||
|
||||
// update builder only if language service is enabled
|
||||
// otherwise tell it to drop its internal state
|
||||
// Note we are retaining builder so we can send events for project change
|
||||
if (this.builder) {
|
||||
if (this.languageServiceEnabled && this.compileOnSaveEnabled) {
|
||||
if (this.languageServiceEnabled) {
|
||||
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -2040,6 +2040,29 @@ namespace ts.server.protocol {
|
||||
languageServiceEnabled: boolean;
|
||||
}
|
||||
|
||||
export type ProjectChangedEventName = "projectChanged";
|
||||
export interface ProjectStructureChangedEvent extends Event {
|
||||
event: ProjectChangedEventName;
|
||||
body: ProjectChangedEventBody;
|
||||
}
|
||||
|
||||
export interface ProjectChangedEventBody {
|
||||
/**
|
||||
* Project name that has changes
|
||||
*/
|
||||
projectName: string;
|
||||
|
||||
/**
|
||||
* Minimum set of file names to emit
|
||||
*/
|
||||
fileNamesToEmit: string[];
|
||||
|
||||
/**
|
||||
* List of files that have changed/added/removed or could have been affected by the changed files
|
||||
*/
|
||||
changedFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Arguments for reload request.
|
||||
*/
|
||||
|
||||
+34
-24
@@ -332,14 +332,18 @@ namespace ts.server {
|
||||
|
||||
private defaultEventHandler(event: ProjectServiceEvent) {
|
||||
switch (event.eventName) {
|
||||
case ContextEvent:
|
||||
const { project, fileName } = event.data;
|
||||
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
|
||||
this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], 100));
|
||||
case ProjectChangedEvent:
|
||||
const { project, filesToEmit, changedFiles } = event.data;
|
||||
this.projectChangedEvent(project, filesToEmit, changedFiles);
|
||||
break;
|
||||
case ConfigFileDiagEvent:
|
||||
const { triggerFile, configFileName, diagnostics } = event.data;
|
||||
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
|
||||
const { triggerFile, configFileName: configFile, diagnostics } = event.data;
|
||||
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
|
||||
this.event<protocol.ConfigFileDiagnosticEventBody>({
|
||||
triggerFile,
|
||||
configFile,
|
||||
diagnostics: bakedDiags
|
||||
}, "configFileDiag");
|
||||
break;
|
||||
case ProjectLanguageServiceStateEvent: {
|
||||
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
|
||||
@@ -360,6 +364,24 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void {
|
||||
this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`);
|
||||
if (changedFiles.length) {
|
||||
const checkList = this.createCheckList(changedFiles, project);
|
||||
|
||||
// For now only queue error checking for open files. We can change this to include non open files as well
|
||||
this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true));
|
||||
|
||||
|
||||
// Send project changed event
|
||||
this.event<protocol.ProjectChangedEventBody>({
|
||||
projectName: project.getProjectName(),
|
||||
changedFiles,
|
||||
fileNamesToEmit
|
||||
}, "projectChanged");
|
||||
}
|
||||
}
|
||||
|
||||
public logError(err: Error, cmd: string) {
|
||||
let msg = "Exception on executing command " + cmd;
|
||||
if (err.message) {
|
||||
@@ -381,21 +403,6 @@ namespace ts.server {
|
||||
this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine));
|
||||
}
|
||||
|
||||
public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ReadonlyArray<Diagnostic>) {
|
||||
const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true));
|
||||
const ev: protocol.ConfigFileDiagnosticEvent = {
|
||||
seq: 0,
|
||||
type: "event",
|
||||
event: "configFileDiag",
|
||||
body: {
|
||||
triggerFile,
|
||||
configFile,
|
||||
diagnostics: bakedDiags
|
||||
}
|
||||
};
|
||||
this.send(ev);
|
||||
}
|
||||
|
||||
public event<T>(info: T, eventName: string) {
|
||||
const ev: protocol.Event = {
|
||||
seq: 0,
|
||||
@@ -1257,13 +1264,16 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
|
||||
const checkList = mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
|
||||
private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] {
|
||||
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
|
||||
const fileName = toNormalizedPath(uncheckedFileName);
|
||||
const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
|
||||
const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true);
|
||||
return project && { fileName, project };
|
||||
});
|
||||
}
|
||||
|
||||
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
|
||||
const checkList = this.createCheckList(fileNames);
|
||||
if (checkList.length > 0) {
|
||||
this.updateErrorCheck(next, checkList, delay);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user