do not use ScriptVersionCache for closed files (#12777)

This commit is contained in:
Vladimir Matveev
2016-12-08 16:17:42 -08:00
committed by GitHub
parent 9dd769dc3c
commit 7da3383504
10 changed files with 304 additions and 77 deletions

View File

@@ -0,0 +1,71 @@
/// <reference path="../harness.ts" />
/// <reference path="../../server/scriptVersionCache.ts"/>
/// <reference path="./tsserverProjectSystem.ts" />
namespace ts.textStorage {
describe("Text storage", () => {
const f = {
path: "/a/app.ts",
content: `
let x = 1;
let y = 2;
function bar(a: number) {
return a + 1;
}`
};
it("text based storage should be have exactly the same as script version cache", () => {
debugger
const host = ts.projectSystem.createServerHost([f]);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path));
const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path));
ts1.useScriptVersionCache();
ts2.useText();
const lineMap = computeLineStarts(f.content);
for (let line = 0; line < lineMap.length; line++) {
const start = lineMap[line];
const end = line === lineMap.length - 1 ? f.path.length : lineMap[line + 1];
for (let offset = 0; offset < end - start; offset++) {
const pos1 = ts1.lineOffsetToPosition(line + 1, offset + 1);
const pos2 = ts2.lineOffsetToPosition(line + 1, offset + 1);
assert.isTrue(pos1 === pos2, `lineOffsetToPosition ${line + 1}-${offset + 1}: expected ${pos1} to equal ${pos2}`);
}
const {start: start1, length: length1 } = ts1.lineToTextSpan(line);
const {start: start2, length: length2 } = ts2.lineToTextSpan(line);
assert.isTrue(start1 === start2, `lineToTextSpan ${line}::start:: expected ${start1} to equal ${start2}`);
assert.isTrue(length1 === length2, `lineToTextSpan ${line}::length:: expected ${length1} to equal ${length2}`);
}
for (let pos = 0; pos < f.content.length; pos++) {
const { line: line1, offset: offset1 } = ts1.positionToLineOffset(pos);
const { line: line2, offset: offset2 } = ts2.positionToLineOffset(pos);
assert.isTrue(line1 === line2, `positionToLineOffset ${pos}::line:: expected ${line1} to equal ${line2}`);
assert.isTrue(offset1 === offset2, `positionToLineOffset ${pos}::offset:: expected ${offset1} to equal ${offset2}`);
}
});
it("should switch to script version cache if necessary", () => {
const host = ts.projectSystem.createServerHost([f]);
const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path));
ts1.getSnapshot();
assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 1");
ts1.edit(0, 5, " ");
assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 1");
ts1.useText();
assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 2");
ts1.getLineInfo(0);
assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 2");
})
});
}

View File

@@ -1574,7 +1574,7 @@ namespace ts.projectSystem {
const project = projectService.externalProjects[0];
const scriptInfo = project.getScriptInfo(file1.path);
const snap = scriptInfo.snap();
const snap = scriptInfo.getSnapshot();
const actualText = snap.getText(0, snap.getLength());
assert.equal(actualText, "", `expected content to be empty string, got "${actualText}"`);
@@ -1587,7 +1587,7 @@ namespace ts.projectSystem {
projectService.closeClientFile(file1.path);
const scriptInfo2 = project.getScriptInfo(file1.path);
const snap2 = scriptInfo2.snap();
const snap2 = scriptInfo2.getSnapshot();
const actualText2 = snap2.getText(0, snap.getLength());
assert.equal(actualText2, "", `expected content to be empty string, got "${actualText2}"`);
});
@@ -2285,13 +2285,13 @@ namespace ts.projectSystem {
p.updateGraph();
const scriptInfo = p.getScriptInfo(f.path);
checkSnapLength(scriptInfo.snap(), f.content.length);
checkSnapLength(scriptInfo.getSnapshot(), f.content.length);
// open project and replace its content with empty string
projectService.openClientFile(f.path, "");
checkSnapLength(scriptInfo.snap(), 0);
checkSnapLength(scriptInfo.getSnapshot(), 0);
});
function checkSnapLength(snap: server.LineIndexSnapshot, expectedLength: number) {
function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) {
assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size");
}
});
@@ -2444,7 +2444,6 @@ namespace ts.projectSystem {
const cwd = {
path: "/a/c"
};
debugger;
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
@@ -2715,7 +2714,7 @@ namespace ts.projectSystem {
// verify content
const projectServiice = session.getProjectService();
const snap1 = projectServiice.getScriptInfo(f1.path).snap();
const snap1 = projectServiice.getScriptInfo(f1.path).getSnapshot();
assert.equal(snap1.getText(0, snap1.getLength()), tmp.content, "content should be equal to the content of temp file");
// reload from original file file
@@ -2727,7 +2726,7 @@ namespace ts.projectSystem {
});
// verify content
const snap2 = projectServiice.getScriptInfo(f1.path).snap();
const snap2 = projectServiice.getScriptInfo(f1.path).getSnapshot();
assert.equal(snap2.getText(0, snap2.getLength()), f1.content, "content should be equal to the content of original file");
});

View File

@@ -424,7 +424,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();
@@ -440,7 +440,7 @@ namespace ts.server {
// TODO: handle isOpen = true case
if (!info.isOpen) {
if (!info.isScriptOpen()) {
this.filenameToScriptInfo.remove(info.path);
this.lastDeletedFile = info;
@@ -634,10 +634,9 @@ 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[];
@@ -989,7 +988,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];
@@ -1089,32 +1088,31 @@ 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 (openedByClient) {
info.isOpen = true;
else if (fileContent !== undefined) {
info.reload(fileContent);
}
}
return info;
@@ -1230,7 +1228,6 @@ namespace ts.server {
const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
if (info) {
this.closeOpenFile(info);
info.isOpen = false;
}
this.printProjects();
}
@@ -1255,7 +1252,7 @@ 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, tryConvertScriptKindName(file.scriptKind), file.hasMixedContent);
}

View File

@@ -169,7 +169,7 @@ namespace ts.server {
getScriptSnapshot(filename: string): ts.IScriptSnapshot {
const scriptInfo = this.project.getScriptInfoLSHost(filename);
if (scriptInfo) {
return scriptInfo.snap();
return scriptInfo.getSnapshot();
}
}

View File

@@ -448,7 +448,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);
}
}

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() {
this.svc = undefined;
this.reloadFromFile();
}
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();
}
public getSnapshot() {
return this.textStorage.getSnapshot();
}
getFormatCodeSettings() {
return this.formatCodeSettings;
}
@@ -112,16 +289,16 @@ 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()));
}
@@ -130,22 +307,17 @@ namespace ts.server {
this.reload("");
}
else {
this.svc.reloadFromFile(tempFileName || 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();
}
@@ -159,17 +331,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);
}
/**
@@ -177,11 +339,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);
}
/**
@@ -189,9 +347,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

@@ -438,8 +438,9 @@ namespace ts.server {
}
}
getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange {
const oldSnap = <LineIndexSnapshot>oldSnapshot;
return this.getTextChangeRangeSinceVersion(oldSnap.version);
if (oldSnapshot instanceof LineIndexSnapshot) {
return this.getTextChangeRangeSinceVersion(oldSnapshot.version);
}
}
}

View File

@@ -709,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) => {
@@ -722,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,
@@ -1326,7 +1326,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

View File

@@ -91,7 +91,9 @@ namespace ts {
}
public getText(start: number, end: number): string {
return this.text.substring(start, end);
return start === 0 && end === this.text.length
? this.text
: this.text.substring(start, end);
}
public getLength(): number {