385 lines
15 KiB
TypeScript
385 lines
15 KiB
TypeScript
module Harness.LanguageService {
|
|
export class ScriptInfo {
|
|
public version: number = 1;
|
|
public editRanges: { length: number; textChangeRange: TypeScript.TextChangeRange; }[] = [];
|
|
public lineMap: TypeScript.LineMap = null;
|
|
|
|
constructor(public fileName: string, public content: string, public isOpen = true, public byteOrderMark: TypeScript.ByteOrderMark = TypeScript.ByteOrderMark.None) {
|
|
this.setContent(content);
|
|
}
|
|
|
|
private setContent(content: string): void {
|
|
this.content = content;
|
|
this.lineMap = TypeScript.LineMap1.fromString(content);
|
|
}
|
|
|
|
public updateContent(content: string): void {
|
|
this.editRanges = [];
|
|
this.setContent(content);
|
|
this.version++;
|
|
}
|
|
|
|
public editContent(minChar: number, limChar: number, newText: string): void {
|
|
// Apply edits
|
|
var prefix = this.content.substring(0, minChar);
|
|
var middle = newText;
|
|
var suffix = this.content.substring(limChar);
|
|
this.setContent(prefix + middle + suffix);
|
|
|
|
// Store edit range + new length of script
|
|
this.editRanges.push({
|
|
length: this.content.length,
|
|
textChangeRange: new TypeScript.TextChangeRange(
|
|
TypeScript.TextSpan.fromBounds(minChar, limChar), newText.length)
|
|
});
|
|
|
|
// Update version #
|
|
this.version++;
|
|
}
|
|
|
|
public getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): TypeScript.TextChangeRange {
|
|
if (startVersion === endVersion) {
|
|
// No edits!
|
|
return TypeScript.TextChangeRange.unchanged;
|
|
}
|
|
|
|
var initialEditRangeIndex = this.editRanges.length - (this.version - startVersion);
|
|
var lastEditRangeIndex = this.editRanges.length - (this.version - endVersion);
|
|
|
|
var entries = this.editRanges.slice(initialEditRangeIndex, lastEditRangeIndex);
|
|
return TypeScript.TextChangeRange.collapseChangesAcrossMultipleVersions(entries.map(e => e.textChangeRange));
|
|
}
|
|
}
|
|
|
|
class ScriptSnapshotShim implements TypeScript.Services.IScriptSnapshotShim {
|
|
private lineMap: TypeScript.LineMap = null;
|
|
private textSnapshot: string;
|
|
private version: number;
|
|
|
|
constructor(private scriptInfo: ScriptInfo) {
|
|
this.textSnapshot = scriptInfo.content;
|
|
this.version = scriptInfo.version;
|
|
}
|
|
|
|
public getText(start: number, end: number): string {
|
|
return this.textSnapshot.substring(start, end);
|
|
}
|
|
|
|
public getLength(): number {
|
|
return this.textSnapshot.length;
|
|
}
|
|
|
|
public getLineStartPositions(): string {
|
|
if (this.lineMap === null) {
|
|
this.lineMap = TypeScript.LineMap1.fromString(this.textSnapshot);
|
|
}
|
|
|
|
return JSON.stringify(this.lineMap.lineStarts());
|
|
}
|
|
|
|
public getChangeRange(oldScript: TypeScript.Services.IScriptSnapshotShim): string {
|
|
var oldShim = <ScriptSnapshotShim>oldScript;
|
|
var range = this.scriptInfo.getTextChangeRangeBetweenVersions(oldShim.version, this.version);
|
|
if (range === null) {
|
|
return null;
|
|
}
|
|
|
|
return JSON.stringify({ span: { start: range.span().start(), length: range.span().length() }, newLength: range.newLength() });
|
|
}
|
|
}
|
|
|
|
export class TypeScriptLS implements TypeScript.Services.ILanguageServiceShimHost {
|
|
IO = TypeScript.Environment ? TypeScript.Environment : Network.getEnvironment();
|
|
private ls: TypeScript.Services.ILanguageServiceShim = null;
|
|
public newLS: ts.LanguageService;
|
|
|
|
private fileNameToScript = new TypeScript.StringHashTable<ScriptInfo>();
|
|
|
|
constructor(private cancellationToken: TypeScript.ICancellationToken = TypeScript.CancellationToken.None) {
|
|
}
|
|
|
|
public addDefaultLibrary() {
|
|
this.addScript("lib.d.ts", Harness.Compiler.libText);
|
|
}
|
|
|
|
public getHostIdentifier(): string {
|
|
return "TypeScriptLS";
|
|
}
|
|
|
|
public addFile(fileName: string) {
|
|
var code = Harness.Environment.readFile(fileName);
|
|
this.addScript(fileName, code);
|
|
}
|
|
|
|
private getScriptInfo(fileName: string): ScriptInfo {
|
|
return this.fileNameToScript.lookup(fileName);
|
|
}
|
|
|
|
public addScript(fileName: string, content: string) {
|
|
var script = new ScriptInfo(fileName, content);
|
|
this.fileNameToScript.add(fileName, script);
|
|
}
|
|
|
|
public updateScript(fileName: string, content: string) {
|
|
var script = this.getScriptInfo(fileName);
|
|
if (script !== null) {
|
|
script.updateContent(content);
|
|
return;
|
|
}
|
|
|
|
this.addScript(fileName, content);
|
|
}
|
|
|
|
public editScript(fileName: string, minChar: number, limChar: number, newText: string) {
|
|
var script = this.getScriptInfo(fileName);
|
|
if (script !== null) {
|
|
script.editContent(minChar, limChar, newText);
|
|
return;
|
|
}
|
|
|
|
throw new Error("No script with name '" + fileName + "'");
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// ILogger implementation
|
|
//
|
|
public information(): boolean { return false; }
|
|
public debug(): boolean { return true; }
|
|
public warning(): boolean { return true; }
|
|
public error(): boolean { return true; }
|
|
public fatal(): boolean { return true; }
|
|
|
|
public log(s: string): void {
|
|
// For debugging...
|
|
//TypeScript.Environment.standardOut.WriteLine("TypeScriptLS:" + s);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
// ILanguageServiceShimHost implementation
|
|
//
|
|
|
|
/// Returns json for Tools.CompilationSettings
|
|
public getCompilationSettings(): string {
|
|
return ""; // i.e. default settings
|
|
}
|
|
|
|
public getCancellationToken(): TypeScript.ICancellationToken {
|
|
return this.cancellationToken;
|
|
}
|
|
|
|
public getScriptFileNames(): string {
|
|
return JSON.stringify(this.fileNameToScript.getAllKeys());
|
|
}
|
|
|
|
public getScriptSnapshot(fileName: string): TypeScript.Services.IScriptSnapshotShim {
|
|
return new ScriptSnapshotShim(this.getScriptInfo(fileName));
|
|
}
|
|
|
|
public getScriptVersion(fileName: string): string {
|
|
return this.getScriptInfo(fileName).version.toString();
|
|
}
|
|
|
|
public getScriptIsOpen(fileName: string): boolean {
|
|
return this.getScriptInfo(fileName).isOpen;
|
|
}
|
|
|
|
public getScriptByteOrderMark(fileName: string): TypeScript.ByteOrderMark {
|
|
return this.getScriptInfo(fileName).byteOrderMark;
|
|
}
|
|
|
|
public getDiagnosticsObject(): TypeScript.Services.ILanguageServicesDiagnostics {
|
|
return new LanguageServicesDiagnostics("");
|
|
}
|
|
|
|
public getLocalizedDiagnosticMessages(): string {
|
|
return "";
|
|
}
|
|
|
|
public fileExists(s: string) {
|
|
return this.IO.fileExists(s);
|
|
}
|
|
|
|
public directoryExists(s: string) {
|
|
return this.IO.directoryExists(s);
|
|
}
|
|
|
|
public resolveRelativePath(path: string, directory: string): string {
|
|
if (TypeScript.isRooted(path) || !directory) {
|
|
return this.IO.absolutePath(path);
|
|
}
|
|
else {
|
|
return this.IO.absolutePath(TypeScript.IOUtils.combine(directory, path));
|
|
}
|
|
}
|
|
|
|
public getParentDirectory(path: string): string {
|
|
return this.IO.directoryName(path);
|
|
}
|
|
|
|
/** Return a new instance of the language service shim, up-to-date wrt to typecheck.
|
|
* To access the non-shim (i.e. actual) language service, use the "ls.languageService" property.
|
|
*/
|
|
public getLanguageService(): TypeScript.Services.ILanguageServiceShim {
|
|
var ls = new TypeScript.Services.TypeScriptServicesFactory().createLanguageServiceShim(this);
|
|
this.ls = ls;
|
|
var hostAdapter = new ts.LanguageServiceShimHostAdapter(this);
|
|
this.newLS = ts.createLanguageService(hostAdapter);
|
|
return ls;
|
|
}
|
|
|
|
/** Parse file given its source text */
|
|
public parseSourceText(fileName: string, sourceText: TypeScript.IScriptSnapshot): TypeScript.SourceUnitSyntax {
|
|
var compilationSettings = new TypeScript.CompilationSettings();
|
|
compilationSettings.codeGenTarget = TypeScript.LanguageVersion.EcmaScript5;
|
|
|
|
var settings = TypeScript.ImmutableCompilationSettings.fromCompilationSettings(compilationSettings);
|
|
var parseOptions = settings.codeGenTarget();
|
|
return TypeScript.Parser.parse(fileName, TypeScript.SimpleText.fromScriptSnapshot(sourceText), parseOptions, TypeScript.isDTSFile(fileName)).sourceUnit();
|
|
}
|
|
|
|
/** Parse a file on disk given its fileName */
|
|
public parseFile(fileName: string) {
|
|
var sourceText = TypeScript.ScriptSnapshot.fromString(this.IO.readFile(fileName, /*codepage:*/ null).contents)
|
|
return this.parseSourceText(fileName, sourceText);
|
|
}
|
|
|
|
/**
|
|
* @param line 1 based index
|
|
* @param col 1 based index
|
|
*/
|
|
public lineColToPosition(fileName: string, line: number, col: number): number {
|
|
var script: ScriptInfo = this.fileNameToScript.lookup(fileName);
|
|
assert.isNotNull(script);
|
|
assert.isTrue(line >= 1);
|
|
assert.isTrue(col >= 1);
|
|
|
|
return script.lineMap.getPosition(line - 1, col - 1);
|
|
}
|
|
|
|
/**
|
|
* @param line 0 based index
|
|
* @param col 0 based index
|
|
*/
|
|
public positionToZeroBasedLineCol(fileName: string, position: number): TypeScript.ILineAndCharacter {
|
|
var script: ScriptInfo = this.fileNameToScript.lookup(fileName);
|
|
assert.isNotNull(script);
|
|
|
|
var result = script.lineMap.getLineAndCharacterFromPosition(position);
|
|
|
|
assert.isTrue(result.line() >= 0);
|
|
assert.isTrue(result.character() >= 0);
|
|
return { line: result.line(), character: result.character() };
|
|
}
|
|
|
|
/** Verify that applying edits to sourceFileName result in the content of the file baselineFileName */
|
|
public checkEdits(sourceFileName: string, baselineFileName: string, edits: TypeScript.Services.TextChange[]) {
|
|
var script = Utils.readFile(sourceFileName);
|
|
var formattedScript = this.applyEdits(script.contents, edits);
|
|
var baseline = Utils.readFile(baselineFileName).contents;
|
|
|
|
function noDiff(text1: string, text2: string) {
|
|
text1 = text1.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
|
|
text2 = text2.replace(/^\s+|\s+$/g, "").replace(/\r\n?/g, "\n");
|
|
|
|
if (text1 !== text2) {
|
|
var errorString = "";
|
|
var text1Lines = text1.split(/\n/);
|
|
var text2Lines = text2.split(/\n/);
|
|
for (var i = 0; i < text1Lines.length; i++) {
|
|
if (text1Lines[i] !== text2Lines[i]) {
|
|
errorString += "Difference at line " + (i + 1) + ":\n";
|
|
errorString += " Left File: " + text1Lines[i] + "\n";
|
|
errorString += " Right File: " + text2Lines[i] + "\n\n";
|
|
}
|
|
}
|
|
throw (new Error(errorString));
|
|
}
|
|
}
|
|
assert.isTrue(noDiff(formattedScript, baseline));
|
|
assert.equal(formattedScript, baseline);
|
|
}
|
|
|
|
|
|
/** Apply an array of text edits to a string, and return the resulting string. */
|
|
public applyEdits(content: string, edits: TypeScript.Services.TextChange[]): string {
|
|
var result = content;
|
|
edits = this.normalizeEdits(edits);
|
|
|
|
for (var i = edits.length - 1; i >= 0; i--) {
|
|
var edit = edits[i];
|
|
var prefix = result.substring(0, edit.span.start());
|
|
var middle = edit.newText;
|
|
var suffix = result.substring(edit.span.end());
|
|
result = prefix + middle + suffix;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Normalize an array of edits by removing overlapping entries and sorting entries on the minChar position. */
|
|
private normalizeEdits(edits: TypeScript.Services.TextChange[]): TypeScript.Services.TextChange[] {
|
|
var result: TypeScript.Services.TextChange[] = [];
|
|
|
|
function mapEdits(edits: TypeScript.Services.TextChange[]): { edit: TypeScript.Services.TextChange; index: number; }[] {
|
|
var result: { edit: TypeScript.Services.TextChange; index: number; }[] = [];
|
|
for (var i = 0; i < edits.length; i++) {
|
|
result.push({ edit: edits[i], index: i });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
var temp = mapEdits(edits).sort(function (a, b) {
|
|
var result = a.edit.span.start() - b.edit.span.start();
|
|
if (result === 0)
|
|
result = a.index - b.index;
|
|
return result;
|
|
});
|
|
|
|
var current = 0;
|
|
var next = 1;
|
|
while (current < temp.length) {
|
|
var currentEdit = temp[current].edit;
|
|
|
|
// Last edit
|
|
if (next >= temp.length) {
|
|
result.push(currentEdit);
|
|
current++;
|
|
continue;
|
|
}
|
|
var nextEdit = temp[next].edit;
|
|
|
|
var gap = nextEdit.span.start() - currentEdit.span.end();
|
|
|
|
// non-overlapping edits
|
|
if (gap >= 0) {
|
|
result.push(currentEdit);
|
|
current = next;
|
|
next++;
|
|
continue;
|
|
}
|
|
|
|
// overlapping edits: for now, we only support ignoring an next edit
|
|
// entirely contained in the current edit.
|
|
if (currentEdit.span.end() >= nextEdit.span.end()) {
|
|
next++;
|
|
continue;
|
|
}
|
|
else {
|
|
throw new Error("Trying to apply overlapping edits");
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
export class LanguageServicesDiagnostics implements TypeScript.Services.ILanguageServicesDiagnostics {
|
|
|
|
constructor(private destination: string) { }
|
|
|
|
public log(content: string): void {
|
|
//Imitates the LanguageServicesDiagnostics object when not in Visual Studio
|
|
}
|
|
|
|
}
|
|
}
|
|
|