Skip to content

Enable tsserver global operations to be performed on all projects #7353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 21, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,15 @@ namespace ts {
return undefined;
}

export function contains<T>(array: T[], value: T): boolean {
export function contains<T>(array: T[], value: T, areEqual?: (a: T, b: T) => boolean): boolean {
if (array) {
for (const v of array) {
if (v === value) {
if (areEqual) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (areEqual ? areEqual(v, value) : v === value) {
   return true;
}

if (areEqual(v, value)) {
return true;
}
}
else if (v === value) {
return true;
}
}
Expand Down Expand Up @@ -156,12 +161,12 @@ namespace ts {
return array1.concat(array2);
}

export function deduplicate<T>(array: T[]): T[] {
export function deduplicate<T>(array: T[], areEqual?: (a: T, b: T) => boolean): T[] {
let result: T[];
if (array) {
result = [];
for (const item of array) {
if (!contains(result, item)) {
if (!contains(result, item, areEqual)) {
result.push(item);
}
}
Expand Down
250 changes: 149 additions & 101 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,15 @@ namespace ts.server {

private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
const defaultProject = this.projectService.getProjectForFile(file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not ?

const projects = this.projectService.findReferencingProjects(info);
if (!projects.lenght) {
    throw Errors.NoProject;
}
const defaultProject = projects[0];

if (!defaultProject) {
throw Errors.NoProject;
}

const compilerService = project.compilerService;
const position = compilerService.host.lineOffsetToPosition(file, line, offset);
const renameInfo = compilerService.languageService.getRenameInfo(file, position);
// The rename info should be the same for every project
const defaultProjectCompilerService = defaultProject.compilerService;
const position = defaultProjectCompilerService.host.lineOffsetToPosition(file, line, offset);
const renameInfo = defaultProjectCompilerService.languageService.getRenameInfo(file, position);
if (!renameInfo) {
return undefined;
}
Expand All @@ -431,98 +432,129 @@ namespace ts.server {
};
}

const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments);
if (!renameLocations) {
return undefined;
}

const bakedRenameLocs = renameLocations.map(location => (<protocol.FileSpan>{
file: location.fileName,
start: compilerService.host.positionToLineOffset(location.fileName, location.textSpan.start),
end: compilerService.host.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)),
})).sort((a, b) => {
if (a.file < b.file) {
return -1;
}
else if (a.file > b.file) {
return 1;
const locs: protocol.SpanGroup[] = [];
const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
for (const project of projects) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a helper function,

function forEachProject<T>(projects: Project[], action: (project: Project) => T[], compare: (a: T, b: T) => number, areEqual: (a: T, b: T) => boolean): T[] {
    var result: T[] = [];
    for (var project of projects) {
        result = concatenate(result, action(project));
    }
    result = result.sort(compare);
    return projects.length === 1 ? result : ts.deduplicate(result, areEqual);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even better:

function forEachProject<T>(projects: Project[], action: (project: Project) => T[], compare: (a: T, b: T) => number, areEqual: (a: T, b: T) => boolean): T[] {
    var result = projects.reduce<T[]>((previous, current) => concatenate(previous, action(current)), []).sort(compare);
    return projects.length > 1 ? deduplicate(result, areEqual): result;
}

const compilerService = project.compilerService;
const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments);
if (!renameLocations) {
continue;
}
else {
// reverse sort assuming no overlap
if (a.start.line < b.start.line) {
return 1;
}
else if (a.start.line > b.start.line) {

const bakedRenameLocs = renameLocations.map(location => (<protocol.FileSpan>{
file: location.fileName,
start: compilerService.host.positionToLineOffset(location.fileName, location.textSpan.start),
end: compilerService.host.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)),
})).sort((a, b) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider extracting this to a helper compareRenameLocation

if (a.file < b.file) {
return -1;
}
else if (a.file > b.file) {
return 1;
}
else {
return b.start.offset - a.start.offset;
// reverse sort assuming no overlap
if (a.start.line < b.start.line) {
return 1;
}
else if (a.start.line > b.start.line) {
return -1;
}
else {
return b.start.offset - a.start.offset;
}
}
}
}).reduce<protocol.SpanGroup[]>((accum: protocol.SpanGroup[], cur: protocol.FileSpan) => {
let curFileAccum: protocol.SpanGroup;
if (accum.length > 0) {
curFileAccum = accum[accum.length - 1];
if (curFileAccum.file != cur.file) {
curFileAccum = undefined;
}).reduce<protocol.SpanGroup[]>((accum: protocol.SpanGroup[], cur: protocol.FileSpan) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need the type argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or the type annotations actually

let curFileAccum: protocol.SpanGroup;
if (accum.length > 0) {
curFileAccum = accum[accum.length - 1];
if (curFileAccum.file != cur.file) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!==

curFileAccum = undefined;
}
}
}
if (!curFileAccum) {
curFileAccum = { file: cur.file, locs: [] };
accum.push(curFileAccum);
}
curFileAccum.locs.push({ start: cur.start, end: cur.end });
return accum;
}, []);
if (!curFileAccum) {
curFileAccum = { file: cur.file, locs: [] };
accum.push(curFileAccum);
}
curFileAccum.locs.push({ start: cur.start, end: cur.end });
return accum;
}, []);

addRange(locs, bakedRenameLocs);
}

return { info: renameInfo, locs: bakedRenameLocs };
return { info: renameInfo, locs: deduplicate(locs, areSpanGroupsForTheSameFile) };

function areSpanGroupsForTheSameFile(a: protocol.SpanGroup, b: protocol.SpanGroup) {
if (a && b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return a && b && return a.file === b.file;

return a.file === b.file;
}
return false;
}
}

private getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody {
// TODO: get all projects for this file; report refs for all projects deleting duplicates
// can avoid duplicates by eliminating same ref file from subsequent projects
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
const defaultProject = this.projectService.getProjectForFile(file);
if (!defaultProject) {
throw Errors.NoProject;
}

const compilerService = project.compilerService;
const position = compilerService.host.lineOffsetToPosition(file, line, offset);

const references = compilerService.languageService.getReferencesAtPosition(file, position);
if (!references) {
return undefined;
}

const nameInfo = compilerService.languageService.getQuickInfoAtPosition(file, position);
const position = defaultProject.compilerService.host.lineOffsetToPosition(file, line, offset);
const nameInfo = defaultProject.compilerService.languageService.getQuickInfoAtPosition(file, position);
if (!nameInfo) {
return undefined;
}

const displayString = ts.displayPartsToString(nameInfo.displayParts);
const nameSpan = nameInfo.textSpan;
const nameColStart = compilerService.host.positionToLineOffset(file, nameSpan.start).offset;
const nameText = compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan));
const bakedRefs: protocol.ReferencesResponseItem[] = references.map(ref => {
const start = compilerService.host.positionToLineOffset(ref.fileName, ref.textSpan.start);
const refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1);
const snap = compilerService.host.getScriptSnapshot(ref.fileName);
const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, "");
return {
file: ref.fileName,
start: start,
lineText: lineText,
end: compilerService.host.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)),
isWriteAccess: ref.isWriteAccess
};
}).sort(compareFileStart);
const nameColStart = defaultProject.compilerService.host.positionToLineOffset(file, nameSpan.start).offset;
const nameText = defaultProject.compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan));

