Merge branch 'master' of https://github.com/Microsoft/TypeScript into 11116

This commit is contained in:
zhengbli
2016-12-28 04:07:04 -08:00
2508 changed files with 247542 additions and 162484 deletions

View File

@@ -5,15 +5,6 @@
namespace ts.server {
interface Hash {
update(data: any, input_encoding?: string): Hash;
digest(encoding: string): any;
}
const crypto: {
createHash(algorithm: string): Hash
} = require("crypto");
export function shouldEmitFile(scriptInfo: ScriptInfo) {
return !scriptInfo.hasMixedContent;
}
@@ -49,9 +40,7 @@ namespace ts.server {
}
private computeHash(text: string): string {
return crypto.createHash("md5")
.update(text)
.digest("base64");
return this.project.projectService.host.createHash(text);
}
private getSourceFile(): SourceFile {
@@ -86,17 +75,32 @@ namespace ts.server {
getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
onProjectUpdateGraph(): void;
emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
clear(): void;
}
abstract class AbstractBuilder<T extends BuilderFileInfo> implements Builder {
private fileInfos = createFileMap<T>();
/**
* stores set of files from the project.
* NOTE: this field is created on demand and should not be accessed directly.
* Use 'getFileInfos' instead.
*/
private fileInfos_doNotAccessDirectly: FileMap<T>;
constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
}
private getFileInfos() {
return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createFileMap<T>());
}
public clear() {
// drop the existing list - it will be re-created as necessary
this.fileInfos_doNotAccessDirectly = undefined;
}
protected getFileInfo(path: Path): T {
return this.fileInfos.get(path);
return this.getFileInfos().get(path);
}
protected getOrCreateFileInfo(path: Path): T {
@@ -110,19 +114,19 @@ namespace ts.server {
}
protected getFileInfoPaths(): Path[] {
return this.fileInfos.getKeys();
return this.getFileInfos().getKeys();
}
protected setFileInfo(path: Path, info: T) {
this.fileInfos.set(path, info);
this.getFileInfos().set(path, info);
}
protected removeFileInfo(path: Path) {
this.fileInfos.remove(path);
this.getFileInfos().remove(path);
}
protected forEachFileInfo(action: (fileInfo: T) => any) {
this.fileInfos.forEachValue((path: Path, value: T) => action(value));
this.getFileInfos().forEachValue((_path, value) => action(value));
}
abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
@@ -242,6 +246,11 @@ namespace ts.server {
private projectVersionForDependencyGraph: string;
public clear() {
this.projectVersionForDependencyGraph = undefined;
super.clear();
}
private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] {
if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
return [];

View File

@@ -10,7 +10,10 @@
"stripInternal": true,
"types": [
"node"
]
],
"target": "es5",
"noUnusedLocals": true,
"noUnusedParameters": true
},
"files": [
"cancellationToken.ts"

View File

@@ -5,11 +5,6 @@ namespace ts.server {
writeMessage(message: string): void;
}
interface CompletionEntry extends CompletionInfo {
fileName: string;
position: number;
}
interface RenameEntry extends RenameInfo {
fileName: string;
position: number;
@@ -246,8 +241,8 @@ namespace ts.server {
return response.body[0];
}
getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol {
throw new Error("Not Implemented Yet.");
getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol {
return notImplemented();
}
getNavigateToItems(searchValue: string): NavigateToItem[] {
@@ -278,7 +273,7 @@ namespace ts.server {
});
}
getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] {
getFormattingEditsForRange(fileName: string, start: number, end: number, _options: ts.FormatCodeOptions): ts.TextChange[] {
const startLineOffset = this.positionToOneBasedLineOffset(fileName, start);
const endLineOffset = this.positionToOneBasedLineOffset(fileName, end);
const args: protocol.FormatRequestArgs = {
@@ -300,7 +295,7 @@ namespace ts.server {
return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName).getLength(), options);
}
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): ts.TextChange[] {
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): ts.TextChange[] {
const lineOffset = this.positionToOneBasedLineOffset(fileName, position);
const args: protocol.FormatOnKeyRequestArgs = {
file: fileName,
@@ -390,7 +385,7 @@ namespace ts.server {
});
}
findReferences(fileName: string, position: number): ReferencedSymbol[] {
findReferences(_fileName: string, _position: number): ReferencedSymbol[] {
// Not yet implemented.
return [];
}
@@ -419,44 +414,50 @@ namespace ts.server {
});
}
getEmitOutput(fileName: string): EmitOutput {
throw new Error("Not Implemented Yet.");
getEmitOutput(_fileName: string): EmitOutput {
return notImplemented();
}
getSyntacticDiagnostics(fileName: string): Diagnostic[] {
const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName };
const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName, includeLinePosition: true };
const request = this.processRequest<protocol.SyntacticDiagnosticsSyncRequest>(CommandNames.SyntacticDiagnosticsSync, args);
const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse>(request);
return (<protocol.Diagnostic[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
return (<protocol.DiagnosticWithLinePosition[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
}
getSemanticDiagnostics(fileName: string): Diagnostic[] {
const args: protocol.SemanticDiagnosticsSyncRequestArgs = { file: fileName };
const args: protocol.SemanticDiagnosticsSyncRequestArgs = { file: fileName, includeLinePosition: true };
const request = this.processRequest<protocol.SemanticDiagnosticsSyncRequest>(CommandNames.SemanticDiagnosticsSync, args);
const response = this.processResponse<protocol.SemanticDiagnosticsSyncResponse>(request);
return (<protocol.Diagnostic[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
return (<protocol.DiagnosticWithLinePosition[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
}
convertDiagnostic(entry: protocol.Diagnostic, fileName: string): Diagnostic {
const start = this.lineOffsetToPosition(fileName, entry.start);
const end = this.lineOffsetToPosition(fileName, entry.end);
convertDiagnostic(entry: protocol.DiagnosticWithLinePosition, _fileName: string): Diagnostic {
let category: DiagnosticCategory;
for (const id in DiagnosticCategory) {
if (typeof id === "string" && entry.category === id.toLowerCase()) {
category = (<any>DiagnosticCategory)[id];
}
}
Debug.assert(category !== undefined, "convertDiagnostic: category should not be undefined");
return {
file: undefined,
start: start,
length: end - start,
messageText: entry.text,
category: undefined,
start: entry.start,
length: entry.length,
messageText: entry.message,
category: category,
code: entry.code
};
}
getCompilerOptionsDiagnostics(): Diagnostic[] {
throw new Error("Not Implemented Yet.");
return notImplemented();
}
getRenameInfo(fileName: string, position: number, findInStrings?: boolean, findInComments?: boolean): RenameInfo {
@@ -560,12 +561,12 @@ namespace ts.server {
this.lineOffsetToPosition(fileName, span.end, lineMap));
}
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan {
throw new Error("Not Implemented Yet.");
getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan {
return notImplemented();
}
getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan {
throw new Error("Not Implemented Yet.");
getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan {
return notImplemented();
}
getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems {
@@ -654,20 +655,20 @@ namespace ts.server {
}
}
getOutliningSpans(fileName: string): OutliningSpan[] {
throw new Error("Not Implemented Yet.");
getOutliningSpans(_fileName: string): OutliningSpan[] {
return notImplemented();
}
getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
throw new Error("Not Implemented Yet.");
getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] {
return notImplemented();
}
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion {
throw new Error("Not Implemented Yet.");
getDocCommentTemplateAtPosition(_fileName: string, _position: number): TextInsertion {
return notImplemented();
}
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
throw new Error("Not Implemented Yet.");
isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean {
return notImplemented();
}
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
@@ -733,35 +734,35 @@ namespace ts.server {
});
}
getIndentationAtPosition(fileName: string, position: number, options: EditorOptions): number {
throw new Error("Not Implemented Yet.");
getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number {
return notImplemented();
}
getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] {
throw new Error("Not Implemented Yet.");
getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
return notImplemented();
}
getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] {
throw new Error("Not Implemented Yet.");
getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] {
return notImplemented();
}
getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications {
throw new Error("Not Implemented Yet.");
getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications {
return notImplemented();
}
getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications {
throw new Error("Not Implemented Yet.");
getEncodedSemanticClassifications(_fileName: string, _span: TextSpan): Classifications {
return notImplemented();
}
getProgram(): Program {
throw new Error("SourceFile objects are not serializable through the server protocol.");
}
getNonBoundSourceFile(fileName: string): SourceFile {
getNonBoundSourceFile(_fileName: string): SourceFile {
throw new Error("SourceFile objects are not serializable through the server protocol.");
}
getSourceFile(fileName: string): SourceFile {
getSourceFile(_fileName: string): SourceFile {
throw new Error("SourceFile objects are not serializable through the server protocol.");
}

View File

@@ -10,13 +10,93 @@
namespace ts.server {
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
export type ProjectServiceEvent =
{ eventName: "context", data: { project: Project, fileName: NormalizedPath } } | { eventName: "configFileDiag", data: { triggerFile?: string, configFileName: string, diagnostics: Diagnostic[] } };
export const ContextEvent = "context";
export const ConfigFileDiagEvent = "configFileDiag";
export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
export interface ContextEvent {
eventName: typeof ContextEvent;
data: { project: Project; fileName: NormalizedPath };
}
export interface ConfigFileDiagEvent {
eventName: typeof ConfigFileDiagEvent;
data: { triggerFile: string, configFileName: string, diagnostics: Diagnostic[] };
}
export interface ProjectLanguageServiceStateEvent {
eventName: typeof ProjectLanguageServiceStateEvent;
data: { project: Project, languageServiceEnabled: boolean };
}
export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent;
export interface ProjectServiceEventHandler {
(event: ProjectServiceEvent): void;
}
function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): Map<Map<number>> {
const map: Map<Map<number>> = createMap<Map<number>>();
for (const option of commandLineOptions) {
if (typeof option.type === "object") {
const optionMap = <Map<number>>option.type;
// verify that map contains only numbers
for (const id in optionMap) {
Debug.assert(typeof optionMap[id] === "number");
}
map[option.name] = optionMap;
}
}
return map;
}
const compilerOptionConverters = prepareConvertersForEnumLikeCompilerOptions(optionDeclarations);
const indentStyle = createMap({
"none": IndentStyle.None,
"block": IndentStyle.Block,
"smart": IndentStyle.Smart
});
export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings {
if (typeof protocolOptions.indentStyle === "string") {
protocolOptions.indentStyle = indentStyle[protocolOptions.indentStyle.toLowerCase()];
Debug.assert(protocolOptions.indentStyle !== undefined);
}
return <any>protocolOptions;
}
export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin {
for (const id in compilerOptionConverters) {
const propertyValue = protocolOptions[id];
if (typeof propertyValue === "string") {
const mappedValues = compilerOptionConverters[id];
protocolOptions[id] = mappedValues[propertyValue.toLowerCase()];
}
}
return <any>protocolOptions;
}
export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind {
return typeof scriptKindName === "string"
? convertScriptKindName(scriptKindName)
: scriptKindName;
}
export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) {
switch (scriptKindName) {
case "JS":
return ScriptKind.JS;
case "JSX":
return ScriptKind.JSX;
case "TS":
return ScriptKind.TS;
case "TSX":
return ScriptKind.TSX;
default:
return ScriptKind.Unknown;
}
}
/**
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
@@ -28,6 +108,7 @@ namespace ts.server {
export interface HostConfiguration {
formatCodeOptions: FormatCodeSettings;
hostInfo: string;
extraFileExtensions?: FileExtensionInfo[];
}
interface ConfigFileConversionResult {
@@ -45,25 +126,28 @@ namespace ts.server {
}
export interface OpenConfiguredProjectResult {
configFileName?: string;
configFileName?: NormalizedPath;
configFileErrors?: Diagnostic[];
}
interface FilePropertyReader<T> {
getFileName(f: T): string;
getScriptKind(f: T): ScriptKind;
hasMixedContent(f: T): boolean;
hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[]): boolean;
}
const fileNamePropertyReader: FilePropertyReader<string> = {
getFileName: x => x,
getScriptKind: _ => undefined,
hasMixedContent: _ => false
hasMixedContent: (fileName, extraFileExtensions) => {
const mixedContentExtensions = ts.map(ts.filter(extraFileExtensions, item => item.isMixedContent), item => item.extension);
return forEach(mixedContentExtensions, extension => fileExtensionIs(fileName, extension))
}
};
const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = {
getFileName: x => x.fileName,
getScriptKind: x => x.scriptKind,
getScriptKind: x => tryConvertScriptKindName(x.scriptKind),
hasMixedContent: x => x.hasMixedContent
};
@@ -177,7 +261,7 @@ namespace ts.server {
private changedFiles: ScriptInfo[];
private toCanonicalFileName: (f: string) => string;
readonly toCanonicalFileName: (f: string) => string;
public lastDeletedFile: ScriptInfo;
@@ -188,6 +272,8 @@ namespace ts.server {
readonly typingsInstaller: ITypingsInstaller = nullTypingsInstaller,
private readonly eventHandler?: ProjectServiceEventHandler) {
Debug.assert(!!host.createHash, "'ServerHost.createHash' is required for ProjectService");
this.toCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
this.directoryWatchers = new DirectoryWatchers(this);
this.throttledOperations = new ThrottledOperations(host);
@@ -200,7 +286,8 @@ namespace ts.server {
this.hostConfiguration = {
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
hostInfo: "Unknown host"
hostInfo: "Unknown host",
extraFileExtensions: []
};
this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
@@ -214,27 +301,44 @@ namespace ts.server {
this.ensureInferredProjectsUpToDate();
}
getCompilerOptionsForInferredProjects() {
return this.compilerOptionsForInferredProjects;
}
onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) {
if (!this.eventHandler) {
return;
}
this.eventHandler(<ProjectLanguageServiceStateEvent>{
eventName: ProjectLanguageServiceStateEvent,
data: { project, languageServiceEnabled }
});
}
updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void {
const project = this.findProject(response.projectName);
if (!project) {
return;
}
switch (response.kind) {
case "set":
this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.typings);
project.updateGraph();
case ActionSet:
this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings);
break;
case "invalidate":
this.typingsCache.invalidateCachedTypingsForProject(project);
case ActionInvalidate:
this.typingsCache.deleteTypingsForProject(response.projectName);
break;
}
project.updateGraph();
}
setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void {
this.compilerOptionsForInferredProjects = projectCompilerOptions;
this.compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions);
// always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside
// previously we did not expose a way for user to change these settings and this option was enabled by default
this.compilerOptionsForInferredProjects.allowNonTsExtensions = true;
this.compileOnSaveForInferredProjects = projectCompilerOptions.compileOnSave;
for (const proj of this.inferredProjects) {
proj.setCompilerOptions(projectCompilerOptions);
proj.setCompilerOptions(this.compilerOptionsForInferredProjects);
proj.compileOnSaveEnabled = projectCompilerOptions.compileOnSave;
}
this.updateProjectGraphs(this.inferredProjects);
@@ -325,7 +429,7 @@ namespace ts.server {
this.handleDeletedFile(info);
}
else {
if (info && (!info.isOpen)) {
if (info && (!info.isScriptOpen())) {
// file has been changed which might affect the set of referenced files in projects that include
// this file and set of inferred projects
info.reloadFromFile();
@@ -341,7 +445,7 @@ namespace ts.server {
// TODO: handle isOpen = true case
if (!info.isOpen) {
if (!info.isScriptOpen()) {
this.filenameToScriptInfo.remove(info.path);
this.lastDeletedFile = info;
@@ -359,7 +463,10 @@ namespace ts.server {
}
for (const openFile of this.openFiles) {
this.eventHandler({ eventName: "context", data: { project: openFile.getDefaultProject(), fileName: openFile.fileName } });
this.eventHandler(<ContextEvent>{
eventName: ContextEvent,
data: { project: openFile.getDefaultProject(), fileName: openFile.fileName }
});
}
}
@@ -368,7 +475,7 @@ namespace ts.server {
private onTypeRootFileChanged(project: ConfiguredProject, fileName: string) {
this.logger.info(`Type root file ${fileName} changed`);
this.throttledOperations.schedule(project.configFileName + " * type root", /*delay*/ 250, () => {
this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => {
project.updateTypes();
this.updateConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?)
this.refreshInferredProjects();
@@ -384,20 +491,20 @@ namespace ts.server {
// If a change was made inside "folder/file", node will trigger the callback twice:
// one with the fileName being "folder/file", and the other one with "folder".
// We don't respond to the second one.
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions())) {
if (fileName && !ts.isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) {
return;
}
this.logger.info(`Detected source file changes: ${fileName}`);
this.throttledOperations.schedule(
project.configFileName,
project.getConfigFilePath(),
/*delay*/250,
() => this.handleChangeInSourceFileForConfiguredProject(project));
() => this.handleChangeInSourceFileForConfiguredProject(project, fileName));
}
private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) {
const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors);
private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject, triggerFile: string) {
const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath());
this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile);
const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f)));
const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f)));
@@ -418,8 +525,10 @@ namespace ts.server {
}
private onConfigChangedForConfiguredProject(project: ConfiguredProject) {
this.logger.info(`Config file changed: ${project.configFileName}`);
this.updateConfiguredProject(project);
const configFileName = project.getConfigFilePath();
this.logger.info(`Config file changed: ${configFileName}`);
const configFileErrors = this.updateConfiguredProject(project);
this.reportConfigFileDiagnostics(configFileName, configFileErrors, /*triggerFile*/ configFileName);
this.refreshInferredProjects();
}
@@ -434,7 +543,7 @@ namespace ts.server {
}
const { configFileErrors } = this.convertConfigFileContentToProjectOptions(fileName);
this.reportConfigFileDiagnostics(fileName, configFileErrors);
this.reportConfigFileDiagnostics(fileName, configFileErrors, fileName);
this.logger.info(`Detected newly added tsconfig file: ${fileName}`);
this.reloadProjects();
@@ -530,15 +639,17 @@ namespace ts.server {
// Closing file should trigger re-reading the file content from disk. This is
// because the user may chose to discard the buffer content before saving
// to the disk, and the server's version of the file can be out of sync.
info.reloadFromFile();
info.close();
removeItemFromSet(this.openFiles, info);
info.isOpen = false;
// collect all projects that should be removed
let projectsToRemove: Project[];
for (const p of info.containingProjects) {
if (p.projectKind === ProjectKind.Configured) {
if (info.hasMixedContent) {
info.registerFileUpdate();
}
// last open file in configured project - close it
if ((<ConfiguredProject>p).deleteOpenRef() === 0) {
(projectsToRemove || (projectsToRemove = [])).push(p);
@@ -548,6 +659,13 @@ namespace ts.server {
// open file in inferred project
(projectsToRemove || (projectsToRemove = [])).push(p);
}
if (!p.languageServiceEnabled) {
// if project language service is disabled then we create a program only for open files.
// this means that project should be marked as dirty to force rebuilding of the program
// on the next request
p.markAsDirty();
}
}
if (projectsToRemove) {
for (const project of projectsToRemove) {
@@ -673,7 +791,13 @@ namespace ts.server {
}
private findConfiguredProjectByProjectName(configFileName: NormalizedPath) {
return findProjectByName(configFileName, this.configuredProjects);
// make sure that casing of config file name is consistent
configFileName = asNormalizedPath(this.toCanonicalFileName(configFileName));
for (const proj of this.configuredProjects) {
if (proj.canonicalConfigFilePath === configFileName) {
return proj;
}
}
}
private findExternalProjectByProjectName(projectFileName: string) {
@@ -701,7 +825,9 @@ namespace ts.server {
this.host,
getDirectoryPath(configFilename),
/*existingOptions*/ {},
configFilename);
configFilename,
/*resolutionStack*/ [],
this.hostConfiguration.extraFileExtensions);
if (parsedCommandLine.errors.length) {
errors = concatenate(errors, parsedCommandLine.errors);
@@ -710,7 +836,7 @@ namespace ts.server {
Debug.assert(!!parsedCommandLine.fileNames);
if (parsedCommandLine.fileNames.length === 0) {
errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename));
(errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename));
return { success: false, configFileErrors: errors };
}
@@ -719,7 +845,7 @@ namespace ts.server {
compilerOptions: parsedCommandLine.options,
configHasFilesProperty: config["files"] !== undefined,
wildcardDirectories: createMap(parsedCommandLine.wildcardDirectories),
typingOptions: parsedCommandLine.typingOptions,
typeAcquisition: parsedCommandLine.typeAcquisition,
compileOnSave: parsedCommandLine.compileOnSave
};
return { success: true, projectOptions, configFileErrors: errors };
@@ -743,27 +869,28 @@ namespace ts.server {
return false;
}
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typingOptions: TypingOptions) {
private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) {
const compilerOptions = convertCompilerOptions(options);
const project = new ExternalProject(
projectFileName,
this,
this.documentRegistry,
options,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(options, files, externalFilePropertyReader),
compilerOptions,
/*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files, externalFilePropertyReader),
options.compileOnSave === undefined ? true : options.compileOnSave);
this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typingOptions, /*configFileErrors*/ undefined);
this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined);
this.externalProjects.push(project);
return project;
}
private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile?: string) {
private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) {
if (!this.eventHandler) {
return;
}
this.eventHandler({
eventName: "configFileDiag",
this.eventHandler(<ConfigFileDiagEvent>{
eventName: ConfigFileDiagEvent,
data: { configFileName, diagnostics: diagnostics || [], triggerFile }
});
}
@@ -780,7 +907,7 @@ namespace ts.server {
/*languageServiceEnabled*/ !sizeLimitExceeded,
projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave);
this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typingOptions, configFileErrors);
this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors);
project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project));
if (!sizeLimitExceeded) {
@@ -799,12 +926,12 @@ namespace ts.server {
}
}
private addFilesToProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName: string, typingOptions: TypingOptions, configFileErrors: Diagnostic[]): void {
private addFilesToProjectAndUpdateGraph<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void {
let errors: Diagnostic[];
for (const f of files) {
const rootFilename = propertyReader.getFileName(f);
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
if (this.host.fileExists(rootFilename)) {
const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent);
project.addRoot(info);
@@ -814,7 +941,7 @@ namespace ts.server {
}
}
project.setProjectErrors(concatenate(configFileErrors, errors));
project.setTypingOptions(typingOptions);
project.setTypeAcquisition(typeAcquisition);
project.updateGraph();
}
@@ -831,7 +958,7 @@ namespace ts.server {
};
}
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypingOptions: TypingOptions, compileOnSave: boolean, configFileErrors: Diagnostic[]) {
private updateNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) {
const oldRootScriptInfos = project.getRootScriptInfos();
const newRootScriptInfos: ScriptInfo[] = [];
const newRootScriptInfoMap: NormalizedPathMap<ScriptInfo> = createNormalizedPathMap<ScriptInfo>();
@@ -850,7 +977,7 @@ namespace ts.server {
rootFilesChanged = true;
if (!scriptInfo) {
const scriptKind = propertyReader.getScriptKind(f);
const hasMixedContent = propertyReader.hasMixedContent(f);
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent);
}
}
@@ -878,7 +1005,7 @@ namespace ts.server {
}
if (toAdd) {
for (const f of toAdd) {
if (f.isOpen && isRootFileInInferredProject(f)) {
if (f.isScriptOpen() && isRootFileInInferredProject(f)) {
// if file is already root in some inferred project
// - remove the file from that project and delete the project if necessary
const inferredProject = f.containingProjects[0];
@@ -893,7 +1020,7 @@ namespace ts.server {
}
project.setCompilerOptions(newOptions);
(<ExternalProject | ConfiguredProject>project).setTypingOptions(newTypingOptions);
(<ExternalProject | ConfiguredProject>project).setTypeAcquisition(newTypeAcquisition);
// VS only set the CompileOnSaveEnabled option in the request if the option was changed recently
// therefore if it is undefined, it should not be updated.
@@ -906,13 +1033,16 @@ namespace ts.server {
}
private updateConfiguredProject(project: ConfiguredProject) {
if (!this.host.fileExists(project.configFileName)) {
if (!this.host.fileExists(project.getConfigFilePath())) {
this.logger.info("Config file deleted");
this.removeProject(project);
return;
}
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
// note: the returned "success" is true does not mean the "configFileErrors" is empty.
// because we might have tolerated the errors and kept going. So always return the configFileErrors
// regardless the "success" here is true or not.
const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath());
if (!success) {
// reset project settings to default
this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/false, configFileErrors);
@@ -923,25 +1053,24 @@ namespace ts.server {
project.setCompilerOptions(projectOptions.compilerOptions);
if (!project.languageServiceEnabled) {
// language service is already disabled
return;
return configFileErrors;
}
project.disableLanguageService();
project.stopWatchingDirectory();
}
else {
if (!project.languageServiceEnabled) {
project.enableLanguageService();
}
project.enableLanguageService();
this.watchConfigDirectoryForProject(project, projectOptions);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions, projectOptions.compileOnSave, configFileErrors);
this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors);
}
return configFileErrors;
}
createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) {
const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length;
const project = useExistingProject
? this.inferredProjects[0]
: new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true, this.compilerOptionsForInferredProjects, /*compileOnSaveEnabled*/ this.compileOnSaveForInferredProjects);
: new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects);
project.addRoot(root);
@@ -974,32 +1103,34 @@ namespace ts.server {
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) {
let info = this.getScriptInfoForNormalizedPath(fileName);
if (!info) {
let content: string;
if (this.host.fileExists(fileName)) {
// by default pick whatever content was supplied as the argument
// if argument was not given - then for mixed content files assume that its content is empty string
content = fileContent || (hasMixedContent ? "" : this.host.readFile(fileName));
}
if (!content) {
if (openedByClient) {
content = "";
}
}
if (content !== undefined) {
info = new ScriptInfo(this.host, fileName, content, scriptKind, openedByClient, hasMixedContent);
// do not watch files with mixed content - server doesn't know how to interpret it
if (openedByClient || this.host.fileExists(fileName)) {
info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent);
this.filenameToScriptInfo.set(info.path, info);
if (!info.isOpen && !hasMixedContent) {
info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName)));
if (openedByClient) {
if (fileContent === undefined) {
// if file is opened by client and its content is not specified - use file text
fileContent = this.host.readFile(fileName) || "";
}
}
else {
// do not watch files with mixed content - server doesn't know how to interpret it
if (!hasMixedContent) {
info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName)));
}
}
}
}
if (info) {
if (fileContent !== undefined) {
info.reload(fileContent);
if (openedByClient && !info.isScriptOpen()) {
info.open(fileContent);
if (hasMixedContent) {
info.registerFileUpdate();
}
}
if (openedByClient) {
info.isOpen = true;
else if (fileContent !== undefined) {
info.reload(fileContent);
}
}
return info;
@@ -1018,7 +1149,7 @@ namespace ts.server {
if (args.file) {
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file));
if (info) {
info.setFormatOptions(args.formatOptions);
info.setFormatOptions(convertFormatOptions(args.formatOptions));
this.logger.info(`Host configuration update for file ${args.file}`);
}
}
@@ -1028,9 +1159,13 @@ namespace ts.server {
this.logger.info(`Host information ${args.hostInfo}`);
}
if (args.formatOptions) {
mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions);
mergeMaps(this.hostConfiguration.formatCodeOptions, convertFormatOptions(args.formatOptions));
this.logger.info("Format host information updated");
}
if (args.extraFileExtensions) {
this.hostConfiguration.extraFileExtensions = args.extraFileExtensions;
this.logger.info("Host file extension mappings updated");
}
}
}
@@ -1096,9 +1231,22 @@ namespace ts.server {
}
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult {
const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName)
? {}
: this.openOrUpdateConfiguredProjectForFile(fileName);
let configFileName: NormalizedPath;
let configFileErrors: Diagnostic[];
let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName);
if (!project) {
({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName));
if (configFileName) {
project = this.findConfiguredProjectByProjectName(configFileName);
}
}
if (project && !project.languageServiceEnabled) {
// if project language service is disabled then we create a program only for open files.
// this means that project should be marked as dirty to force rebuilding of the program
// on the next request
project.markAsDirty();
}
// at this point if file is the part of some configured/external project then this project should be created
const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
@@ -1115,7 +1263,6 @@ namespace ts.server {
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
if (info) {
this.closeOpenFile(info);
info.isOpen = false;
}
this.printProjects();
}
@@ -1140,9 +1287,9 @@ namespace ts.server {
if (openFiles) {
for (const file of openFiles) {
const scriptInfo = this.getScriptInfo(file.fileName);
Debug.assert(!scriptInfo || !scriptInfo.isOpen);
Debug.assert(!scriptInfo || !scriptInfo.isScriptOpen());
const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName);
this.openClientFileWithNormalizedPath(normalizedPath, file.content, file.scriptKind, file.hasMixedContent);
this.openClientFileWithNormalizedPath(normalizedPath, file.content, tryConvertScriptKindName(file.scriptKind), file.hasMixedContent);
}
}
@@ -1212,13 +1359,42 @@ namespace ts.server {
}
}
openExternalProject(proj: protocol.ExternalProject): void {
openExternalProjects(projects: protocol.ExternalProject[]): void {
// record project list before the update
const projectsToClose = arrayToMap(this.externalProjects, p => p.getProjectName(), _ => true);
for (const externalProjectName in this.externalProjectToConfiguredProjectMap) {
projectsToClose[externalProjectName] = true;
}
for (const externalProject of projects) {
this.openExternalProject(externalProject, /*suppressRefreshOfInferredProjects*/ true);
// delete project that is present in input list
delete projectsToClose[externalProject.projectFileName];
}
// close projects that were missing in the input list
for (const externalProjectName in projectsToClose) {
this.closeExternalProject(externalProjectName, /*suppressRefresh*/ true)
}
this.refreshInferredProjects();
}
openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void {
// typingOptions has been deprecated and is only supported for backward compatibility
// purposes. It should be removed in future releases - use typeAcquisition instead.
if (proj.typingOptions && !proj.typeAcquisition) {
const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions);
proj.typeAcquisition = typeAcquisition;
}
let tsConfigFiles: NormalizedPath[];
const rootFiles: protocol.ExternalFile[] = [];
for (const file of proj.rootFiles) {
const normalized = toNormalizedPath(file.fileName);
if (getBaseFileName(normalized) === "tsconfig.json") {
(tsConfigFiles || (tsConfigFiles = [])).push(normalized);
if (this.host.fileExists(normalized)) {
(tsConfigFiles || (tsConfigFiles = [])).push(normalized);
}
}
else {
rootFiles.push(file);
@@ -1234,8 +1410,15 @@ namespace ts.server {
let exisingConfigFiles: string[];
if (externalProject) {
if (!tsConfigFiles) {
const compilerOptions = convertCompilerOptions(proj.options);
if (this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, proj.rootFiles, externalFilePropertyReader)) {
externalProject.disableLanguageService();
}
else {
externalProject.enableLanguageService();
}
// external project already exists and not config files were added - update the project and return;
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined);
this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, /*configFileErrors*/ undefined);
return;
}
// some config files were added to external project (that previously were not there)
@@ -1295,9 +1478,11 @@ namespace ts.server {
else {
// no config files - remove the item from the collection
delete this.externalProjectToConfiguredProjectMap[proj.projectFileName];
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typingOptions);
this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition);
}
if (!suppressRefreshOfInferredProjects) {
this.refreshInferredProjects();
}
this.refreshInferredProjects();
}
}
}

