[in progress] project system work

This commit is contained in:
Vladimir Matveev 2016-06-21 17:31:54 -07:00
parent 9763dc825e
commit c9b82eddda
9 changed files with 1141 additions and 1054 deletions

View File

@ -101,7 +101,11 @@ var servicesSources = [
var serverCoreSources = [
"node.d.ts",
"utilities.ts",
"scriptVersionCache.ts",
"scriptInfo.ts",
"lsHost.ts",
"project.ts",
"editorServices.ts",
"protocol.d.ts",
"session.ts",

File diff suppressed because it is too large Load Diff

160
src/server/lshost.ts Normal file
View File

@ -0,0 +1,160 @@
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts" />
/// <reference path="scriptInfo.ts" />
namespace ts.server {
export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost, ServerLanguageServiceHost {
private compilationSettings: ts.CompilerOptions;
private readonly resolvedModuleNames: ts.FileMap<Map<ResolvedModuleWithFailedLookupLocations>>;
private readonly resolvedTypeReferenceDirectives: ts.FileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>;
private readonly getCanonicalFileName: (fileName: 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>>();
}
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[] {
const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const currentResolutionsInFile = cache.get(path);
const newResolutions: Map<T> = {};
const resolvedModules: R[] = [];
const compilerOptions = this.getCompilationSettings();
for (const name of names) {
// check if this is a duplicate entry in the list
let resolution = lookUp(newResolutions, name);
if (!resolution) {
const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, name);
if (moduleResolutionIsValid(existingResolution)) {
// ok, it is safe to use existing name resolution results
resolution = existingResolution;
}
else {
newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this);
}
}
ts.Debug.assert(resolution !== undefined);
resolvedModules.push(getResult(resolution));
}
// replace old results with a new one
cache.set(path, newResolutions);
return resolvedModules;
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution) {
return false;
}
if (getResult(resolution)) {
// TODO: consider checking failedLookupLocations
return true;
}
// consider situation if we have no candidate locations as valid resolution.
// after all there is no point to invalidate it if we have no idea where to look for the module.
return resolution.failedLookupLocations.length === 0;
}
}
getProjectVersion() {
return this.project.getProjectVersion();
}
getCancellationToken() {
return this.cancellationToken;
}
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective);
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule);
}
getDefaultLibFileName() {
const nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath()));
return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings));
}
getScriptSnapshot(filename: string): ts.IScriptSnapshot {
const scriptInfo = this.project.getScriptInfo(filename);
if (scriptInfo) {
return scriptInfo.snap();
}
}
setCompilationSettings(opt: ts.CompilerOptions) {
this.compilationSettings = opt;
// conservatively assume that changing compiler options might affect module resolution strategy
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
getCompilationSettings() {
// change this to return active project settings for file
return this.compilationSettings;
}
getScriptFileNames() {
return this.project.getRootFiles();
}
getScriptKind(fileName: string) {
const info = this.project.getScriptInfo(fileName);
return info && info.scriptKind;
}
getScriptVersion(filename: string) {
return this.project.getScriptInfo(filename).getLatestVersion();
}
getCurrentDirectory(): string {
return "";
}
removeReferencedFile(info: ScriptInfo) {
if (!info.isOpen) {
this.resolvedModuleNames.remove(info.path);
this.resolvedTypeReferenceDirectives.remove(info.path);
}
}
removeRoot(info: ScriptInfo) {
this.resolvedModuleNames.remove(info.path);
this.resolvedTypeReferenceDirectives.remove(info.path);
}
resolvePath(path: string): string {
return this.host.resolvePath(path);
}
fileExists(path: string): boolean {
return this.host.fileExists(path);
}
directoryExists(path: string): boolean {
return this.host.directoryExists(path);
}
readFile(fileName: string): string {
return this.host.readFile(fileName);
}
getDirectories(path: string): string[] {
return this.host.getDirectories(path);
}
}
}

396
src/server/project.ts Normal file
View File