const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
const refs: protocol.ReferencesResponseItem[] = [];
for (const project of projects)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous line

const compilerService = project.compilerService;
const references = compilerService.languageService.getReferencesAtPosition(file, position);
if (!references) {
continue;
}

const bakedRefs: protocol.ReferencesResponseItem[] = references.map(ref => {
const start = compilerService.host.positionToLineOffset(ref.fileName, ref.textSpan.start);
const refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1);
const snap = compilerService.host.getScriptSnapshot(ref.fileName);
const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, "");
return {
file: ref.fileName,
start: start,
lineText: lineText,
end: compilerService.host.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)),
isWriteAccess: ref.isWriteAccess
};
}).sort(compareFileStart);

addRange(refs, bakedRefs);
}

return {
refs: bakedRefs,
refs: deduplicate(refs, areReferencesResponseItemsForTheSameLocation),
symbolName: nameText,
symbolStartOffset: nameColStart,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why deduplicate again?

symbolDisplayString: displayString
};

function areReferencesResponseItemsForTheSameLocation(a: protocol.ReferencesResponseItem, b: protocol.ReferencesResponseItem) {
if (a && b) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will one of these be undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally it might be undefined when getReferencesAtPosition returns undefined. Now it is not likely, though this is just to be conservative.

return a.file === b.file &&
a.start === b.start &&
a.end === b.end;
}
return false;
}
}