View File

@@ -3,61 +3,67 @@
/// <reference path="scriptInfo.ts" />
namespace ts.server {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost {
private compilationSettings: ts.CompilerOptions;
private readonly resolvedModuleNames: ts.FileMap<Map<ResolvedModuleWithFailedLookupLocations>>;
private readonly resolvedTypeReferenceDirectives: ts.FileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>;
private readonly resolvedModuleNames = createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedTypeReferenceDirectives = createFileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
private readonly getCanonicalFileName: (fileName: string) => string;
private filesWithChangedSetOfUnresolvedImports: Path[];
private readonly resolveModuleName: typeof resolveModuleName;
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;
constructor(private readonly host: ServerHost, private readonly project: Project, private readonly cancellationToken: HostCancellationToken) {
this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames);
this.resolvedModuleNames = createFileMap<Map<ResolvedModuleWithFailedLookupLocations>>();
this.resolvedTypeReferenceDirectives = createFileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
if (host.trace) {
this.trace = s => host.trace(s);
}
this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => {
const globalCache = this.project.getTypeAcquisition().enable
? this.project.projectService.typingsInstaller.globalTypingsCacheLocation
: undefined;
const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host);
if (primaryResult.resolvedModule) {
// return result immediately only if it is .ts, .tsx or .d.ts
// return result immediately only if it is .ts, .tsx or .d.ts
if (!(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
// otherwise try to load typings from @types
if (fileExtensionIsAny(primaryResult.resolvedModule.resolvedFileName, supportedTypeScriptExtensions)) {
return primaryResult;
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache);
if (resolvedModule) {
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
}
}
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const secondaryLookupFailedLookupLocations: string[] = [];
const globalCache = this.project.projectService.typingsInstaller.globalTypingsCacheLocation;
if (this.project.getTypingOptions().enableAutoDiscovery && globalCache) {
const traceEnabled = isTraceEnabled(compilerOptions, host);
if (traceEnabled) {
trace(host, Diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, this.project.getProjectName(), moduleName, globalCache);
}
const state: ModuleResolutionState = { compilerOptions, host, skipTsx: false, traceEnabled };
const resolvedName = loadModuleFromNodeModules(moduleName, globalCache, secondaryLookupFailedLookupLocations, state, /*checkOneLevel*/ true);
if (resolvedName) {
return createResolvedModule(resolvedName, /*isExternalLibraryImport*/ true, primaryResult.failedLookupLocations.concat(secondaryLookupFailedLookupLocations));
}
}
if (!primaryResult.resolvedModule && secondaryLookupFailedLookupLocations.length) {
primaryResult.failedLookupLocations = primaryResult.failedLookupLocations.concat(secondaryLookupFailedLookupLocations);
}
return primaryResult;
};
if (this.host.realpath) {
this.realpath = path => this.host.realpath(path);
}
}
private resolveNamesWithLocalCache<T extends { failedLookupLocations: string[] }, R extends { resolvedFileName?: string }>(
public startRecordingFilesWithChangedResolutions() {
this.filesWithChangedSetOfUnresolvedImports = [];
}
public finishRecordingFilesWithChangedResolutions() {
const collected = this.filesWithChangedSetOfUnresolvedImports;
this.filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
private resolveNamesWithLocalCache<T extends { failedLookupLocations: string[] }, R>(
names: string[],
containingFile: string,
cache: ts.FileMap<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R): R[] {
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined,
logChanges: boolean): R[] {
const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const currentResolutionsInFile = cache.get(path);
@@ -79,6 +85,11 @@ namespace ts.server {
else {
newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this);
}
if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
this.filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
ts.Debug.assert(resolution !== undefined);
@@ -90,6 +101,24 @@ namespace ts.server {
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return getResultFileName(oldResult) === getResultFileName(newResult);
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution) {
return false;
@@ -97,10 +126,7 @@ namespace ts.server {
const result = getResult(resolution);
if (result) {
if (result.resolvedFileName && result.resolvedFileName === lastDeletedFileName) {
return false;
}
return true;
return getResultFileName(result) !== lastDeletedFileName;
}
// consider situation if we have no candidate locations as valid resolution.
@@ -109,6 +135,10 @@ namespace ts.server {
}
}
getNewLine() {
return this.host.newLine;
}
getProjectVersion() {
return this.project.getProjectVersion();
}
@@ -126,11 +156,13 @@ namespace ts.server {
}
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective);
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName, m => m.resolvedModule);
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName,
m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true);
}
getDefaultLibFileName() {
@@ -141,7 +173,7 @@ namespace ts.server {
getScriptSnapshot(filename: string): ts.IScriptSnapshot {
const scriptInfo = this.project.getScriptInfoLSHost(filename);
if (scriptInfo) {
return scriptInfo.snap();
return scriptInfo.getSnapshot();
}
}
@@ -197,10 +229,11 @@ namespace ts.server {
}
setCompilationSettings(opt: ts.CompilerOptions) {
if (changesAffectModuleResolution(this.compilationSettings, opt)) {
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
this.compilationSettings = opt;
// conservatively assume that changing compiler options might affect module resolution strategy
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
}
}

View File

@@ -1,4 +1,4 @@
/// <reference path="..\services\services.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts"/>
/// <reference path="scriptInfo.ts"/>
/// <reference path="lsHost.ts"/>
@@ -62,14 +62,52 @@ namespace ts.server {
projectErrors: Diagnostic[];
}
export class UnresolvedImportsMap {
readonly perFileMap = createFileMap<ReadonlyArray<string>>();
private version = 0;
public clear() {
this.perFileMap.clear();
this.version = 0;
}
public getVersion() {
return this.version;
}
public remove(path: Path) {
this.perFileMap.remove(path);
this.version++;
}
public get(path: Path) {
return this.perFileMap.get(path);
}
public set(path: Path, value: ReadonlyArray<string>) {
this.perFileMap.set(path, value);
this.version++;
}
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
private lsHost: LSHost;
private program: ts.Program;
private languageService: LanguageService;
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
private readonly languageService: LanguageService;
public languageServiceEnabled = true;
builder: Builder;
/**
* Set of files names that were updated since the last call to getChangesSinceVersion.
*/
private updatedFileNames: Map<string>;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
*/
@@ -91,7 +129,7 @@ namespace ts.server {
*/
private projectStateVersion = 0;
private typingFiles: TypingsArray;
private typingFiles: SortedReadonlyArray<string>;
protected projectErrors: Diagnostic[];
@@ -107,12 +145,17 @@ namespace ts.server {
return hasOneOrMoreJsAndNoTsFiles(this);
}
public getCachedUnresolvedImportsPerFile_TestOnly() {
return this.cachedUnresolvedImportsPerFile;
}
constructor(
private readonly projectName: string,
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions,
public compileOnSaveEnabled: boolean) {
@@ -133,10 +176,14 @@ namespace ts.server {
}
}
if (languageServiceEnabled) {
this.enableLanguageService();
}
else {
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
this.lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry);
if (!languageServiceEnabled) {
this.disableLanguageService();
}
@@ -144,6 +191,12 @@ namespace ts.server {
this.markAsDirty();
}
private setInternalCompilerOptionsForEmittingJsFiles() {
if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) {
this.compilerOptions.noEmitForJsFiles = true;
}
}
getProjectErrors() {
return this.projectErrors;
}
@@ -168,23 +221,27 @@ namespace ts.server {
}
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;
if (this.languageServiceEnabled) {
return;
}
this.languageServiceEnabled = true;
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true);
}
disableLanguageService() {
this.languageService = nullLanguageService;
this.lsHost = nullLanguageServiceHost;
if (!this.languageServiceEnabled) {
return;
}
this.languageService.cleanupSemanticCache();
this.languageServiceEnabled = false;
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
}
abstract getProjectName(): string;
getProjectName() {
return this.projectName;
}
abstract getProjectRootPath(): string | undefined;
abstract getTypingOptions(): TypingOptions;
abstract getTypeAcquisition(): TypeAcquisition;
getSourceFile(path: Path) {
if (!this.program) {
@@ -207,8 +264,9 @@ namespace ts.server {
info.detachFromProject(this);
}
}
else {
// release all root files
if (!this.program || !this.languageServiceEnabled) {
// release all root files either if there is no program or language service is disabled.
// in the latter case set of root files can be larger than the set of files in program.
for (const root of this.rootFiles) {
root.detachFromProject(this);
}
@@ -237,7 +295,10 @@ namespace ts.server {
const result: string[] = [];
if (this.rootFiles) {
for (const f of this.rootFiles) {
result.push(f.fileName);
if (this.languageServiceEnabled || f.isScriptOpen()) {
// if language service is disabled - process only files that are open
result.push(f.fileName);
}
}
if (this.typingFiles) {
for (const f of this.typingFiles) {
@@ -253,6 +314,10 @@ namespace ts.server {
}
getScriptInfos() {
if (!this.languageServiceEnabled) {
// if language service is not enabled - return just root files
return this.rootFiles;
}
return map(this.program.getSourceFiles(), sourceFile => {
const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.path);
if (!scriptInfo) {
@@ -269,7 +334,7 @@ namespace ts.server {
return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
}
getFileNames() {
getFileNames(excludeFilesFromExternalLibraries?: boolean) {
if (!this.program) {
return [];
}
@@ -285,8 +350,14 @@ namespace ts.server {
}
return rootFiles;
}
const sourceFiles = this.program.getSourceFiles();
return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
const result: NormalizedPath[] = [];
for (const f of this.program.getSourceFiles()) {
if (excludeFilesFromExternalLibraries && this.program.isSourceFileFromExternalLibrary(f)) {
continue;
}
result.push(asNormalizedPath(f.fileName));
}
return result;
}
getAllEmittableFiles() {
@@ -310,7 +381,7 @@ namespace ts.server {
containsFile(filename: NormalizedPath, requireOpen?: boolean) {
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
if (info && (info.isOpen || !requireOpen)) {
if (info && (info.isScriptOpen() || !requireOpen)) {
return this.containsScriptInfo(info);
}
}
@@ -333,6 +404,7 @@ namespace ts.server {
removeFile(info: ScriptInfo, detachFromProject = true) {
this.removeRootFileIfNecessary(info);
this.lsHost.notifyFileRemoved(info);
this.cachedUnresolvedImportsPerFile.remove(info.path);
if (detachFromProject) {
info.detachFromProject(this);
@@ -341,30 +413,99 @@ namespace ts.server {
this.markAsDirty();
}
registerFileUpdate(fileName: string) {
(this.updatedFileNames || (this.updatedFileNames = createMap<string>()))[fileName] = fileName;
}
markAsDirty() {
this.projectStateVersion++;
}
private extractUnresolvedImportsFromSourceFile(file: SourceFile, result: string[]) {
const cached = this.cachedUnresolvedImportsPerFile.get(file.path);
if (cached) {
// found cached result - use it and return
for (const f of cached) {
result.push(f);
}
return;
}
let unresolvedImports: string[];
if (file.resolvedModules) {
for (const name in file.resolvedModules) {
// pick unresolved non-relative names
if (!file.resolvedModules[name] && !isExternalModuleNameRelative(name)) {
// for non-scoped names extract part up-to the first slash
// for scoped names - extract up to the second slash
let trimmed = name.trim();
let i = trimmed.indexOf("/");
if (i !== -1 && trimmed.charCodeAt(0) === CharacterCodes.at) {
i = trimmed.indexOf("/", i + 1);
}
if (i !== -1) {
trimmed = trimmed.substr(0, i);
}
(unresolvedImports || (unresolvedImports = [])).push(trimmed);
result.push(trimmed);
}
}
}
this.cachedUnresolvedImportsPerFile.set(file.path, unresolvedImports || emptyArray);
}
/**
* 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;
}
this.lsHost.startRecordingFilesWithChangedResolutions();
let hasChanges = this.updateGraphWorker();
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, hasChanges);
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
for (const file of changedFiles) {
// delete cached information for changed files
this.cachedUnresolvedImportsPerFile.remove(file);
}
// 1. no changes in structure, no changes in unresolved imports - do nothing
// 2. no changes in structure, unresolved imports were changed - collect unresolved imports for all files
// (can reuse cached imports for files that were not changed)
// 3. new files were added/removed, but compilation settings stays the same - collect unresolved imports for all new/modified files
// (can reuse cached imports for files that were not changed)
// 4. compilation settings were changed in the way that might affect module resolution - drop all caches and collect all data from the scratch
let unresolvedImports: SortedReadonlyArray<string>;
if (hasChanges || changedFiles.length) {
const result: string[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
this.extractUnresolvedImportsFromSourceFile(sourceFile, result);
}
this.lastCachedUnresolvedImportsList = toSortedReadonlyArray(result);
}
unresolvedImports = this.lastCachedUnresolvedImportsList;
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, unresolvedImports, hasChanges);
if (this.setTypings(cachedTypings)) {
hasChanges = this.updateGraphWorker() || hasChanges;
}
// update builder only if language service is enabled
// otherwise tell it to drop its internal state
if (this.languageServiceEnabled) {
this.builder.onProjectUpdateGraph();
}
else {
this.builder.clear();
}
if (hasChanges) {
this.projectStructureVersion++;
}
return !hasChanges;
}
private setTypings(typings: TypingsArray): boolean {
private setTypings(typings: SortedReadonlyArray<string>): boolean {
if (arrayIsEqualTo(this.typingFiles, typings)) {
return false;
}
@@ -396,7 +537,6 @@ namespace ts.server {
}
}
}
this.builder.onProjectUpdateGraph();
return hasChanges;
}
@@ -437,18 +577,24 @@ namespace ts.server {
compilerOptions.allowJs = true;
}
compilerOptions.allowNonTsExtensions = true;
if (changesAffectModuleResolution(this.compilerOptions, compilerOptions)) {
// reset cached unresolved imports if changes in compiler options affected module resolution
this.cachedUnresolvedImportsPerFile.clear();
this.lastCachedUnresolvedImportsList = undefined;
}
this.compilerOptions = compilerOptions;
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost.setCompilationSettings(compilerOptions);
this.markAsDirty();
}
}
reloadScript(filename: NormalizedPath): boolean {
reloadScript(filename: NormalizedPath, tempFileName?: NormalizedPath): boolean {
const script = this.projectService.getScriptInfoForNormalizedPath(filename);
if (script) {
Debug.assert(script.isAttached(this));
script.reloadFromFile();
script.reloadFromFile(tempFileName);
return true;
}
return false;
@@ -461,12 +607,15 @@ namespace ts.server {
projectName: this.getProjectName(),
version: this.projectStructureVersion,
isInferred: this.projectKind === ProjectKind.Inferred,
options: this.getCompilerOptions()
options: this.getCompilerOptions(),
languageServiceDisabled: !this.languageServiceEnabled
};
const updatedFileNames = this.updatedFileNames;
this.updatedFileNames = undefined;
// 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) {
// if current structure version is the same - return info without any changes
if (this.projectStructureVersion == this.lastReportedVersion && !updatedFileNames) {
return { info, projectErrors: this.projectErrors };
}
// compute and return the difference
@@ -475,6 +624,7 @@ namespace ts.server {
const added: string[] = [];
const removed: string[] = [];
const updated: string[] = getOwnKeys(updatedFileNames);
for (const id in currentFiles) {
if (!hasProperty(lastReportedFileNames, id)) {
added.push(id);
@@ -487,7 +637,7 @@ namespace ts.server {
}
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.projectStructureVersion;
return { info, changes: { added, removed }, projectErrors: this.projectErrors };
return { info, changes: { added, removed, updated }, projectErrors: this.projectErrors };
}
else {
// unknown version - return everything
@@ -563,31 +713,27 @@ namespace ts.server {
export class InferredProject extends Project {
private static NextId = 1;
/**
* Unique name that identifies this particular inferred project
*/
private readonly inferredProjectName: string;
private static newName = (() => {
let nextId = 1;
return () => {
const id = nextId;
nextId++;
return makeInferredProjectName(id);
}
})();
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean) {
super(ProjectKind.Inferred,
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, compilerOptions: CompilerOptions) {
super(InferredProject.newName(),
ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
/*languageServiceEnabled*/ true,
compilerOptions,
compileOnSaveEnabled);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
}
getProjectName() {
return this.inferredProjectName;
/*compileOnSaveEnabled*/ false);
}
getProjectRootPath() {
@@ -607,9 +753,9 @@ namespace ts.server {
}
}
getTypingOptions(): TypingOptions {
getTypeAcquisition(): TypeAcquisition {
return {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
enable: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
@@ -617,16 +763,17 @@ namespace ts.server {
}
export class ConfiguredProject extends Project {
private typingOptions: TypingOptions;
private typeAcquisition: TypeAcquisition;
private projectFileWatcher: FileWatcher;
private directoryWatcher: FileWatcher;
private directoriesWatchedForWildcards: Map<FileWatcher>;
private typeRootsWatchers: FileWatcher[];
readonly canonicalConfigFilePath: NormalizedPath;
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(readonly configFileName: NormalizedPath,
constructor(configFileName: NormalizedPath,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
@@ -634,31 +781,32 @@ namespace ts.server {
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName));
}
getConfigFilePath() {
return this.getProjectName();
}
getProjectRootPath() {
return getDirectoryPath(this.configFileName);
return getDirectoryPath(this.getConfigFilePath());
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
this.typingOptions = newTypingOptions;
setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void {
this.typeAcquisition = newTypeAcquisition;
}
getTypingOptions() {
return this.typingOptions;
}
getProjectName() {
return this.configFileName;
getTypeAcquisition() {
return this.typeAcquisition;
}
watchConfigFile(callback: (project: ConfiguredProject) => void) {
this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this));
this.projectFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), _ => callback(this));
}
watchTypeRoots(callback: (project: ConfiguredProject, path: string) => void) {
@@ -676,7 +824,7 @@ namespace ts.server {
return;
}
const directoryToWatch = getDirectoryPath(this.configFileName);
const directoryToWatch = getDirectoryPath(this.getConfigFilePath());
this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`);
this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true);
}
@@ -685,7 +833,7 @@ namespace ts.server {
if (!this.wildcardDirectories) {
return;
}
const configDirectoryPath = getDirectoryPath(this.configFileName);
const configDirectoryPath = getDirectoryPath(this.getConfigFilePath());
this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => {
if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
@@ -744,15 +892,15 @@ namespace ts.server {
}
export class ExternalProject extends Project {
private typingOptions: TypingOptions;
constructor(readonly externalProjectName: string,
private typeAcquisition: TypeAcquisition;
constructor(externalProjectName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
languageServiceEnabled: boolean,
public compileOnSaveEnabled: boolean,
private readonly projectFilePath?: string) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
getProjectRootPath() {
@@ -762,43 +910,39 @@ namespace ts.server {
// if the projectFilePath is not given, we make the assumption that the project name
// is the path of the project file. AS the project name is provided by VS, we need to
// normalize slashes before using it as a file name.
return getDirectoryPath(normalizeSlashes(this.externalProjectName));
return getDirectoryPath(normalizeSlashes(this.getProjectName()));
}
getTypingOptions() {
return this.typingOptions;
getTypeAcquisition() {
return this.typeAcquisition;
}
setProjectErrors(projectErrors: Diagnostic[]) {
this.projectErrors = projectErrors;
}
setTypingOptions(newTypingOptions: TypingOptions): void {
if (!newTypingOptions) {
setTypeAcquisition(newTypeAcquisition: TypeAcquisition): void {
if (!newTypeAcquisition) {
// set default typings options
newTypingOptions = {
enableAutoDiscovery: allRootFilesAreJsOrDts(this),
newTypeAcquisition = {
enable: allRootFilesAreJsOrDts(this),
include: [],
exclude: []
};
}
else {
if (newTypingOptions.enableAutoDiscovery === undefined) {
if (newTypeAcquisition.enable === undefined) {
// if autoDiscovery was not specified by the caller - set it based on the content of the project
newTypingOptions.enableAutoDiscovery = allRootFilesAreJsOrDts(this);
newTypeAcquisition.enable = allRootFilesAreJsOrDts(this);
}
if (!newTypingOptions.include) {
newTypingOptions.include = [];
if (!newTypeAcquisition.include) {
newTypeAcquisition.include = [];
}
if (!newTypingOptions.exclude) {
newTypingOptions.exclude = [];
if (!newTypeAcquisition.exclude) {
newTypeAcquisition.exclude = [];
}
}
this.typingOptions = newTypingOptions;
}
getProjectName() {
return this.externalProjectName;
this.typeAcquisition = newTypeAcquisition;
}
}
}

View File

@@ -1,4 +1,4 @@
/**
/**
* Declaration module describing the TypeScript Server protocol
*/
namespace ts.server.protocol {
@@ -109,7 +109,7 @@ namespace ts.server.protocol {
/**
* One of "request", "response", or "event"
*/
type: string;
type: "request" | "response" | "event";
}
/**
@@ -833,7 +833,7 @@ namespace ts.server.protocol {
/**
* Script kind of the file
*/
scriptKind?: ScriptKind;
scriptKind?: ScriptKindName | ts.ScriptKind;
/**
* Whether file has mixed content (i.e. .cshtml file that combines html markup with C#/JavaScript)
*/
@@ -861,25 +861,32 @@ namespace ts.server.protocol {
*/
options: ExternalProjectCompilerOptions;
/**
* Explicitly specified typing options for the project
* @deprecated typingOptions. Use typeAcquisition instead
*/
typingOptions?: TypingOptions;
typingOptions?: TypeAcquisition;
/**
* Explicitly specified type acquisition for the project
*/
typeAcquisition?: TypeAcquisition;
}
/**
* For external projects, some of the project settings are sent together with
* compiler settings.
*/
export interface ExternalProjectCompilerOptions extends CompilerOptions {
export interface CompileOnSaveMixin {
/**
* If compile on save is enabled for the project
*/
compileOnSave?: boolean;
}
/**
* For external projects, some of the project settings are sent together with
* compiler settings.
*/
export type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin;
/**
* Contains information about current project version
*/
/* @internal */
export interface ProjectVersionInfo {
/**
* Project name
@@ -896,7 +903,12 @@ namespace ts.server.protocol {
/**
* Current set of compiler options for project
*/
options: CompilerOptions;
options: ts.CompilerOptions;
/**
* true if project language service is disabled
*/
languageServiceDisabled: boolean;
}
/**
@@ -911,6 +923,10 @@ namespace ts.server.protocol {
* List of removed files
*/
removed: string[];
/**
* List of updated files
*/
updated: string[];
}
/**
@@ -920,6 +936,7 @@ namespace ts.server.protocol {
* if changes is set - then this is the set of changes that should be applied to existing project
* otherwise - assume that nothing is changed
*/
/* @internal */
export interface ProjectFiles {
/**
* Information abount project verison
@@ -938,6 +955,7 @@ namespace ts.server.protocol {
/**
* Combines project information with project level errors.
*/
/* @internal */
export interface ProjectFilesWithDiagnostics extends ProjectFiles {
/**
* List of errors in project
@@ -981,6 +999,11 @@ namespace ts.server.protocol {
* The format options to use during formatting and other code editing features.
*/
formatOptions?: FormatCodeSettings;
/**
* The host's additional supported file extensions
*/
extraFileExtensions?: FileExtensionInfo[];
}
/**
@@ -1012,9 +1035,11 @@ namespace ts.server.protocol {
* Used to specify the script kind of the file explicitly. It could be one of the following:
* "TS", "JS", "TSX", "JSX"
*/
scriptKindName?: "TS" | "JS" | "TSX" | "JSX";
scriptKindName?: ScriptKindName;
}
export type ScriptKindName = "TS" | "JS" | "TSX" | "JSX";
/**
* Open request; value of command field is "open". Notify the
* server that the client has file open. The server will not
@@ -1109,6 +1134,7 @@ namespace ts.server.protocol {
/**
* Arguments to SynchronizeProjectListRequest
*/
/* @internal */
export interface SynchronizeProjectListRequestArgs {
/**
* List of last known projects
@@ -1806,6 +1832,27 @@ namespace ts.server.protocol {
event: "configFileDiag";
}
export type ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
export interface ProjectLanguageServiceStateEvent extends Event {
event: ProjectLanguageServiceStateEventName;
body?: ProjectLanguageServiceStateEventBody;
}
export interface ProjectLanguageServiceStateEventBody {
/**
* Project name that has changes in the state of language service.
* For configured projects this will be the config file path.
* For external projects this will be the name of the projects specified when project was open.
* For inferred projects this event is not raised.
*/
projectName: string;
/**
* True if language service state switched from disabled to enabled
* and false otherwise.
*/
languageServiceEnabled: boolean;
}
/**
* Arguments for reload request.
*/
@@ -2049,6 +2096,75 @@ namespace ts.server.protocol {
childItems?: NavigationTree[];
}
export type TelemetryEventName = "telemetry";
export interface TelemetryEvent extends Event {
event: TelemetryEventName;
body: TelemetryEventBody;
}
export interface TelemetryEventBody {
telemetryEventName: string;
payload: any;
}
export type TypingsInstalledTelemetryEventName = "typingsInstalled";
export interface TypingsInstalledTelemetryEventBody extends TelemetryEventBody {
telemetryEventName: TypingsInstalledTelemetryEventName;
payload: TypingsInstalledTelemetryEventPayload;
}
export interface TypingsInstalledTelemetryEventPayload {
/**
* Comma separated list of installed typing packages
*/
installedPackages: string;
/**
* true if install request succeeded, otherwise - false
*/
installSuccess: boolean;
/**
* version of typings installer
*/
typingsInstallerVersion: string;
}
export type BeginInstallTypesEventName = "beginInstallTypes";
export type EndInstallTypesEventName = "endInstallTypes";
export interface BeginInstallTypesEvent extends Event {
event: BeginInstallTypesEventName;
body: BeginInstallTypesEventBody;
}
export interface EndInstallTypesEvent extends Event {
event: EndInstallTypesEventName;
body: EndInstallTypesEventBody;
}
export interface InstallTypesEventBody {
/**
* correlation id to match begin and end events
*/
eventId: number;
/**
* list of packages to install
*/
packages: ReadonlyArray<string>;
}
export interface BeginInstallTypesEventBody extends InstallTypesEventBody {
}
export interface EndInstallTypesEventBody extends InstallTypesEventBody {
/**
* true if installation succeeded, otherwise false
*/
success: boolean;
}
export interface NavBarResponse extends Response {
body?: NavigationBarItem[];
}
@@ -2056,4 +2172,143 @@ namespace ts.server.protocol {
export interface NavTreeResponse extends Response {
body?: NavigationTree;
}
export namespace IndentStyle {
export type None = "None";
export type Block = "Block";
export type Smart = "Smart";
}
export type IndentStyle = IndentStyle.None | IndentStyle.Block | IndentStyle.Smart;
export interface EditorSettings {
baseIndentSize?: number;
indentSize?: number;
tabSize?: number;
newLineCharacter?: string;
convertTabsToSpaces?: boolean;
indentStyle?: IndentStyle | ts.IndentStyle;
}
export interface FormatCodeSettings extends EditorSettings {
insertSpaceAfterCommaDelimiter?: boolean;
insertSpaceAfterSemicolonInForStatements?: boolean;
insertSpaceBeforeAndAfterBinaryOperators?: boolean;
insertSpaceAfterConstructor?: boolean;
insertSpaceAfterKeywordsInControlFlowStatements?: boolean;
insertSpaceAfterFunctionKeywordForAnonymousFunctions?: boolean;
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis?: boolean;
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets?: boolean;
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces?: boolean;
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces?: boolean;
insertSpaceBeforeFunctionParenthesis?: boolean;
placeOpenBraceOnNewLineForFunctions?: boolean;
placeOpenBraceOnNewLineForControlBlocks?: boolean;
}
export interface CompilerOptions {
allowJs?: boolean;
allowSyntheticDefaultImports?: boolean;
allowUnreachableCode?: boolean;
allowUnusedLabels?: boolean;
baseUrl?: string;
charset?: string;
declaration?: boolean;
declarationDir?: string;
disableSizeLimit?: boolean;
emitBOM?: boolean;
emitDecoratorMetadata?: boolean;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
inlineSourceMap?: boolean;
inlineSources?: boolean;
isolatedModules?: boolean;
jsx?: JsxEmit | ts.JsxEmit;
lib?: string[];
locale?: string;
mapRoot?: string;
maxNodeModuleJsDepth?: number;
module?: ModuleKind | ts.ModuleKind;
moduleResolution?: ModuleResolutionKind | ts.ModuleResolutionKind;
newLine?: NewLineKind | ts.NewLineKind;
noEmit?: boolean;
noEmitHelpers?: boolean;
noEmitOnError?: boolean;
noErrorTruncation?: boolean;
noFallthroughCasesInSwitch?: boolean;
noImplicitAny?: boolean;
noImplicitReturns?: boolean;
noImplicitThis?: boolean;
noUnusedLocals?: boolean;
noUnusedParameters?: boolean;
noImplicitUseStrict?: boolean;
noLib?: boolean;
noResolve?: boolean;
out?: string;
outDir?: string;
outFile?: string;
paths?: MapLike<string[]>;
preserveConstEnums?: boolean;
project?: string;
reactNamespace?: string;
removeComments?: boolean;
rootDir?: string;
rootDirs?: string[];
skipLibCheck?: boolean;
skipDefaultLibCheck?: boolean;
sourceMap?: boolean;
sourceRoot?: string;
strictNullChecks?: boolean;
suppressExcessPropertyErrors?: boolean;
suppressImplicitAnyIndexErrors?: boolean;
target?: ScriptTarget | ts.ScriptTarget;
traceResolution?: boolean;
types?: string[];
/** Paths used to used to compute primary types search locations */
typeRoots?: string[];
[option: string]: CompilerOptionsValue | undefined;
}
export namespace JsxEmit {
export type None = "None";
export type Preserve = "Preserve";
export type React = "React";
}
export type JsxEmit = JsxEmit.None | JsxEmit.Preserve | JsxEmit.React;
export namespace ModuleKind {
export type None = "None";
export type CommonJS = "CommonJS";
export type AMD = "AMD";
export type UMD = "UMD";
export type System = "System";
export type ES6 = "ES6";
export type ES2015 = "ES2015";
}
export type ModuleKind = ModuleKind.None | ModuleKind.CommonJS | ModuleKind.AMD | ModuleKind.UMD | ModuleKind.System | ModuleKind.ES6 | ModuleKind.ES2015;
export namespace ModuleResolutionKind {
export type Classic = "Classic";
export type Node = "Node";
}
export type ModuleResolutionKind = ModuleResolutionKind.Classic | ModuleResolutionKind.Node;
export namespace NewLineKind {
export type Crlf = "Crlf";
export type Lf = "Lf";
}
export type NewLineKind = NewLineKind.Crlf | NewLineKind.Lf;
export namespace ScriptTarget {
export type ES3 = "ES3";
export type ES5 = "ES5";
export type ES6 = "ES6";
export type ES2015 = "ES2015";
}
export type ScriptTarget = ScriptTarget.ES3 | ScriptTarget.ES5 | ScriptTarget.ES6 | ScriptTarget.ES2015;
}

View File

@@ -2,6 +2,161 @@
namespace ts.server {
/* @internal */
export class TextStorage {
private svc: ScriptVersionCache | undefined;
private svcVersion = 0;
private text: string;
private lineMap: number[];
private textVersion = 0;
constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath) {
}
public getVersion() {
return this.svc
? `SVC-${this.svcVersion}-${this.svc.getSnapshot().version}`
: `Text-${this.textVersion}`;
}
public hasScriptVersionCache() {
return this.svc !== undefined;
}
public useScriptVersionCache(newText?: string) {
this.switchToScriptVersionCache(newText);
}
public useText(newText?: string) {
this.svc = undefined;
this.setText(newText);
}
public edit(start: number, end: number, newText: string) {
this.switchToScriptVersionCache().edit(start, end - start, newText);
}
public reload(text: string) {
if (this.svc) {
this.svc.reload(text);
}
else {
this.setText(text);
}
}
public reloadFromFile(tempFileName?: string) {
if (this.svc || (tempFileName !== this.fileName)) {
this.reload(this.getFileText(tempFileName))
}
else {
this.setText(undefined);
}
}
public getSnapshot(): IScriptSnapshot {
return this.svc
? this.svc.getSnapshot()
: ScriptSnapshot.fromString(this.getOrLoadText());
}
public getLineInfo(line: number) {
return this.switchToScriptVersionCache().getSnapshot().index.lineNumberToInfo(line);
}
/**
* @param line 0 based index
*/
lineToTextSpan(line: number) {
if (!this.svc) {
const lineMap = this.getLineMap();
const start = lineMap[line]; // -1 since line is 1-based
const end = line + 1 < lineMap.length ? lineMap[line + 1] : this.text.length;
return ts.createTextSpanFromBounds(start, end);
}
const index = this.svc.getSnapshot().index;
const lineInfo = index.lineNumberToInfo(line + 1);
let len: number;
if (lineInfo.leaf) {
len = lineInfo.leaf.text.length;
}
else {
const nextLineInfo = index.lineNumberToInfo(line + 2);
len = nextLineInfo.offset - lineInfo.offset;
}
return ts.createTextSpan(lineInfo.offset, len);
}
/**
* @param line 1 based index
* @param offset 1 based index
*/
lineOffsetToPosition(line: number, offset: number): number {
if (!this.svc) {
return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1);
}
const index = this.svc.getSnapshot().index;
const lineInfo = index.lineNumberToInfo(line);
// TODO: assert this offset is actually on the line
return (lineInfo.offset + offset - 1);
}
/**
* @param line 1-based index
* @param offset 1-based index
*/
positionToLineOffset(position: number): ILineInfo {
if (!this.svc) {
const { line, character } = computeLineAndCharacterOfPosition(this.getLineMap(), position);
return { line: line + 1, offset: character + 1 };
}
const index = this.svc.getSnapshot().index;
const lineOffset = index.charOffsetToLineNumberAndPos(position);
return { line: lineOffset.line, offset: lineOffset.offset + 1 };
}
private getFileText(tempFileName?: string) {
return this.host.readFile(tempFileName || this.fileName) || "";
}
private ensureNoScriptVersionCache() {
Debug.assert(!this.svc, "ScriptVersionCache should not be set");
}
private switchToScriptVersionCache(newText?: string): ScriptVersionCache {
if (!this.svc) {
this.svc = ScriptVersionCache.fromString(this.host, newText !== undefined ? newText : this.getOrLoadText());
this.svcVersion++;
this.text = undefined;
}
return this.svc;
}
private getOrLoadText() {
this.ensureNoScriptVersionCache();
if (this.text === undefined) {
this.setText(this.getFileText());
}
return this.text;
}
private getLineMap() {
this.ensureNoScriptVersionCache();
return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText()));
}
private setText(newText: string) {
this.ensureNoScriptVersionCache();
if (newText === undefined || this.text !== newText) {
this.text = newText;
this.lineMap = undefined;
this.textVersion++;
}
}
}
export class ScriptInfo {
/**
* All projects that include this file
@@ -11,24 +166,46 @@ namespace ts.server {
readonly path: Path;
private fileWatcher: FileWatcher;
private svc: ScriptVersionCache;
private textStorage: TextStorage;
private isOpen: boolean;
// TODO: allow to update hasMixedContent from the outside
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
content: string,
readonly scriptKind: ScriptKind,
public isOpen = false,
public hasMixedContent = false) {
this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames));
this.svc = ScriptVersionCache.fromString(host, content);
this.textStorage = new TextStorage(host, fileName);
if (hasMixedContent) {
this.textStorage.reload("");
}
this.scriptKind = scriptKind
? scriptKind
: getScriptKindFromFileName(fileName);
}
public isScriptOpen() {
return this.isOpen;
}
public open(newText: string) {
this.isOpen = true;
this.textStorage.useScriptVersionCache(newText);
this.markContainingProjectsAsDirty();
}
public close() {
this.isOpen = false;
this.textStorage.useText(this.hasMixedContent ? "" : undefined);
this.markContainingProjectsAsDirty();
}
public getSnapshot() {
return this.textStorage.getSnapshot();
}
getFormatCodeSettings() {
return this.formatCodeSettings;
}
@@ -87,10 +264,15 @@ namespace ts.server {
if (this.containingProjects.length === 0) {
return Errors.ThrowNoProject();
}
Debug.assert(this.containingProjects.length !== 0);
return this.containingProjects[0];
}
registerFileUpdate(): void {
for (const p of this.containingProjects) {
p.registerFileUpdate(this.path);
}
}
setFormatOptions(formatSettings: FormatCodeSettings): void {
if (formatSettings) {
if (!this.formatCodeSettings) {
@@ -113,40 +295,35 @@ namespace ts.server {
}
getLatestVersion() {
return this.svc.latestVersion().toString();
return this.textStorage.getVersion();
}
reload(script: string) {
this.svc.reload(script);
this.textStorage.reload(script);
this.markContainingProjectsAsDirty();
}
saveTo(fileName: string) {
const snap = this.snap();
const snap = this.textStorage.getSnapshot();
this.host.writeFile(fileName, snap.getText(0, snap.getLength()));
}
reloadFromFile() {
reloadFromFile(tempFileName?: NormalizedPath) {
if (this.hasMixedContent) {
this.reload("");
}
else {
this.svc.reloadFromFile(this.fileName);
this.textStorage.reloadFromFile(tempFileName);
this.markContainingProjectsAsDirty();
}
}
snap() {
return this.svc.getSnapshot();
}
getLineInfo(line: number) {
const snap = this.snap();
return snap.index.lineNumberToInfo(line);
return this.textStorage.getLineInfo(line);
}
editContent(start: number, end: number, newText: string): void {
this.svc.edit(start, end - start, newText);
this.textStorage.edit(start, end, newText);
this.markContainingProjectsAsDirty();
}
@@ -160,17 +337,7 @@ namespace ts.server {
* @param line 1 based index
*/
lineToTextSpan(line: number) {
const index = this.snap().index;
const lineInfo = index.lineNumberToInfo(line + 1);
let len: number;
if (lineInfo.leaf) {
len = lineInfo.leaf.text.length;
}
else {
const nextLineInfo = index.lineNumberToInfo(line + 2);
len = nextLineInfo.offset - lineInfo.offset;
}
return ts.createTextSpan(lineInfo.offset, len);
return this.textStorage.lineToTextSpan(line);
}
/**
@@ -178,11 +345,7 @@ namespace ts.server {
* @param offset 1 based index
*/
lineOffsetToPosition(line: number, offset: number): number {
const index = this.snap().index;
const lineInfo = index.lineNumberToInfo(line);
// TODO: assert this offset is actually on the line
return (lineInfo.offset + offset - 1);
return this.textStorage.lineOffsetToPosition(line, offset);
}
/**
@@ -190,9 +353,7 @@ namespace ts.server {
* @param offset 1-based index
*/
positionToLineOffset(position: number): ILineInfo {
const index = this.snap().index;
const lineOffset = index.charOffsetToLineNumberAndPos(position);
return { line: lineOffset.line, offset: lineOffset.offset + 1 };
return this.textStorage.positionToLineOffset(position);
}
}
}

View File

@@ -41,7 +41,7 @@ namespace ts.server {
class BaseLineIndexWalker implements ILineIndexWalker {
goSubtree = true;
done = false;
leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) {
leaf(_rangeStart: number, _rangeLength: number, _ll: LineLeaf) {
}
}
@@ -113,7 +113,7 @@ namespace ts.server {
if (len > 1) {
let insertedNodes = <LineCollection[]>new Array(len - 1);
let startNode = <LineCollection>leafNode;
for (let i = 1, len = lines.length; i < len; i++) {
for (let i = 1; i < lines.length; i++) {
insertedNodes[i - 1] = new LineLeaf(lines[i]);
}
let pathIndex = this.startPath.length - 2;
@@ -150,7 +150,7 @@ namespace ts.server {
return this.lineIndex;
}
post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection {
post(_relativeStart: number, _relativeLength: number, lineCollection: LineCollection): LineCollection {
// have visited the path for start of range, now looking for end
// if range is on single line, we will never make this state transition
if (lineCollection === this.lineCollectionAtBranch) {
@@ -161,7 +161,7 @@ namespace ts.server {
return undefined;
}
pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) {
pre(_relativeStart: number, _relativeLength: number, lineCollection: LineCollection, _parent: LineCollection, nodeType: CharRangeSection) {
// currentNode corresponds to parent, but in the new tree
const currentNode = this.stack[this.stack.length - 1];
@@ -341,8 +341,7 @@ namespace ts.server {
let snap = this.versions[this.currentVersionToIndex()];
if (this.changes.length > 0) {
let snapIndex = snap.index;
for (let i = 0, len = this.changes.length; i < len; i++) {
const change = this.changes[i];
for (const change of this.changes) {
snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText);
}
snap = new LineIndexSnapshot(this.currentVersion + 1, this);
@@ -366,8 +365,7 @@ namespace ts.server {
const textChangeRanges: ts.TextChangeRange[] = [];
for (let i = oldVersion + 1; i <= newVersion; i++) {
const snap = this.versions[this.versionToIndex(i)];
for (let j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) {
const textChange = snap.changesSincePreviousVersion[j];
for (const textChange of snap.changesSincePreviousVersion) {
textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange();
}
}
@@ -398,7 +396,7 @@ namespace ts.server {
index: LineIndex;
changesSincePreviousVersion: TextChange[] = [];
constructor(public version: number, public cache: ScriptVersionCache) {
constructor(readonly version: number, readonly cache: ScriptVersionCache) {
}
getText(rangeStart: number, rangeEnd: number) {
@@ -414,7 +412,7 @@ namespace ts.server {
const starts: number[] = [-1];
let count = 1;
let pos = 0;
this.index.every((ll, s, len) => {
this.index.every(ll => {
starts[count] = pos;
count++;
pos += ll.text.length;
@@ -438,8 +436,9 @@ namespace ts.server {
}
}
getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange {
const oldSnap = <LineIndexSnapshot>oldSnapshot;
return this.getTextChangeRangeSinceVersion(oldSnap.version);
if (oldSnapshot instanceof LineIndexSnapshot && this.cache === oldSnapshot.cache) {
return this.getTextChangeRangeSinceVersion(oldSnapshot.version);
}
}
}
@@ -470,7 +469,7 @@ namespace ts.server {
load(lines: string[]) {
if (lines.length > 0) {
const leaves: LineLeaf[] = [];
for (let i = 0, len = lines.length; i < len; i++) {
for (let i = 0; i < lines.length; i++) {
leaves[i] = new LineLeaf(lines[i]);
}
this.root = LineIndex.buildTreeFromBottom(leaves);
@@ -642,8 +641,7 @@ namespace ts.server {
updateCounts() {
this.totalChars = 0;
this.totalLines = 0;
for (let i = 0, len = this.children.length; i < len; i++) {
const child = this.children[i];
for (const child of this.children) {
this.totalChars += child.charCount();
this.totalLines += child.lineCount();
}

View File

@@ -1,4 +1,5 @@
/// <reference types="node" />
/// <reference path="shared.ts" />
/// <reference path="session.ts" />
// used in fs.writeSync
/* tslint:disable:no-null-keyword */
@@ -14,32 +15,53 @@ namespace ts.server {
} = require("child_process");
const os: {
homedir(): string
homedir?(): string;
tmpdir(): string;
} = require("os");
function getGlobalTypingsCacheLocation() {
let basePath: string;
switch (process.platform) {
case "win32":
basePath = process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir();
break;
case "linux":
basePath = os.homedir();
break;
case "win32": {
const basePath = process.env.LOCALAPPDATA ||
process.env.APPDATA ||
(os.homedir && os.homedir()) ||
process.env.USERPROFILE ||
(process.env.HOMEDRIVE && process.env.HOMEPATH && normalizeSlashes(process.env.HOMEDRIVE + process.env.HOMEPATH)) ||
os.tmpdir();
return combinePaths(normalizeSlashes(basePath), "Microsoft/TypeScript");
}
case "darwin":
basePath = combinePaths(os.homedir(), "Library/Application Support/");
break;
case "linux":
case "android": {
const cacheLocation = getNonWindowsCacheLocation(process.platform === "darwin");
return combinePaths(cacheLocation, "typescript");
}
default:
Debug.fail(`unsupported platform '${process.platform}'`);
return;
}
}
Debug.assert(basePath !== undefined);
return combinePaths(normalizeSlashes(basePath), "Microsoft/TypeScript");
function getNonWindowsCacheLocation(platformIsDarwin: boolean) {
if (process.env.XDG_CACHE_HOME) {
return process.env.XDG_CACHE_HOME;
}
const usersDir = platformIsDarwin ? "Users" : "home"
const homePath = (os.homedir && os.homedir()) ||
process.env.HOME ||
((process.env.LOGNAME || process.env.USER) && `/${usersDir}/${process.env.LOGNAME || process.env.USER}`) ||
os.tmpdir();
const cacheFolder = platformIsDarwin
? "Library/Caches"
: ".cache"
return combinePaths(normalizeSlashes(homePath), cacheFolder);
}
interface NodeChildProcess {
send(message: any, sendHandle?: any): void;
on(message: "message", f: (m: any) => void): void;
kill(): void;
pid: number;
}
interface NodeSocket {
@@ -53,14 +75,6 @@ namespace ts.server {
historySize?: number;
}
interface Key {
sequence?: string;
name?: string;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
}
interface Stats {
isFile(): boolean;
isDirectory(): boolean;
@@ -187,30 +201,58 @@ namespace ts.server {
class NodeTypingsInstaller implements ITypingsInstaller {
private installer: NodeChildProcess;
private installerPidReported = false;
private socket: NodeSocket;
private projectService: ProjectService;
private throttledOperations: ThrottledOperations;
private eventSender: EventSender;
constructor(
private readonly telemetryEnabled: boolean,
private readonly logger: server.Logger,
private readonly eventPort: number,
host: ServerHost,
eventPort: number,
readonly globalTypingsCacheLocation: string,
private newLine: string) {
this.throttledOperations = new ThrottledOperations(host);
if (eventPort) {
const s = net.connect({ port: eventPort }, () => {
this.socket = s;
this.reportInstallerProcessId();
});
}
}
private reportInstallerProcessId() {
if (this.installerPidReported) {
return;
}
if (this.socket && this.installer) {
this.sendEvent(0, "typingsInstallerPid", { pid: this.installer.pid });
this.installerPidReported = true;
}
}
private sendEvent(seq: number, event: string, body: any): void {
this.socket.write(formatMessage({ seq, type: "event", event, body }, this.logger, Buffer.byteLength, this.newLine), "utf8");
}
setTelemetrySender(telemetrySender: EventSender) {
this.eventSender = telemetrySender;
}
attach(projectService: ProjectService) {
this.projectService = projectService;
if (this.logger.hasLevel(LogLevel.requestTime)) {
this.logger.info("Binding...");
}
const args: string[] = ["--globalTypingsCacheLocation", this.globalTypingsCacheLocation];
const args: string[] = [Arguments.GlobalCacheLocation, this.globalTypingsCacheLocation];
if (this.telemetryEnabled) {
args.push(Arguments.EnableTelemetry);
}
if (this.logger.loggingEnabled() && this.logger.getLogFileName()) {
args.push("--logFile", combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`));
args.push(Arguments.LogFile, combinePaths(getDirectoryPath(normalizeSlashes(this.logger.getLogFileName())), `ti-${process.pid}.log`));
}
const execArgv: string[] = [];
{
@@ -218,7 +260,7 @@ namespace ts.server {
const match = /^--(debug|inspect)(=(\d+))?$/.exec(arg);
if (match) {
// if port is specified - use port + 1
// otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1
// otherwise pick a default port depending on if 'debug' or 'inspect' and use its value + 1
const currentPort = match[3] !== undefined
? +match[3]
: match[1] === "debug" ? 5858 : 9229;
@@ -230,6 +272,8 @@ namespace ts.server {
this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv });
this.installer.on("message", m => this.handleMessage(m));
this.reportInstallerProcessId();
process.on("exit", () => {
this.installer.kill();
});
@@ -239,21 +283,70 @@ namespace ts.server {
this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" });
}
enqueueInstallTypingsRequest(project: Project, typingOptions: TypingOptions): void {
const request = createInstallTypingsRequest(project, typingOptions);
enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void {
const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Sending request: ${JSON.stringify(request)}`);
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Scheduling throttled operation: ${JSON.stringify(request)}`);
}
}
this.installer.send(request);
this.throttledOperations.schedule(project.getProjectName(), /*ms*/ 250, () => {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Sending request: ${JSON.stringify(request)}`);
}
this.installer.send(request);
});
}
private handleMessage(response: SetTypings | InvalidateCachedTypings) {
private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes) {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Received response: ${JSON.stringify(response)}`);
}
if (response.kind === EventBeginInstallTypes) {
if (!this.eventSender) {
return;
}
const body: protocol.BeginInstallTypesEventBody = {
eventId: response.eventId,
packages: response.packagesToInstall,
};
const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes";
this.eventSender.event(body, eventName);
return;
}
if (response.kind === EventEndInstallTypes) {
if (!this.eventSender) {
return;
}
if (this.telemetryEnabled) {
const body: protocol.TypingsInstalledTelemetryEventBody = {
telemetryEventName: "typingsInstalled",
payload: {
installedPackages: response.packagesToInstall.join(","),
installSuccess: response.installSuccess,
typingsInstallerVersion: response.typingsInstallerVersion
}
};
const eventName: protocol.TelemetryEventName = "telemetry";
this.eventSender.event(body, eventName);
}
const body: protocol.EndInstallTypesEventBody = {
eventId: response.eventId,
packages: response.packagesToInstall,
success: response.installSuccess,
};
const eventName: protocol.EndInstallTypesEventName = "endInstallTypes";
this.eventSender.event(body, eventName);
return;
}
this.projectService.updateTypingsForProject(response);
if (response.kind == "set" && this.socket) {
this.socket.write(formatMessage({ seq: 0, type: "event", message: response }, this.logger, Buffer.byteLength, this.newLine), "utf8");
if (response.kind == ActionSet && this.socket) {
this.sendEvent(0, "setTypings", response);
}
}
}
@@ -265,17 +358,27 @@ namespace ts.server {
installerEventPort: number,
canUseEvents: boolean,
useSingleInferredProject: boolean,
disableAutomaticTypingAcquisition: boolean,
globalTypingsCacheLocation: string,
telemetryEnabled: boolean,
logger: server.Logger) {
super(
host,
cancellationToken,
useSingleInferredProject,
new NodeTypingsInstaller(logger, installerEventPort, globalTypingsCacheLocation, host.newLine),
Buffer.byteLength,
process.hrtime,
logger,
canUseEvents);
const typingsInstaller = disableAutomaticTypingAcquisition
? undefined
: new NodeTypingsInstaller(telemetryEnabled, logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine);
super(
host,
cancellationToken,
useSingleInferredProject,
typingsInstaller || nullTypingsInstaller,
Buffer.byteLength,
process.hrtime,
logger,
canUseEvents);
if (telemetryEnabled && typingsInstaller) {
typingsInstaller.setTelemetrySender(this);
}
}
exit() {
@@ -306,7 +409,8 @@ namespace ts.server {
function parseLoggingEnvironmentString(logEnvStr: string): LogOptions {
const logEnv: LogOptions = { logToFile: true };
const args = logEnvStr.split(" ");
for (let i = 0, len = args.length; i < (len - 1); i += 2) {
const len = args.length - 1;
for (let i = 0; i < len; i += 2) {
const option = args[i];
const value = args[i + 1];
if (option && value) {
@@ -502,23 +606,31 @@ namespace ts.server {
let eventPort: number;
{
const index = sys.args.indexOf("--eventPort");
if (index >= 0 && index < sys.args.length - 1) {
const v = parseInt(sys.args[index + 1]);
if (!isNaN(v)) {
eventPort = v;
}
const str = findArgument("--eventPort");
const v = str && parseInt(str);
if (!isNaN(v)) {
eventPort = v;
}
}
const useSingleInferredProject = sys.args.indexOf("--useSingleInferredProject") >= 0;
const localeStr = findArgument("--locale");
if (localeStr) {
validateLocaleAndSetLanguage(localeStr, sys);
}
const useSingleInferredProject = hasArgument("--useSingleInferredProject");
const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition");
const telemetryEnabled = hasArgument(Arguments.EnableTelemetry);
const ioSession = new IOSession(
sys,
cancellationToken,
eventPort,
/*canUseEvents*/ eventPort === undefined,
useSingleInferredProject,
disableAutomaticTypingAcquisition,
getGlobalTypingsCacheLocation(),
telemetryEnabled,
logger);
process.on("uncaughtException", function (err: Error) {
ioSession.logError(err, "unknown");
@@ -527,4 +639,4 @@ namespace ts.server {
(process as any).noAsar = true;
// Start listening
ioSession.listen();
}
}

View File

@@ -73,6 +73,10 @@ namespace ts.server {
project: Project;
}
export interface EventSender {
event(payload: any, eventName: string): void;
}
function allEditsBeforePos(edits: ts.TextChange[], pos: number) {
for (const edit of edits) {
if (textSpanEnd(edit.span) >= pos) {
@@ -165,7 +169,7 @@ namespace ts.server {
return `Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`;
}
export class Session {
export class Session implements EventSender {
private readonly gcTimer: GcTimer;
protected projectService: ProjectService;
private errorTimer: any; /*NodeJS.Timer | number*/
@@ -195,15 +199,23 @@ namespace ts.server {
private defaultEventHandler(event: ProjectServiceEvent) {
switch (event.eventName) {
case "context":
case ContextEvent:
const { project, fileName } = event.data;
this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`);
this.updateErrorCheck([{ fileName, project }], this.changeSeq,
(n) => n === this.changeSeq, 100);
break;
case "configFileDiag":
case ConfigFileDiagEvent:
const { triggerFile, configFileName, diagnostics } = event.data;
this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics);
break;
case ProjectLanguageServiceStateEvent:
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
this.event(<protocol.ProjectLanguageServiceStateEventBody>{
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled
}, eventName);
break;
}
}
@@ -389,9 +401,9 @@ namespace ts.server {
});
}
private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[], includeLinePosition: boolean) {
private getDiagnosticsWorker(args: protocol.FileRequestArgs, isSemantic: boolean, selector: (project: Project, file: string) => Diagnostic[], includeLinePosition: boolean) {
const { project, file } = this.getFileAndProject(args);
if (shouldSkipSematicCheck(project)) {
if (isSemantic && shouldSkipSematicCheck(project)) {
return [];
}
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
@@ -492,11 +504,11 @@ namespace ts.server {
}
private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs): protocol.Diagnostic[] | protocol.DiagnosticWithLinePosition[] {
return this.getDiagnosticsWorker(args, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), args.includeLinePosition);
return this.getDiagnosticsWorker(args, /*isSemantic*/ false, (project, file) => project.getLanguageService().getSyntacticDiagnostics(file), args.includeLinePosition);
}
private getSemanticDiagnosticsSync(args: protocol.SemanticDiagnosticsSyncRequestArgs): protocol.Diagnostic[] | protocol.DiagnosticWithLinePosition[] {
return this.getDiagnosticsWorker(args, (project, file) => project.getLanguageService().getSemanticDiagnostics(file), args.includeLinePosition);
return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSemanticDiagnostics(file), args.includeLinePosition);
}
private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): protocol.DocumentHighlightsItem[] | DocumentHighlights[] {
@@ -697,7 +709,7 @@ namespace ts.server {
const displayString = ts.displayPartsToString(nameInfo.displayParts);
const nameSpan = nameInfo.textSpan;
const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset;
const nameText = scriptInfo.snap().getText(nameSpan.start, ts.textSpanEnd(nameSpan));
const nameText = scriptInfo.getSnapshot().getText(nameSpan.start, ts.textSpanEnd(nameSpan));
const refs = combineProjectOutput<protocol.ReferencesResponseItem>(
projects,
(project: Project) => {
@@ -710,7 +722,7 @@ namespace ts.server {
const refScriptInfo = project.getScriptInfo(ref.fileName);
const start = refScriptInfo.positionToLineOffset(ref.textSpan.start);
const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1);
const lineText = refScriptInfo.snap().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, "");
const lineText = refScriptInfo.getSnapshot().getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, "");
return {
file: ref.fileName,
start: start,
@@ -761,7 +773,7 @@ namespace ts.server {
if (this.eventHander) {
this.eventHander({
eventName: "configFileDiag",
data: { fileName, configFileName, diagnostics: configFileErrors || [] }
data: { triggerFile: fileName, configFileName, diagnostics: configFileErrors || [] }
});
}
}
@@ -807,7 +819,7 @@ namespace ts.server {
private getIndentation(args: protocol.IndentationRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file));
const options = args.options || this.projectService.getFormatCodeOptions(file);
const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file);
const indentation = project.getLanguageService(/*ensureSynchronized*/ false).getIndentationAtPosition(file, position, options);
return { position, indentation };
}
@@ -874,19 +886,19 @@ namespace ts.server {
private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const options = args.options || this.projectService.getFormatCodeOptions(file);
const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file);
return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsForRange(file, args.position, args.endPosition, options);
}
private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const options = args.options || this.projectService.getFormatCodeOptions(file);
const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file);
return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsForDocument(file, options);
}
private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const options = args.options || this.projectService.getFormatCodeOptions(file);
const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file);
return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsAfterKeystroke(file, args.position, args.key, options);
}
@@ -967,7 +979,7 @@ namespace ts.server {
result.push({ name, kind, kindModifiers, sortText, replacementSpan: convertedSpan });
}
return result;
}, []).sort((a, b) => a.name.localeCompare(b.name));
}, []).sort((a, b) => ts.compareStrings(a.name, b.name));
}
else {
return completions;
@@ -1014,6 +1026,9 @@ namespace ts.server {
if (!project) {
Errors.ThrowNoProject();
}
if (!project.languageServiceEnabled) {
return false;
}
const scriptInfo = project.getScriptInfo(file);
return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark));
}
@@ -1076,11 +1091,12 @@ namespace ts.server {
private reload(args: protocol.ReloadRequestArgs, reqSeq: number) {
const file = toNormalizedPath(args.file);
const tempFileName = args.tmpfile && toNormalizedPath(args.tmpfile);
const project = this.projectService.getDefaultProjectForFile(file, /*refreshInferredProjects*/ true);
if (project) {
this.changeSeq++;
// make sure no changes happen before this one is finished
if (project.reloadScript(file)) {
if (project.reloadScript(file, tempFileName)) {
this.output(undefined, CommandNames.Reload, reqSeq);
}
}
@@ -1299,7 +1315,7 @@ namespace ts.server {
}
// No need to analyze lib.d.ts
let fileNamesInProject = fileNames.filter((value, index, array) => value.indexOf("lib.d.ts") < 0);
let fileNamesInProject = fileNames.filter(value => value.indexOf("lib.d.ts") < 0);
// Sort the file name list to make the recently touched files come first
const highPriorityFiles: NormalizedPath[] = [];
@@ -1313,7 +1329,7 @@ namespace ts.server {
highPriorityFiles.push(fileNameInProject);
else {
const info = this.projectService.getScriptInfo(fileNameInProject);
if (!info.isOpen) {
if (!info.isScriptOpen()) {
if (fileNameInProject.indexOf(".d.ts") > 0)
veryLowPriorityFiles.push(fileNameInProject);
else
@@ -1352,14 +1368,12 @@ namespace ts.server {
private handlers = createMap<(request: protocol.Request) => { response?: any, responseRequired?: boolean }>({
[CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => {
this.projectService.openExternalProject(request.arguments);
this.projectService.openExternalProject(request.arguments, /*suppressRefreshOfInferredProjects*/ false);
// TODO: report errors
return this.requiredResponse(true);
},
[CommandNames.OpenExternalProjects]: (request: protocol.OpenExternalProjectsRequest) => {
for (const proj of request.arguments.projects) {
this.projectService.openExternalProject(proj);
}
this.projectService.openExternalProjects(request.arguments.projects);
// TODO: report errors
return this.requiredResponse(true);
},
@@ -1426,24 +1440,8 @@ namespace ts.server {
[CommandNames.RenameInfoFull]: (request: protocol.FileLocationRequest) => {
return this.requiredResponse(this.getRenameInfo(request.arguments));
},
[CommandNames.Open]: (request: protocol.Request) => {
const openArgs = <protocol.OpenRequestArgs>request.arguments;
let scriptKind: ScriptKind;
switch (openArgs.scriptKindName) {
case "TS":
scriptKind = ScriptKind.TS;
break;
case "JS":
scriptKind = ScriptKind.JS;
break;
case "TSX":
scriptKind = ScriptKind.TSX;
break;
case "JSX":
scriptKind = ScriptKind.JSX;
break;
}
this.openClientFile(toNormalizedPath(openArgs.file), openArgs.fileContent, scriptKind);
[CommandNames.Open]: (request: protocol.OpenRequest) => {
this.openClientFile(toNormalizedPath(request.arguments.file), request.arguments.fileContent, convertScriptKindName(request.arguments.scriptKindName));
return this.notRequired();
},
[CommandNames.Quickinfo]: (request: protocol.QuickInfoRequest) => {
@@ -1515,7 +1513,7 @@ namespace ts.server {
[CommandNames.EncodedSemanticClassificationsFull]: (request: protocol.EncodedSemanticClassificationsRequest) => {
return this.requiredResponse(this.getEncodedSemanticClassifications(request.arguments));
},
[CommandNames.Cleanup]: (request: protocol.Request) => {
[CommandNames.Cleanup]: () => {
this.cleanup();
return this.requiredResponse(true);
},
@@ -1590,12 +1588,13 @@ namespace ts.server {
return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.CompilerOptionsForInferredProjects]: (request: protocol.SetCompilerOptionsForInferredProjectsRequest) => {
return this.requiredResponse(this.setCompilerOptionsForInferredProjects(request.arguments));
this.setCompilerOptionsForInferredProjects(request.arguments);
return this.requiredResponse(true);
},
[CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => {
return this.requiredResponse(this.getProjectInfo(request.arguments));
},
[CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => {
[CommandNames.ReloadProjects]: () => {
this.projectService.reloadProjects();
return this.notRequired();
},
@@ -1605,7 +1604,7 @@ namespace ts.server {
[CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => {
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.GetSupportedCodeFixes]: (request: protocol.Request) => {
[CommandNames.GetSupportedCodeFixes]: () => {
return this.requiredResponse(this.getSupportedCodeFixes());
}
});

25
src/server/shared.ts Normal file
View File

@@ -0,0 +1,25 @@
/// <reference path="types.d.ts" />
namespace ts.server {
export const ActionSet: ActionSet = "action::set";
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes";
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";
export namespace Arguments {
export const GlobalCacheLocation = "--globalTypingsCacheLocation";
export const LogFile = "--logFile";
export const EnableTelemetry = "--enableTelemetry";
}
export function hasArgument(argumentName: string) {
return sys.args.indexOf(argumentName) >= 0;
}
export function findArgument(argumentName: string) {
const index = sys.args.indexOf(argumentName);
return index >= 0 && index < sys.args.length - 1
? sys.args[index + 1]
: undefined;
}
}

View File

@@ -10,11 +10,15 @@
"stripInternal": true,
"types": [
"node"
]
],
"target": "es5",
"noUnusedLocals": true,
"noUnusedParameters": true
},
"files": [
"../services/shims.ts",
"../services/utilities.ts",
"shared.ts",
"utilities.ts",
"scriptVersionCache.ts",
"scriptInfo.ts",

View File

@@ -7,11 +7,15 @@
"sourceMap": true,
"stripInternal": true,
"declaration": true,
"types": []
"types": [],
"target": "es5",
"noUnusedLocals": true,
"noUnusedParameters": true
},
"files": [
"../services/shims.ts",
"../services/utilities.ts",
"shared.ts",
"utilities.ts",
"scriptVersionCache.ts",
"scriptInfo.ts",

48
src/server/types.d.ts vendored
View File

@@ -18,6 +18,10 @@ declare namespace ts.server {
trace?(s: string): void;
}
export interface SortedReadonlyArray<T> extends ReadonlyArray<T> {
" __sortedReadonlyArrayBrand": any;
}
export interface TypingInstallerRequest {
readonly projectName: string;
readonly kind: "discover" | "closeProject";
@@ -26,8 +30,9 @@ declare namespace ts.server {
export interface DiscoverTypings extends TypingInstallerRequest {
readonly fileNames: string[];
readonly projectRootPath: ts.Path;
readonly typingOptions: ts.TypingOptions;
readonly compilerOptions: ts.CompilerOptions;
readonly typeAcquisition: ts.TypeAcquisition;
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly cachePath?: string;
readonly kind: "discover";
}
@@ -36,25 +41,50 @@ declare namespace ts.server {
readonly kind: "closeProject";
}
export type ActionSet = "action::set";
export type ActionInvalidate = "action::invalidate";
export type EventBeginInstallTypes = "event::beginInstallTypes";
export type EventEndInstallTypes = "event::endInstallTypes";
export interface TypingInstallerResponse {
readonly projectName: string;
readonly kind: "set" | "invalidate";
readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes;
}
export interface SetTypings extends TypingInstallerResponse {
readonly typingOptions: ts.TypingOptions;
export interface ProjectResponse extends TypingInstallerResponse {
readonly projectName: string;
}
export interface SetTypings extends ProjectResponse {
readonly typeAcquisition: ts.TypeAcquisition;
readonly compilerOptions: ts.CompilerOptions;
readonly typings: string[];
readonly kind: "set";
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly kind: ActionSet;
}
export interface InvalidateCachedTypings extends TypingInstallerResponse {
readonly kind: "invalidate";
export interface InvalidateCachedTypings extends ProjectResponse {
readonly kind: ActionInvalidate;
}
export interface InstallTypes extends ProjectResponse {
readonly kind: EventBeginInstallTypes | EventEndInstallTypes;
readonly eventId: number;
readonly typingsInstallerVersion: string;
readonly packagesToInstall: ReadonlyArray<string>;
}
export interface BeginInstallTypes extends InstallTypes {
readonly kind: EventBeginInstallTypes;
}
export interface EndInstallTypes extends InstallTypes {
readonly kind: EventEndInstallTypes;
readonly installSuccess: boolean;
}
export interface InstallTypingHost extends JsTyping.TypingResolutionHost {
writeFile(path: string, content: string): void;
createDirectory(path: string): void;
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
}
}

View File

@@ -2,23 +2,25 @@
namespace ts.server {
export interface ITypingsInstaller {
enqueueInstallTypingsRequest(p: Project, typingOptions: TypingOptions): void;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string;
}
export const nullTypingsInstaller: ITypingsInstaller = {
enqueueInstallTypingsRequest: () => {},
attach: (projectService: ProjectService) => {},
onProjectClosed: (p: Project) => {},
enqueueInstallTypingsRequest: noop,
attach: noop,
onProjectClosed: noop,
globalTypingsCacheLocation: undefined
};
class TypingsCacheEntry {
readonly typingOptions: TypingOptions;
readonly typeAcquisition: TypeAcquisition;
readonly compilerOptions: CompilerOptions;
readonly typings: TypingsArray;
readonly typings: SortedReadonlyArray<string>;
readonly unresolvedImports: SortedReadonlyArray<string>;
/* mainly useful for debugging */
poisoned: boolean;
}
@@ -50,8 +52,8 @@ namespace ts.server {
return unique === 0;
}
function typingOptionsChanged(opt1: TypingOptions, opt2: TypingOptions): boolean {
return opt1.enableAutoDiscovery !== opt2.enableAutoDiscovery ||
function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean {
return opt1.enable !== opt2.enable ||
!setIsEqualTo(opt1.include, opt2.include) ||
!setIsEqualTo(opt1.exclude, opt2.exclude);
}
@@ -61,13 +63,11 @@ namespace ts.server {
return opt1.allowJs != opt2.allowJs;
}
export interface TypingsArray extends ReadonlyArray<string> {
" __typingsArrayBrand": any;
}
function toTypingsArray(arr: string[]): TypingsArray {
arr.sort();
return <any>arr;
function unresolvedImportsChanged(imports1: SortedReadonlyArray<string>, imports2: SortedReadonlyArray<string>): boolean {
if (imports1 === imports2) {
return false;
}
return !arrayIsEqualTo(imports1, imports2);
}
export class TypingsCache {
@@ -76,47 +76,49 @@ namespace ts.server {
constructor(private readonly installer: ITypingsInstaller) {
}
getTypingsForProject(project: Project, forceRefresh: boolean): TypingsArray {
const typingOptions = project.getTypingOptions();
getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray<string>, forceRefresh: boolean): SortedReadonlyArray<string> {
const typeAcquisition = project.getTypeAcquisition();
if (!typingOptions || !typingOptions.enableAutoDiscovery) {
if (!typeAcquisition || !typeAcquisition.enable) {
return <any>emptyArray;
}
const entry = this.perProjectCache[project.getProjectName()];
const result: TypingsArray = entry ? entry.typings : <any>emptyArray;
if (forceRefresh || !entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) {
const result: SortedReadonlyArray<string> = entry ? entry.typings : <any>emptyArray;
if (forceRefresh ||
!entry ||
typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) ||
compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions) ||
unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports)) {
// Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options.
// instead it acts as a placeholder to prevent issuing multiple requests
this.perProjectCache[project.getProjectName()] = {
compilerOptions: project.getCompilerOptions(),
typingOptions,
typeAcquisition,
typings: result,
unresolvedImports,
poisoned: true
};
// something has been changed, issue a request to update typings
this.installer.enqueueInstallTypingsRequest(project, typingOptions);
this.installer.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
}
return result;
}
invalidateCachedTypingsForProject(project: Project) {
const typingOptions = project.getTypingOptions();
if (!typingOptions.enableAutoDiscovery) {
return;
}
this.installer.enqueueInstallTypingsRequest(project, typingOptions);
}
updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typingOptions: TypingOptions, newTypings: string[]) {
updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, newTypings: string[]) {
this.perProjectCache[projectName] = {
compilerOptions,
typingOptions,
typings: toTypingsArray(newTypings),
typeAcquisition,
typings: toSortedReadonlyArray(newTypings),
unresolvedImports,
poisoned: false
};
}
deleteTypingsForProject(projectName: string) {
delete this.perProjectCache[projectName];
}
onProjectClosed(project: Project) {
delete this.perProjectCache[project.getProjectName()];
this.installer.onProjectClosed(project);

View File

@@ -33,27 +33,74 @@ namespace ts.server.typingsInstaller {
}
}
export class NodeTypingsInstaller extends TypingsInstaller {
private readonly exec: { (command: string, options: { cwd: string }, callback?: (error: Error, stdout: string, stderr: string) => void): any };
interface TypesRegistryFile {
entries: MapLike<void>;
}
readonly installTypingHost: InstallTypingHost = sys;
function loadTypesRegistryFile(typesRegistryFilePath: string, host: InstallTypingHost, log: Log): Map<void> {
if (!host.fileExists(typesRegistryFilePath)) {
if (log.isEnabled()) {
log.writeLine(`Types registry file '${typesRegistryFilePath}' does not exist`);
}
return createMap<void>();
}
try {
const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath));
return createMap<void>(content.entries);
}
catch (e) {
if (log.isEnabled()) {
log.writeLine(`Error when loading types registry file '${typesRegistryFilePath}': ${(<Error>e).message}, ${(<Error>e).stack}`);
}
return createMap<void>();
}
}
const TypesRegistryPackageName = "types-registry";
function getTypesRegistryFileLocation(globalTypingsCacheLocation: string): string {
return combinePaths(normalizeSlashes(globalTypingsCacheLocation), `node_modules/${TypesRegistryPackageName}/index.json`);
}
type ExecSync = {
(command: string, options: { cwd: string, stdio?: "ignore" }): any
}
export class NodeTypingsInstaller extends TypingsInstaller {
private readonly execSync: ExecSync;
private readonly npmPath: string;
readonly typesRegistry: Map<void>;
constructor(globalTypingsCacheLocation: string, throttleLimit: number, log: Log) {
super(
sys,
globalTypingsCacheLocation,
/*npmPath*/ getNPMLocation(process.argv[0]),
toPath("typingSafeList.json", __dirname, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
throttleLimit,
log);
if (this.log.isEnabled()) {
this.log.writeLine(`Process id: ${process.pid}`);
}
const { exec } = require("child_process");
this.exec = exec;
this.npmPath = getNPMLocation(process.argv[0]);
({ execSync: this.execSync } = require("child_process"));
this.ensurePackageDirectoryExists(globalTypingsCacheLocation);
try {
if (this.log.isEnabled()) {
this.log.writeLine(`Updating ${TypesRegistryPackageName} npm package...`);
}
this.execSync(`${this.npmPath} install ${TypesRegistryPackageName}`, { cwd: globalTypingsCacheLocation, stdio: "ignore" });
}
catch (e) {
if (this.log.isEnabled()) {
this.log.writeLine(`Error updating ${TypesRegistryPackageName} package: ${(<Error>e).message}`);
}
}
this.typesRegistry = loadTypesRegistryFile(getTypesRegistryFileLocation(globalTypingsCacheLocation), this.installTypingHost, this.log);
}
init() {
super.init();
listen() {
process.on("message", (req: DiscoverTypings | CloseProject) => {
switch (req.kind) {
case "discover":
@@ -65,7 +112,7 @@ namespace ts.server.typingsInstaller {
});
}
protected sendResponse(response: SetTypings | InvalidateCachedTypings) {
protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes) {
if (this.log.isEnabled()) {
this.log.writeLine(`Sending response: ${JSON.stringify(response)}`);
}
@@ -75,29 +122,33 @@ namespace ts.server.typingsInstaller {
}
}
protected runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void {
protected installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
if (this.log.isEnabled()) {
this.log.writeLine(`#${requestId} running command '${command}'.`);
this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(args)}'.`);
}
this.exec(command, { cwd }, (err, stdout, stderr) => {
if (this.log.isEnabled()) {
this.log.writeLine(`${requestKind} #${requestId} stdout: ${stdout}`);
this.log.writeLine(`${requestKind} #${requestId} stderr: ${stderr}`);
}
onRequestCompleted(err, stdout, stderr);
});
const command = `${this.npmPath} install ${args.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`;
const start = Date.now();
let stdout: Buffer;
let stderr: Buffer;
let hasError = false;
try {
stdout = this.execSync(command, { cwd });
}
catch (e) {
stdout = e.stdout;
stderr = e.stderr;
hasError = true;
}
if (this.log.isEnabled()) {
this.log.writeLine(`npm install #${requestId} took: ${Date.now() - start} ms${sys.newLine}stdout: ${stdout && stdout.toString()}${sys.newLine}stderr: ${stderr && stderr.toString()}`);
}
onRequestCompleted(!hasError);
}
}
function findArgument(argumentName: string) {
const index = sys.args.indexOf(argumentName);
return index >= 0 && index < sys.args.length - 1
? sys.args[index + 1]
: undefined;
}
const logFilePath = findArgument(server.Arguments.LogFile);
const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation);
const logFilePath = findArgument("--logFile");
const globalTypingsCacheLocation = findArgument("--globalTypingsCacheLocation");
const log = new FileLog(logFilePath);
if (log.isEnabled()) {
process.on("uncaughtException", (e: Error) => {
@@ -111,5 +162,5 @@ namespace ts.server.typingsInstaller {
process.exit(0);
});
const installer = new NodeTypingsInstaller(globalTypingsCacheLocation, /*throttleLimit*/5, log);
installer.init();
installer.listen();
}

View File

@@ -10,10 +10,14 @@
"stripInternal": true,
"types": [
"node"
]
],
"target": "es5",
"noUnusedLocals": true,
"noUnusedParameters": true
},
"files": [
"../types.d.ts",
"../shared.ts",
"typingsInstaller.ts",
"nodeTypingsInstaller.ts"
]

View File

@@ -2,6 +2,7 @@
/// <reference path="../../compiler/moduleNameResolver.ts" />
/// <reference path="../../services/jsTyping.ts"/>
/// <reference path="../types.d.ts"/>
/// <reference path="../shared.ts"/>
namespace ts.server.typingsInstaller {
interface NpmConfig {
@@ -15,17 +16,26 @@ namespace ts.server.typingsInstaller {
const nullLog: Log = {
isEnabled: () => false,
writeLine: () => {}
writeLine: noop
};
function typingToFileName(cachePath: string, packageName: string, installTypingHost: InstallTypingHost): string {
const result = resolveModuleName(packageName, combinePaths(cachePath, "index.d.ts"), { moduleResolution: ModuleResolutionKind.NodeJs }, installTypingHost);
return result.resolvedModule && result.resolvedModule.resolvedFileName;
function typingToFileName(cachePath: string, packageName: string, installTypingHost: InstallTypingHost, log: Log): string {
try {
const result = resolveModuleName(packageName, combinePaths(cachePath, "index.d.ts"), { moduleResolution: ModuleResolutionKind.NodeJs }, installTypingHost);
return result.resolvedModule && result.resolvedModule.resolvedFileName;
}
catch (e) {
if (log.isEnabled()) {
log.writeLine(`Failed to resolve ${packageName} in folder '${cachePath}': ${(<Error>e).message}`);
}
return undefined;
}
}
export enum PackageNameValidationResult {
Ok,
ScopedPackagesNotSupported,
EmptyName,
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
@@ -35,10 +45,12 @@ namespace ts.server.typingsInstaller {
export const MaxPackageNameLength = 214;
/**
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
*/
export function validatePackageName(packageName: string): PackageNameValidationResult {
Debug.assert(!!packageName, "Package name is not specified");
if (!packageName) {
return PackageNameValidationResult.EmptyName;
}
if (packageName.length > MaxPackageNameLength) {
return PackageNameValidationResult.NameTooLong;
}
@@ -60,18 +72,12 @@ namespace ts.server.typingsInstaller {
return PackageNameValidationResult.Ok;
}
export const NpmViewRequest: "npm view" = "npm view";
export const NpmInstallRequest: "npm install" = "npm install";
export type RequestKind = typeof NpmViewRequest | typeof NpmInstallRequest;
export type RequestCompletedAction = (err: Error, stdout: string, stderr: string) => void;
export type RequestCompletedAction = (success: boolean) => void;
type PendingRequest = {
requestKind: RequestKind;
requestId: number;
command: string;
args: string[];
cwd: string;
onRequestCompleted: RequestCompletedAction
onRequestCompleted: RequestCompletedAction;
};
export abstract class TypingsInstaller {
@@ -84,20 +90,17 @@ namespace ts.server.typingsInstaller {
private installRunCount = 1;
private inFlightRequestCount = 0;
abstract readonly installTypingHost: InstallTypingHost;
abstract readonly typesRegistry: Map<void>;
constructor(
readonly installTypingHost: InstallTypingHost,
readonly globalCachePath: string,
readonly npmPath: string,
readonly safeListPath: Path,
readonly throttleLimit: number,
protected readonly log = nullLog) {
if (this.log.isEnabled()) {
this.log.writeLine(`Global cache location '${globalCachePath}', safe file path '${safeListPath}'`);
}
}
init() {
this.processCacheLocation(this.globalCachePath);
}
@@ -132,7 +135,7 @@ namespace ts.server.typingsInstaller {
this.log.writeLine(`Got install request ${JSON.stringify(req)}`);
}
// load existing typing information from the cache
// load existing typing information from the cache
if (req.cachePath) {
if (this.log.isEnabled()) {
this.log.writeLine(`Request specifies cache path '${req.cachePath}', loading cached information...`);
@@ -146,8 +149,8 @@ namespace ts.server.typingsInstaller {
req.projectRootPath,
this.safeListPath,
this.packageNameToTypingLocation,
req.typingOptions,
req.compilerOptions);
req.typeAcquisition,
req.unresolvedImports);
if (this.log.isEnabled()) {
this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`);
@@ -196,8 +199,9 @@ namespace ts.server.typingsInstaller {
if (!packageName) {
continue;
}
const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost);
const typingFile = typingToFileName(cacheLocation, packageName, this.installTypingHost, this.log);
if (!typingFile) {
this.missingTypingsSet[packageName] = true;
continue;
}
const existingTypingFile = this.packageNameToTypingLocation[packageName];
@@ -228,18 +232,28 @@ namespace ts.server.typingsInstaller {
}
const result: string[] = [];
for (const typing of typingsToInstall) {
if (this.missingTypingsSet[typing]) {
if (this.missingTypingsSet[typing] || this.packageNameToTypingLocation[typing]) {
continue;
}
const validationResult = validatePackageName(typing);
if (validationResult === PackageNameValidationResult.Ok) {
result.push(typing);
if (typing in this.typesRegistry) {
result.push(typing);
}
else {
if (this.log.isEnabled()) {
this.log.writeLine(`Entry for package '${typing}' does not exist in local types registry - skipping...`);
}
}
}
else {
// add typing name to missing set so we won't process it again
this.missingTypingsSet[typing] = true;
if (this.log.isEnabled()) {
switch (validationResult) {
case PackageNameValidationResult.EmptyName:
this.log.writeLine(`Package name '${typing}' cannot be empty`);
break;
case PackageNameValidationResult.NameTooLong:
this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`);
break;
@@ -262,19 +276,8 @@ namespace ts.server.typingsInstaller {
return result;
}
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
if (this.log.isEnabled()) {
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
}
typingsToInstall = this.filterTypings(typingsToInstall);
if (typingsToInstall.length === 0) {
if (this.log.isEnabled()) {
this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`);
}
return;
}
const npmConfigPath = combinePaths(cachePath, "package.json");
protected ensurePackageDirectoryExists(directory: string) {
const npmConfigPath = combinePaths(directory, "package.json");
if (this.log.isEnabled()) {
this.log.writeLine(`Npm config file: ${npmConfigPath}`);
}
@@ -282,82 +285,82 @@ namespace ts.server.typingsInstaller {
if (this.log.isEnabled()) {
this.log.writeLine(`Npm config file: '${npmConfigPath}' is missing, creating new one...`);
}
this.ensureDirectoryExists(cachePath, this.installTypingHost);
this.ensureDirectoryExists(directory, this.installTypingHost);
this.installTypingHost.writeFile(npmConfigPath, "{}");
}
this.runInstall(cachePath, typingsToInstall, installedTypings => {
// TODO: watch project directory
if (this.log.isEnabled()) {
this.log.writeLine(`Requested to install typings ${JSON.stringify(typingsToInstall)}, installed typings ${JSON.stringify(installedTypings)}`);
}
const installedPackages: Map<true> = createMap<true>();
const installedTypingFiles: string[] = [];
for (const t of installedTypings) {
const packageName = getBaseFileName(t);
if (!packageName) {
continue;
}
installedPackages[packageName] = true;
const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost);
if (!typingFile) {
continue;
}
if (!this.packageNameToTypingLocation[packageName]) {
this.packageNameToTypingLocation[packageName] = typingFile;
}
installedTypingFiles.push(typingFile);
}
if (this.log.isEnabled()) {
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
}
for (const toInstall of typingsToInstall) {
if (!installedPackages[toInstall]) {
if (this.log.isEnabled()) {
this.log.writeLine(`New missing typing package '${toInstall}'`);
}
this.missingTypingsSet[toInstall] = true;
}
}
this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles)));
});
}
private runInstall(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void {
const requestId = this.installRunCount;
this.installRunCount++;
let execInstallCmdCount = 0;
const filteredTypings: string[] = [];
for (const typing of typingsToInstall) {
execNpmViewTyping(this, typing);
private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) {
if (this.log.isEnabled()) {
this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`);
}
function execNpmViewTyping(self: TypingsInstaller, typing: string) {
const command = `${self.npmPath} view @types/${typing} --silent name`;
self.execAsync(NpmViewRequest, requestId, command, cachePath, (err, stdout, stderr) => {
if (stdout) {
filteredTypings.push(typing);
}
execInstallCmdCount++;
if (execInstallCmdCount === typingsToInstall.length) {
installFilteredTypings(self, filteredTypings);
}
});
}
function installFilteredTypings(self: TypingsInstaller, filteredTypings: string[]) {
if (filteredTypings.length === 0) {
postInstallAction([]);
return;
const filteredTypings = this.filterTypings(typingsToInstall);
const scopedTypings = filteredTypings.map(x => `@types/${x}`);
if (scopedTypings.length === 0) {
if (this.log.isEnabled()) {
this.log.writeLine(`All typings are known to be missing or invalid - no need to go any further`);
}
const scopedTypings = filteredTypings.map(t => "@types/" + t);
const command = `${self.npmPath} install ${scopedTypings.join(" ")} --save-dev`;
self.execAsync(NpmInstallRequest, requestId, command, cachePath, (err, stdout, stderr) => {
postInstallAction(stdout ? scopedTypings : []);
});
return;
}
this.ensurePackageDirectoryExists(cachePath);
const requestId = this.installRunCount;
this.installRunCount++;
// send progress event
this.sendResponse(<BeginInstallTypes>{
kind: EventBeginInstallTypes,
eventId: requestId,
typingsInstallerVersion: ts.version, // qualified explicitly to prevent occasional shadowing
projectName: req.projectName
});
this.installTypingsAsync(requestId, scopedTypings, cachePath, ok => {
try {
if (!ok) {
if (this.log.isEnabled()) {
this.log.writeLine(`install request failed, marking packages as missing to prevent repeated requests: ${JSON.stringify(filteredTypings)}`);
}
for (const typing of filteredTypings) {
this.missingTypingsSet[typing] = true;
}
return;
}
// TODO: watch project directory
if (this.log.isEnabled()) {
this.log.writeLine(`Installed typings ${JSON.stringify(scopedTypings)}`);
}
const installedTypingFiles: string[] = [];
for (const packageName of filteredTypings) {
const typingFile = typingToFileName(cachePath, packageName, this.installTypingHost, this.log);
if (!typingFile) {
this.missingTypingsSet[packageName] = true;
continue;
}
if (!this.packageNameToTypingLocation[packageName]) {
this.packageNameToTypingLocation[packageName] = typingFile;
}
installedTypingFiles.push(typingFile);
}
if (this.log.isEnabled()) {
this.log.writeLine(`Installed typing files ${JSON.stringify(installedTypingFiles)}`);
}
this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles)));
}
finally {
this.sendResponse(<EndInstallTypes>{
kind: EventEndInstallTypes,
eventId: requestId,
projectName: req.projectName,
packagesToInstall: scopedTypings,
installSuccess: ok,
typingsInstallerVersion: ts.version // qualified explicitly to prevent occasional shadowing
});
}
});
}
private ensureDirectoryExists(directory: string, host: InstallTypingHost): void {
@@ -385,9 +388,11 @@ namespace ts.server.typingsInstaller {
if (this.log.isEnabled()) {
this.log.writeLine(`Got FS notification for ${f}, handler is already invoked '${isInvoked}'`);
}
this.sendResponse({ projectName: projectName, kind: "invalidate" });
isInvoked = true;
});
if (!isInvoked) {
this.sendResponse({ projectName: projectName, kind: server.ActionInvalidate });
isInvoked = true;
}
}, /*pollingInterval*/ 2000);
watchers.push(w);
}
this.projectWatchers[projectName] = watchers;
@@ -396,15 +401,16 @@ namespace ts.server.typingsInstaller {
private createSetTypings(request: DiscoverTypings, typings: string[]): SetTypings {
return {
projectName: request.projectName,
typingOptions: request.typingOptions,
typeAcquisition: request.typeAcquisition,
compilerOptions: request.compilerOptions,
typings,
kind: "set"
unresolvedImports: request.unresolvedImports,
kind: ActionSet
};
}
private execAsync(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void {
this.pendingRunRequests.unshift({ requestKind, requestId, command, cwd, onRequestCompleted });
private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted });
this.executeWithThrottling();
}
@@ -412,15 +418,15 @@ namespace ts.server.typingsInstaller {
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
this.inFlightRequestCount++;
const request = this.pendingRunRequests.pop();
this.runCommand(request.requestKind, request.requestId, request.command, request.cwd, (err, stdout, stderr) => {
this.installWorker(request.requestId, request.args, request.cwd, ok => {
this.inFlightRequestCount--;
request.onRequestCompleted(err, stdout, stderr);
request.onRequestCompleted(ok);
this.executeWithThrottling();
});
}
}
protected abstract runCommand(requestKind: RequestKind, requestId: number, command: string, cwd: string, onRequestCompleted: RequestCompletedAction): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void;
protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void;
}
}

View File

@@ -1,4 +1,5 @@
/// <reference path="types.d.ts" />
/// <reference path="types.d.ts" />
/// <reference path="shared.ts" />
namespace ts.server {
export enum LogLevel {
@@ -45,12 +46,13 @@ namespace ts.server {
}
}
export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, cachePath?: string): DiscoverTypings {
export function createInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, cachePath?: string): DiscoverTypings {
return {
projectName: project.getProjectName(),
fileNames: project.getFileNames(),
fileNames: project.getFileNames(/*excludeFilesFromExternalLibraries*/ true),
compilerOptions: project.getCompilerOptions(),
typingOptions,
typeAcquisition,
unresolvedImports,
projectRootPath: getProjectRootPath(project),
cachePath,
kind: "discover"
@@ -76,6 +78,7 @@ namespace ts.server {
newLineCharacter: host.newLine || "\n",
convertTabsToSpaces: true,
indentStyle: ts.IndentStyle.Smart,
insertSpaceAfterConstructor: false,
insertSpaceAfterCommaDelimiter: true,
insertSpaceAfterSemicolonInForStatements: true,
insertSpaceBeforeAndAfterBinaryOperators: true,
@@ -83,8 +86,10 @@ namespace ts.server {
insertSpaceAfterFunctionKeywordForAnonymousFunctions: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false,
insertSpaceBeforeFunctionParenthesis: false,
placeOpenBraceOnNewLineForFunctions: false,
placeOpenBraceOnNewLineForControlBlocks: false,
};
@@ -157,64 +162,6 @@ namespace ts.server {
}
};
}
function throwLanguageServiceIsDisabledError(): never {
throw new Error("LanguageService is disabled");
}
export const nullLanguageService: LanguageService = {
cleanupSemanticCache: throwLanguageServiceIsDisabledError,
getSyntacticDiagnostics: throwLanguageServiceIsDisabledError,
getSemanticDiagnostics: throwLanguageServiceIsDisabledError,
getCompilerOptionsDiagnostics: throwLanguageServiceIsDisabledError,
getSyntacticClassifications: throwLanguageServiceIsDisabledError,
getEncodedSyntacticClassifications: throwLanguageServiceIsDisabledError,
getSemanticClassifications: throwLanguageServiceIsDisabledError,
getEncodedSemanticClassifications: throwLanguageServiceIsDisabledError,
getCompletionsAtPosition: throwLanguageServiceIsDisabledError,
findReferences: throwLanguageServiceIsDisabledError,
getCompletionEntryDetails: throwLanguageServiceIsDisabledError,
getQuickInfoAtPosition: throwLanguageServiceIsDisabledError,
findRenameLocations: throwLanguageServiceIsDisabledError,
getNameOrDottedNameSpan: throwLanguageServiceIsDisabledError,
getBreakpointStatementAtPosition: throwLanguageServiceIsDisabledError,
getBraceMatchingAtPosition: throwLanguageServiceIsDisabledError,
getSignatureHelpItems: throwLanguageServiceIsDisabledError,
getDefinitionAtPosition: throwLanguageServiceIsDisabledError,
getRenameInfo: throwLanguageServiceIsDisabledError,
getTypeDefinitionAtPosition: throwLanguageServiceIsDisabledError,
getReferencesAtPosition: throwLanguageServiceIsDisabledError,
getDocumentHighlights: throwLanguageServiceIsDisabledError,
getOccurrencesAtPosition: throwLanguageServiceIsDisabledError,
getNavigateToItems: throwLanguageServiceIsDisabledError,
getNavigationBarItems: throwLanguageServiceIsDisabledError,
getNavigationTree: throwLanguageServiceIsDisabledError,
getOutliningSpans: throwLanguageServiceIsDisabledError,
getTodoComments: throwLanguageServiceIsDisabledError,
getIndentationAtPosition: throwLanguageServiceIsDisabledError,
getFormattingEditsForRange: throwLanguageServiceIsDisabledError,
getFormattingEditsForDocument: throwLanguageServiceIsDisabledError,
getFormattingEditsAfterKeystroke: throwLanguageServiceIsDisabledError,
getDocCommentTemplateAtPosition: throwLanguageServiceIsDisabledError,
isValidBraceCompletionAtPosition: throwLanguageServiceIsDisabledError,
getEmitOutput: throwLanguageServiceIsDisabledError,
getProgram: throwLanguageServiceIsDisabledError,
getNonBoundSourceFile: throwLanguageServiceIsDisabledError,
dispose: throwLanguageServiceIsDisabledError,
getCompletionEntrySymbol: throwLanguageServiceIsDisabledError,
getImplementationAtPosition: throwLanguageServiceIsDisabledError,
getSourceFile: throwLanguageServiceIsDisabledError,
getCodeFixesAtPosition: throwLanguageServiceIsDisabledError
};
export interface ServerLanguageServiceHost {
setCompilationSettings(options: CompilerOptions): void;
notifyFileRemoved(info: ScriptInfo): void;
}
export const nullLanguageServiceHost: ServerLanguageServiceHost = {
setCompilationSettings: () => undefined,
notifyFileRemoved: () => undefined
};
export interface ProjectOptions {
/**
@@ -227,7 +174,7 @@ namespace ts.server {
files?: string[];
wildcardDirectories?: Map<WatchDirectoryFlags>;
compilerOptions?: CompilerOptions;
typingOptions?: TypingOptions;
typeAcquisition?: TypeAcquisition;
compileOnSave?: boolean;
}
@@ -240,6 +187,11 @@ namespace ts.server {
return `/dev/null/inferredProject${counter}*`;
}
export function toSortedReadonlyArray(arr: string[]): SortedReadonlyArray<string> {
arr.sort();
return <any>arr;
}
export class ThrottledOperations {
private pendingTimeouts: Map<any> = createMap<any>();
constructor(private readonly host: ServerHost) {