/// 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 */ readonly containingProjects: Project[] = []; private formatCodeSettings: ts.FormatCodeSettings; readonly path: Path; private fileWatcher: FileWatcher; private textStorage: TextStorage; private isOpen: boolean; constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, readonly scriptKind: ScriptKind, public hasMixedContent = false) { this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); 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; } attachToProject(project: Project): boolean { const isNew = !this.isAttached(project); if (isNew) { this.containingProjects.push(project); } return isNew; } isAttached(project: Project) { // unrolled for common cases switch (this.containingProjects.length) { case 0: return false; case 1: return this.containingProjects[0] === project; case 2: return this.containingProjects[0] === project || this.containingProjects[1] === project; default: return contains(this.containingProjects, project); } } detachFromProject(project: Project) { // unrolled for common cases switch (this.containingProjects.length) { case 0: return; case 1: if (this.containingProjects[0] === project) { this.containingProjects.pop(); } break; case 2: if (this.containingProjects[0] === project) { this.containingProjects[0] = this.containingProjects.pop(); } else if (this.containingProjects[1] === project) { this.containingProjects.pop(); } break; default: removeItemFromSet(this.containingProjects, project); break; } } detachAllProjects() { for (const p of this.containingProjects) { // detach is unnecessary since we'll clean the list of containing projects anyways p.removeFile(this, /*detachFromProjects*/ false); } this.containingProjects.length = 0; } getDefaultProject() { if (this.containingProjects.length === 0) { return Errors.ThrowNoProject(); } 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) { this.formatCodeSettings = getDefaultFormatCodeSettings(this.host); } 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.textStorage.getVersion(); } reload(script: string) { this.textStorage.reload(script); this.markContainingProjectsAsDirty(); } saveTo(fileName: string) { const snap = this.textStorage.getSnapshot(); this.host.writeFile(fileName, snap.getText(0, snap.getLength())); } reloadFromFile(tempFileName?: NormalizedPath) { if (this.hasMixedContent) { this.reload(""); } else { this.textStorage.reloadFromFile(tempFileName); this.markContainingProjectsAsDirty(); } } getLineInfo(line: number) { return this.textStorage.getLineInfo(line); } editContent(start: number, end: number, newText: string): void { this.textStorage.edit(start, end, newText); this.markContainingProjectsAsDirty(); } markContainingProjectsAsDirty() { for (const p of this.containingProjects) { p.markAsDirty(); } } /** * @param line 1 based index */ lineToTextSpan(line: number) { return this.textStorage.lineToTextSpan(line); } /** * @param line 1 based index * @param offset 1 based index */ lineOffsetToPosition(line: number, offset: number): number { return this.textStorage.lineOffsetToPosition(line, offset); } /** * @param line 1-based index * @param offset 1-based index */ positionToLineOffset(position: number): ILineInfo { return this.textStorage.positionToLineOffset(position); } } }