/**
Expand Down Expand Up @@ -836,41 +868,57 @@ namespace ts.server {

private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
const defaultProject = this.projectService.getProjectForFile(file);
if (!defaultProject) {
throw Errors.NoProject;
}

const compilerService = project.compilerService;
const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount);
if (!navItems) {
return undefined;
const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
const allNavToItems: protocol.NavtoItem[] = [];
for (const project of projects) {
const compilerService = project.compilerService;
const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount);
if (!navItems) {
continue;
}

const bakedNavItems = navItems.map((navItem) => {
const start = compilerService.host.positionToLineOffset(navItem.fileName, navItem.textSpan.start);
const end = compilerService.host.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan));
const bakedItem: protocol.NavtoItem = {
name: navItem.name,
kind: navItem.kind,
file: navItem.fileName,
start: start,
end: end,
};
if (navItem.kindModifiers && (navItem.kindModifiers != "")) {
bakedItem.kindModifiers = navItem.kindModifiers;
}
if (navItem.matchKind !== "none") {
bakedItem.matchKind = navItem.matchKind;
}
if (navItem.containerName && (navItem.containerName.length > 0)) {
bakedItem.containerName = navItem.containerName;
}
if (navItem.containerKind && (navItem.containerKind.length > 0)) {
bakedItem.containerKind = navItem.containerKind;
}
return bakedItem;
});
addRange(allNavToItems, bakedNavItems);
}
return deduplicate(allNavToItems, areNavToItemsForTheSameLocation);

return navItems.map((navItem) => {
const start = compilerService.host.positionToLineOffset(navItem.fileName, navItem.textSpan.start);
const end = compilerService.host.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan));
const bakedItem: protocol.NavtoItem = {
name: navItem.name,
kind: navItem.kind,
file: navItem.fileName,
start: start,
end: end,
};
if (navItem.kindModifiers && (navItem.kindModifiers != "")) {
bakedItem.kindModifiers = navItem.kindModifiers;
}
if (navItem.matchKind !== "none") {
bakedItem.matchKind = navItem.matchKind;
}
if (navItem.containerName && (navItem.containerName.length > 0)) {
bakedItem.containerName = navItem.containerName;
function areNavToItemsForTheSameLocation(a: protocol.NavtoItem, b: protocol.NavtoItem) {
if (a && b) {
return a.file === b.file &&
a.start === b.start &&
a.end === b.end;
}
if (navItem.containerKind && (navItem.containerKind.length > 0)) {
bakedItem.containerKind = navItem.containerKind;
}
return bakedItem;
});
return false;
}
}

private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] {
Expand Down
17 changes: 17 additions & 0 deletions tests/cases/fourslash/findReferencesAcrossMultipleProjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path="fourslash.ts" />

//@Filename: a.ts
////var /*1*/x: number;

//@Filename: b.ts
/////// <reference path="a.ts" />
////x++;

//@Filename: c.ts
/////// <reference path="a.ts" />
////x++;

goTo.file("a.ts");
goTo.marker("1");

verify.referencesCountIs(3);
17 changes: 17 additions & 0 deletions tests/cases/fourslash/goToDefinitionAcrossMultipleProjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path="fourslash.ts" />

//@Filename: a.ts
////var x: number;

//@Filename: b.ts
////var x: number;

//@Filename: c.ts
/////// <reference path="a.ts" />
/////// <reference path="b.ts" />
/////**/x++;

goTo.file("c.ts");
goTo.marker();

verify.definitionCountIs(2);
17 changes: 17 additions & 0 deletions tests/cases/fourslash/renameAcrossMultipleProjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path="fourslash.ts" />

//@Filename: a.ts
////var /*1*/[|x|]: number;

//@Filename: b.ts
/////// <reference path="a.ts" />
////[|x|]++;

//@Filename: c.ts
/////// <reference path="a.ts" />
////[|x|]++;

goTo.file("a.ts");
goTo.marker("1");

verify.renameLocations( /*findInStrings*/ false, /*findInComments*/ false);