@ -0,0 +1,396 @@
/// <reference path="..\services\services.ts" />
/// <reference path="scriptInfo.ts"/>
/// <reference path="lshost.ts"/>
namespace ts.server {
export enum ProjectKind {
Inferred,
Configured,
External
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
protected program: ts.Program;
private version = 0;
languageService: LanguageService;
constructor(
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
private compilerOptions: CompilerOptions) {
if (!this.compilerOptions) {
this.compilerOptions = ts.getDefaultCompilerOptions();
this.compilerOptions.allowNonTsExtensions = true;
this.compilerOptions.allowJs = true;
}
else if (hasExplicitListOfFiles) {
// If files are listed explicitly, allow all extensions
this.compilerOptions.allowNonTsExtensions = true;
}
if (languageServiceEnabled) {
this.enableLanguageService();
}
else {
this.disableLanguageService();
}
this.markAsDirty();
}
getProjectVersion() {
return this.version.toString();
}
enableLanguageService() {
const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
lsHost.setCompilationSettings(this.compilerOptions);
this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
this.lsHost = lsHost;
this.languageServiceEnabled = true;
}
disableLanguageService() {
this.languageService = nullLanguageService;
this.lsHost = nullLanguageServiceHost;
this.languageServiceEnabled = false;
}
abstract getProjectName(): string;
close() {
for (const fileName of this.getFileNames()) {
const info = this.projectKind.getScriptInfoForNormalizedPath(fileName);
info.detachFromProject(project);
}
// signal language service to release files acquired from document registry
this.languageService.dispose();
}
getCompilerOptions() {
return this.compilerOptions;
}
getRootFiles() {
return this.rootFiles.map(info => info.fileName);
}
getFileNames() {
if (!this.languageServiceEnabled) {
// if language service is disabled assume that all files in program are root files + default library
let rootFiles = this.getRootFiles();
if (this.compilerOptions) {
const defaultLibrary = getDefaultLibFilePath(this.compilerOptions);
if (defaultLibrary) {
(rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary));
}
}
return rootFiles;
}
const sourceFiles = this.program.getSourceFiles();
return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
}
containsScriptInfo(info: ScriptInfo): boolean {
return this.program && this.program.getSourceFileByPath(info.path) !== undefined;
}
containsFile(filename: NormalizedPath, requireOpen?: boolean) {
const info = this.projectService.getScriptInfoForNormalizedPath(filename);
if (info) {
if ((!requireOpen) || info.isOpen) {
return this.containsScriptInfo(info);
}
}
}
isRoot(info: ScriptInfo) {
return this.rootFilesMap.contains(info.path);
}
// add a root file to project
addRoot(info: ScriptInfo) {
if (!this.isRoot(info)) {
this.rootFiles.push(info);
this.rootFilesMap.set(info.path, info);
info.attachToProject(this);
this.markAsDirty();
}
}
removeFile(info: ScriptInfo) {
if (!this.removeRoot(info)) {
this.removeReferencedFile(info)
}
info.detachFromProject(this);
this.markAsDirty();
}
markAsDirty() {
this.version++;
}
// remove a root file from project
private removeRoot(info: ScriptInfo): boolean {
if (this.isRoot(info)) {
this.rootFiles = copyListRemovingItem(info, this.rootFiles);
this.rootFilesMap.remove(info.path);
this.lsHost.removeRoot(info);
return true;
}
return false;
}
private removeReferencedFile(info: ScriptInfo) {
this.lsHost.removeReferencedFile(info)
this.updateGraph();
}
updateGraph() {
this.program = this.languageService.getProgram();
}
getScriptInfo(uncheckedFileName: string) {
const scriptInfo = this.projectService.getOrCreateScriptInfo(toNormalizedPath(uncheckedFileName), /*openedByClient*/ false);
if (scriptInfo.attachToProject(this)) {
this.markAsDirty();
}
return scriptInfo;
}
filesToString() {
if (!this.program) {
return "";
}
let strBuilder = "";
for (const file of this.program.getSourceFiles()) {
strBuilder += `${file.fileName}\n`;
}
return strBuilder;
}
setCompilerOptions(compilerOptions: CompilerOptions) {
if (compilerOptions) {
compilerOptions.allowNonTsExtensions = true;
this.compilerOptions = compilerOptions;
this.lsHost.setCompilationSettings(compilerOptions);
this.markAsDirty();
}
}
saveTo(filename: string, tmpfilename: string) {
const script = this.getScriptInfo(filename);
if (script) {
const snap = script.snap();
this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength()));
}
}
reloadScript(filename: string, tmpfilename: string, cb: () => void) {
const script = this.getScriptInfo(filename);
if (script) {
script.reloadFromFile(filename, cb);
}
}
}
export class InferredProject extends Project {
static NextId = 0;
readonly inferredProjectName;
// Used to keep track of what directories are watched for this project
directoriesWatchedForTsconfig: string[] = [];
constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean) {
super(ProjectKind.Inferred,
projectService,
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
/*compilerOptions*/ undefined);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId++);
}
getProjectName() {
return this.inferredProjectName;
}
close() {
super.close();
for (const directory of this.directoriesWatchedForTsconfig) {
this.projectService.stopWatchingDirectory(directory);
}
}
}
export abstract class VersionedProject extends Project {
private lastReportedFileNames: Map<string>;
private lastReportedVersion: number = 0;
currentVersion: number = 1;
updateGraph() {
if (!this.languageServiceEnabled) {
return;
}
const oldProgram = this.program;
super.updateGraph();
if (!oldProgram || !oldProgram.structureIsReused) {
this.currentVersion++;
}
}
getChangesSinceVersion(lastKnownVersion?: number): protocol.ExternalProjectFiles {
const info = {
projectName: this.getProjectName(),
version: this.currentVersion
};
if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
if (this.currentVersion == this.lastReportedVersion) {
return { info };
}
const lastReportedFileNames = this.lastReportedFileNames;
const currentFiles = arrayToMap(this.getFileNames(), x => x);
const added: string[] = [];
const removed: string[] = [];
for (const id in currentFiles) {
if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) {
added.push(id);
}
}
for (const id in lastReportedFileNames) {
if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) {
removed.push(id);
}
}
this.lastReportedFileNames = currentFiles;
this.lastReportedFileNames = currentFiles;
this.lastReportedVersion = this.currentVersion;
return { info, changes: { added, removed } };
}
else {
// unknown version - return everything
const projectFileNames = this.getFileNames();
this.lastReportedFileNames = arrayToMap(projectFileNames, x => x);
this.lastReportedVersion = this.currentVersion;
return { info, files: projectFileNames };
}
}
}
export class ConfiguredProject extends VersionedProject {
private projectFileWatcher: FileWatcher;
private directoryWatcher: FileWatcher;
private directoriesWatchedForWildcards: Map<FileWatcher>;
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(readonly configFileName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
compilerOptions: CompilerOptions,
private wildcardDirectories: Map<WatchDirectoryFlags>,
languageServiceEnabled: boolean) {
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions);
}
getProjectName() {
return this.configFileName;
}
watchConfigFile(callback: (project: ConfiguredProject) => void) {
this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this));
}
watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) {
if (this.directoryWatcher) {
return;
}
const directoryToWatch = getDirectoryPath(this.configFileName);
this.projectService.log(`Add recursive watcher for: ${directoryToWatch}`);
this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true);
}
watchWildcards(callback: (project: ConfiguredProject, path: string) => void) {
if (!this.wildcardDirectories) {
return;
}
const configDirectoryPath = getDirectoryPath(this.configFileName);
this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => {
if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
this.projectService.log(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`);
watchers[directory] = this.projectService.host.watchDirectory(
directory,
path => callback(this, path),
recursive
);
}
return watchers;
}, <Map<FileWatcher>>{});
}
stopWatchingDirectory() {
if (this.directoryWatcher) {
this.directoryWatcher.close();
this.directoryWatcher = undefined;
}
}
close() {
super.close();
if (this.projectFileWatcher) {
this.projectFileWatcher.close();
}
forEachValue(this.directoriesWatchedForWildcards, watcher => { watcher.close(); });
this.directoriesWatchedForWildcards = undefined;
this.stopWatchingDirectory();
}
addOpenRef() {
this.openRefCount++;
}
deleteOpenRef() {
this.openRefCount--;
return this.openRefCount;
}
}
export class ExternalProject extends VersionedProject {
constructor(readonly externalProjectName: string,
projectService: ProjectService,
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
languageServiceEnabled: boolean) {
super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions);
}
getProjectName() {
return this.externalProjectName;
}
}
}

View File

@ -495,7 +495,7 @@ declare namespace ts.server.protocol {
}
export interface ExternalProjectInfo {
projectFileName: string;
projectName: string;
version: number;
}

151
src/server/scriptInfo.ts Normal file
View File

@ -0,0 +1,151 @@
/// <reference path="scriptVersionCache.ts"/>
namespace ts.server {
export class ScriptInfo {
private svc: ScriptVersionCache;
/**
* All projects that include this file
*/
readonly containingProjects: Project[] = [];
private fileWatcher: FileWatcher;
formatCodeSettings: ts.FormatCodeSettings;
readonly path: Path;
constructor(
private readonly host: ServerHost,
readonly fileName: NormalizedPath,
content: string,
readonly scriptKind: ScriptKind,
public isOpen = false) {
this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames));
this.svc = ScriptVersionCache.fromString(host, content);
this.formatCodeSettings = getDefaultFormatCodeSettings(this.host);
this.scriptKind = scriptKind && scriptKind !== ScriptKind.Unknown
? scriptKind
: getScriptKindFromFileName(fileName);
}
attachToProject(project: Project): boolean {
if (!contains(this.containingProjects, project)) {
this.containingProjects.push(project);
return true;
}
return false;
}
detachFromProject(project: Project) {
const index = this.containingProjects.indexOf(project);
if (index < 0) {
// TODO: (assert?) attempt to detach file from project that didn't include this file
return;
}
removeItemFromSet(this.containingProjects, project);
}
detachAllProjects() {
for (const p of this.containingProjects) {
p.removeFile(this);
}
this.containingProjects.length = 0;
}
getDefaultProject() {
Debug.assert(this.containingProjects.length !== 0);
return this.containingProjects[0];
}
setFormatOptions(formatSettings: protocol.FormatOptions): void {
if (formatSettings) {
mergeMaps(this.formatCodeSettings, formatSettings);
}
}
setWatcher(watcher: FileWatcher): void {
this.stopWatcher();
this.fileWatcher = watcher;
}
stopWatcher() {
if (this.fileWatcher) {
this.fileWatcher.close();
this.fileWatcher = undefined;
}
}
getLatestVersion() {
return this.svc.latestVersion().toString();
}
reload(script: string) {
this.svc.reload(script);
this.markContainingProjectsAsDirty();
}
reloadFromFile(fileName: string, cb?: () => void) {
this.svc.reloadFromFile(fileName, cb)
this.markContainingProjectsAsDirty();
}
snap() {
return this.svc.getSnapshot();
}
getLineInfo(line: number) {
const snap = this.snap();
return snap.index.lineNumberToInfo(line);
}
editContent(start: number, end: number, newText: string): void {
this.svc.edit(start, end - start, newText);
this.markContainingProjectsAsDirty();
}
markContainingProjectsAsDirty() {
for (const p of this.containingProjects) {
p.markAsDirty();
}
}
/**
* @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);
}
/**
* @param line 1 based index
* @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);
}
/**
* @param line 1-based index
* @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 };
}
}
}

View File

@ -297,7 +297,7 @@ namespace ts.server {
return this.currentVersion;
}
reloadFromFile(filename: string, cb?: () => any) {
reloadFromFile(filename: string, cb?: () => void) {
let content = this.host.readFile(filename);
// If the file doesn't exist or cannot be read, we should
// wipe out its cached content on the server to avoid side effects.

View File

@ -86,7 +86,7 @@ namespace ts.server {
}
export interface PendingErrorCheck {
fileName: string;
fileName: NormalizedPath;
project: Project;
}
@ -187,7 +187,7 @@ namespace ts.server {
});
}
private handleEvent(eventName: string, project: Project, fileName: string) {
private handleEvent(eventName: string, project: Project, fileName: NormalizedPath) {
if (eventName == "context") {
this.projectService.log("got context event, updating diagnostics for" + fileName, "Info");
this.updateErrorCheck([{ fileName, project }], this.changeSeq,
@ -370,16 +370,12 @@ namespace ts.server {
}
private getEncodedSemanticClassifications(args: protocol.FileSpanRequestArgs) {
const file = normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
return project.languageService.getEncodedSemanticClassifications(file, args);
}
private getProject(projectFileName: string) {
return projectFileName && this.projectService.getProject(projectFileName);
return projectFileName && this.projectService.findProject(projectFileName);
}
private getCompilerOptionsDiagnostics(args: protocol.ProjectRequestArgs) {
@ -400,11 +396,7 @@ namespace ts.server {
}
private getDiagnosticsWorker(args: protocol.FileRequestArgs, selector: (project: Project, file: string) => Diagnostic[]) {
const file = normalizePath(args.file);
const project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { project, file } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const diagnostics = selector(project, file);
return this.convertDiagnostics(diagnostics, scriptInfo);
@ -419,11 +411,7 @@ namespace ts.server {
}
private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.FileSpan[] | DefinitionInfo[] {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const position = this.getPosition(args, scriptInfo);
@ -780,9 +768,9 @@ namespace ts.server {
return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset);
}
private getFileAndProject(fileName: string) {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
private getFileAndProject(args: protocol.FileLocationRequestArgs) {
const file = ts.normalizePath(args.file);
const project: Project = this.getProject(args.projectFileName) || this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
@ -790,54 +778,49 @@ namespace ts.server {
}
private getOutliningSpans(args: protocol.FileRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
return project.languageService.getOutliningSpans(file);
}
private getTodoComments(args: protocol.TodoCommentRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
return project.languageService.getTodoComments(file, args.descriptors);
}
private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const position = this.getPosition(args, scriptInfo);
return project.languageService.getDocCommentTemplateAtPosition(file, position);
}
private getIndentation(args: protocol.IndentationRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
const position = this.getPosition(args, project.getScriptInfo(file));
const indentation = project.languageService.getIndentationAtPosition(file, position, args.options);
return { position, indentation };
}
private getBreakpointStatement(args: protocol.FileLocationRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
const position = this.getPosition(args, project.getScriptInfo(file));
return project.languageService.getBreakpointStatementAtPosition(file, position);
}
private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
const position = this.getPosition(args, project.getScriptInfo(file));
return project.languageService.getNameOrDottedNameSpan(file, position, position);
}
private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) {
const { file, project } = this.getFileAndProject(args.file);
const { file, project } = this.getFileAndProject(args);
const position = this.getPosition(args, project.getScriptInfo(file));
return project.languageService.isValidBraceCompletionAtPostion(file, position, args.openingBrace.charCodeAt(0));
}
private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const quickInfo = project.languageService.getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo));
if (!quickInfo) {
@ -889,31 +872,17 @@ namespace ts.server {
}
private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
return project.languageService.getFormattingEditsForRange(file, args.position, args.endPosition, args.options);
}
private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
return project.languageService.getFormattingEditsForDocument(file, args.options);
}
private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
return project.languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, args.options);
}
@ -990,11 +959,7 @@ namespace ts.server {
private getCompletions(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): protocol.CompletionEntry[] | CompletionInfo {
const prefix = args.prefix || "";
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const position = this.getPosition(args, scriptInfo);
@ -1017,12 +982,7 @@ namespace ts.server {
}
private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): protocol.CompletionEntryDetails[] {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const position = this.getPosition(args, scriptInfo);
@ -1036,12 +996,7 @@ namespace ts.server {
}
private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems {
const file = ts.normalizePath(args.file);
const project = this.projectService.getProjectForFile(file);
if (!project) {
throw Errors.NoProject;
}
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfo(file);
const position = this.getPosition(args, scriptInfo);
const helpItems = project.languageService.getSignatureHelpItems(file, position);
@ -1069,6 +1024,7 @@ namespace ts.server {
private getDiagnostics(delay: number, fileNames: string[]) {
const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
if (project) {

175
src/server/utilities.ts Normal file
View File

@ -0,0 +1,175 @@
/// <reference path="..\services\services.ts" />
/// <reference path="session.ts" />
namespace ts.server {
export interface Logger {
close(): void;
isVerbose(): boolean;
loggingEnabled(): boolean;
perftrc(s: string): void;
info(s: string): void;
startGroup(): void;
endGroup(): void;
msg(s: string, type?: string): void;
}
export function getDefaultFormatCodeSettings(host: ServerHost): FormatCodeSettings {
return {
indentSize: 4,
tabSize: 4,
newLineCharacter: host.newLine || "\n",
convertTabsToSpaces: true,
indentStyle: ts.IndentStyle.Smart,
insertSpaceAfterCommaDelimiter: true,
insertSpaceAfterSemicolonInForStatements: true,
insertSpaceBeforeAndAfterBinaryOperators: true,
insertSpaceAfterKeywordsInControlFlowStatements: true,
insertSpaceAfterFunctionKeywordForAnonymousFunctions: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
placeOpenBraceOnNewLineForFunctions: false,
placeOpenBraceOnNewLineForControlBlocks: false,
};
}
export function mergeMaps(target: Map<any>, source: Map<any>): void {
for (const key in source) {
if (hasProperty(source, key)) {
target[key] = source[key];
}
}
}
export function removeItemFromSet<T>(items: T[], itemToRemove: T) {
const index = items.indexOf(itemToRemove);
if (index < 0) {
return;
}
if (items.length === 0) {
items.pop();
}
else {
items[index] = items.pop();
}
}
export type NormalizedPath = string & { __normalizedPathTag: any };
export function toNormalizedPath(fileName: string): NormalizedPath {
return <NormalizedPath>normalizePath(fileName);
}
export function asNormalizedPath(fileName: string): NormalizedPath {
return <NormalizedPath>fileName;
}
export function asNormalizedPathArray(fileNames: string[]): NormalizedPath[] {
return <NormalizedPath[]>fileNames;
}
export interface NormalizedPathMap<T> {
get (path: NormalizedPath): T;
set (path: NormalizedPath, value: T): void;
contains(path: NormalizedPath): boolean;
remove(path: NormalizedPath): void;
}
export function createNormalizedPathMap<T>(): NormalizedPathMap<T> {
const map: Map<T> = Object.create(null);
return {
get(path) {
return map[path];
},
set(path, value) {
map[path] = value;
},
contains(path) {
return hasProperty(map, path);
},
remove(path) {
delete map[path]
}
}
}
function throwLanguageServiceIsDisabledError() {;
throw new Error("LanguageService is disabled");
}
export const nullLanguageService: LanguageService = {
cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(),
getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(),
getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(),
getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(),
getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(),
getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(),
getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(),
getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(),
getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(),
findReferences: (): any => throwLanguageServiceIsDisabledError(),
getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(),
getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(),
findRenameLocations: (): any => throwLanguageServiceIsDisabledError(),
getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(),
getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(),
getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getRenameInfo: (): any => throwLanguageServiceIsDisabledError(),
getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(),
getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(),
getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(),
getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(),
getTodoComments: (): any => throwLanguageServiceIsDisabledError(),
getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(),
getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(),
getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(),
getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(),
getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(),
isValidBraceCompletionAtPostion: (): any => throwLanguageServiceIsDisabledError(),
getEmitOutput: (): any => throwLanguageServiceIsDisabledError(),
getProgram: (): any => throwLanguageServiceIsDisabledError(),
getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(),
dispose: (): any => throwLanguageServiceIsDisabledError(),
};
export interface ServerLanguageServiceHost {
getCompilationSettings(): CompilerOptions;
setCompilationSettings(options: CompilerOptions): void;
removeRoot(info: ScriptInfo): void;
removeReferencedFile(info: ScriptInfo): void;
}
export const nullLanguageServiceHost: ServerLanguageServiceHost = {
getCompilationSettings: () => undefined,
setCompilationSettings: () => undefined,
removeRoot: () => undefined,
removeReferencedFile: () => undefined
};
export interface ProjectOptions {
/**
* true if config file explicitly listed files
**/
configHasFilesProperty?: boolean;
/**
* these fields can be present in the project file
**/
files?: string[];
wildcardDirectories?: Map<WatchDirectoryFlags>;
compilerOptions?: CompilerOptions;
}
export function isInferredProjectName(name: string) {
// POSIX defines /dev/null as a device - there should be no file with this prefix
return /dev\/null\/inferredProject\d+\*/.test(name);
}
export function makeInferredProjectName(counter: number) {
return `/dev/null/inferredProject${counter}*`;
}
}