mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
5665c098da
Dynamic files are generally created by the debugger when while debugging it can't find a matching file on disk. Since these files don't exist on disk, we shouldn't check if the file exists on disk, and allow the content to be controlled by the host.
359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
/// <reference path="scriptVersionCache.ts"/>
|
|
|
|
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.getSnapshotVersion()}`
|
|
: `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): AbsolutePositionAndLineText {
|
|
return this.switchToScriptVersionCache().getLineInfo(line);
|
|
}
|
|
/**
|
|
* @param line 0 based index
|
|
*/
|
|
lineToTextSpan(line: number): TextSpan {
|
|
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 createTextSpanFromBounds(start, end);
|
|
}
|
|
return this.svc.lineToTextSpan(line);
|
|
}
|
|
|
|
/**
|
|
* @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, this.text);
|
|
}
|
|
|
|
// TODO: assert this offset is actually on the line
|
|
return this.svc.lineOffsetToPosition(line, offset);
|
|
}
|
|
|
|
positionToLineOffset(position: number): protocol.Location {
|
|
if (!this.svc) {
|
|
const { line, character } = computeLineAndCharacterOfPosition(this.getLineMap(), position);
|
|
return { line: line + 1, offset: character + 1 };
|
|
}
|
|
return this.svc.positionToLineOffset(position);
|
|
}
|
|
|
|
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(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: 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,
|
|
public isDynamic = false) {
|
|
|
|
this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames));
|
|
this.textStorage = new TextStorage(host, fileName);
|
|
if (hasMixedContent || isDynamic) {
|
|
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 || this.isDynamic ? "" : 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:
|
|
unorderedRemoveItem(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);
|
|
}
|
|
clear(this.containingProjects);
|
|
}
|
|
|
|
getDefaultProject() {
|
|
switch (this.containingProjects.length) {
|
|
case 0:
|
|
return Errors.ThrowNoProject();
|
|
case 1:
|
|
return this.containingProjects[0];
|
|
default:
|
|
// if this file belongs to multiple projects, the first configured project should be
|
|
// the default project; if no configured projects, the first external project should
|
|
// be the default project; otherwise the first inferred project should be the default.
|
|
let firstExternalProject;
|
|
for (const project of this.containingProjects) {
|
|
if (project.projectKind === ProjectKind.Configured) {
|
|
return project;
|
|
}
|
|
else if (project.projectKind === ProjectKind.External && !firstExternalProject) {
|
|
firstExternalProject = project;
|
|
}
|
|
}
|
|
return firstExternalProject || 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);
|
|
}
|
|
mergeMapLikes(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.isDynamic) {
|
|
this.reload("");
|
|
}
|
|
else {
|
|
this.textStorage.reloadFromFile(tempFileName);
|
|
this.markContainingProjectsAsDirty();
|
|
}
|
|
}
|
|
|
|
getLineInfo(line: number): AbsolutePositionAndLineText {
|
|
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);
|
|
}
|
|
|
|
positionToLineOffset(position: number): protocol.Location {
|
|
return this.textStorage.positionToLineOffset(position);
|
|
}
|
|
|
|
public isJavaScript() {
|
|
return this.scriptKind === ScriptKind.JS || this.scriptKind === ScriptKind.JSX;
|
|
}
|
|
}
|
|
}
|