617251f2e0
* feat(45010): handle unclosed fragment in `getJsxClosingTagAtPosition` * Update tests * Fix types of `JsxText.parent` and `JsxExpression.parent`
2815 lines
134 KiB
TypeScript
2815 lines
134 KiB
TypeScript
namespace ts {
|
|
/** The version of the language service API */
|
|
export const servicesVersion = "0.8";
|
|
|
|
function createNode<TKind extends SyntaxKind>(kind: TKind, pos: number, end: number, parent: Node): NodeObject | TokenObject<TKind> | IdentifierObject | PrivateIdentifierObject {
|
|
const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) :
|
|
kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) :
|
|
kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(SyntaxKind.PrivateIdentifier, pos, end) :
|
|
new TokenObject(kind, pos, end);
|
|
node.parent = parent;
|
|
node.flags = parent.flags & NodeFlags.ContextFlags;
|
|
return node;
|
|
}
|
|
|
|
class NodeObject implements Node {
|
|
public kind: SyntaxKind;
|
|
public pos: number;
|
|
public end: number;
|
|
public flags: NodeFlags;
|
|
public modifierFlagsCache: ModifierFlags;
|
|
public transformFlags: TransformFlags;
|
|
public parent: Node;
|
|
public symbol!: Symbol; // Actually optional, but it was too annoying to access `node.symbol!` everywhere since in many cases we know it must be defined
|
|
public jsDoc?: JSDoc[];
|
|
public original?: Node;
|
|
private _children: Node[] | undefined;
|
|
|
|
constructor(kind: SyntaxKind, pos: number, end: number) {
|
|
this.pos = pos;
|
|
this.end = end;
|
|
this.flags = NodeFlags.None;
|
|
this.modifierFlagsCache = ModifierFlags.None;
|
|
this.transformFlags = TransformFlags.None;
|
|
this.parent = undefined!;
|
|
this.kind = kind;
|
|
}
|
|
|
|
private assertHasRealPosition(message?: string) {
|
|
// eslint-disable-next-line debug-assert
|
|
Debug.assert(!positionIsSynthesized(this.pos) && !positionIsSynthesized(this.end), message || "Node must have a real position for this operation");
|
|
}
|
|
|
|
public getSourceFile(): SourceFile {
|
|
return getSourceFileOfNode(this);
|
|
}
|
|
|
|
public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number {
|
|
this.assertHasRealPosition();
|
|
return getTokenPosOfNode(this, sourceFile, includeJsDocComment);
|
|
}
|
|
|
|
public getFullStart(): number {
|
|
this.assertHasRealPosition();
|
|
return this.pos;
|
|
}
|
|
|
|
public getEnd(): number {
|
|
this.assertHasRealPosition();
|
|
return this.end;
|
|
}
|
|
|
|
public getWidth(sourceFile?: SourceFile): number {
|
|
this.assertHasRealPosition();
|
|
return this.getEnd() - this.getStart(sourceFile);
|
|
}
|
|
|
|
public getFullWidth(): number {
|
|
this.assertHasRealPosition();
|
|
return this.end - this.pos;
|
|
}
|
|
|
|
public getLeadingTriviaWidth(sourceFile?: SourceFile): number {
|
|
this.assertHasRealPosition();
|
|
return this.getStart(sourceFile) - this.pos;
|
|
}
|
|
|
|
public getFullText(sourceFile?: SourceFile): string {
|
|
this.assertHasRealPosition();
|
|
return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end);
|
|
}
|
|
|
|
public getText(sourceFile?: SourceFile): string {
|
|
this.assertHasRealPosition();
|
|
if (!sourceFile) {
|
|
sourceFile = this.getSourceFile();
|
|
}
|
|
return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd());
|
|
}
|
|
|
|
public getChildCount(sourceFile?: SourceFile): number {
|
|
return this.getChildren(sourceFile).length;
|
|
}
|
|
|
|
public getChildAt(index: number, sourceFile?: SourceFile): Node {
|
|
return this.getChildren(sourceFile)[index];
|
|
}
|
|
|
|
public getChildren(sourceFile?: SourceFileLike): Node[] {
|
|
this.assertHasRealPosition("Node without a real position cannot be scanned and thus has no token nodes - use forEachChild and collect the result if that's fine");
|
|
return this._children || (this._children = createChildren(this, sourceFile));
|
|
}
|
|
|
|
public getFirstToken(sourceFile?: SourceFileLike): Node | undefined {
|
|
this.assertHasRealPosition();
|
|
const children = this.getChildren(sourceFile);
|
|
if (!children.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const child = find(children, kid => kid.kind < SyntaxKind.FirstJSDocNode || kid.kind > SyntaxKind.LastJSDocNode)!;
|
|
return child.kind < SyntaxKind.FirstNode ?
|
|
child :
|
|
child.getFirstToken(sourceFile);
|
|
}
|
|
|
|
public getLastToken(sourceFile?: SourceFileLike): Node | undefined {
|
|
this.assertHasRealPosition();
|
|
const children = this.getChildren(sourceFile);
|
|
|
|
const child = lastOrUndefined(children);
|
|
if (!child) {
|
|
return undefined;
|
|
}
|
|
|
|
return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile);
|
|
}
|
|
|
|
public forEachChild<T>(cbNode: (node: Node) => T, cbNodeArray?: (nodes: NodeArray<Node>) => T): T | undefined {
|
|
return forEachChild(this, cbNode, cbNodeArray);
|
|
}
|
|
}
|
|
|
|
function createChildren(node: Node, sourceFile: SourceFileLike | undefined): Node[] {
|
|
if (!isNodeKind(node.kind)) {
|
|
return emptyArray;
|
|
}
|
|
|
|
const children: Node[] = [];
|
|
|
|
if (isJSDocCommentContainingNode(node)) {
|
|
/** Don't add trivia for "tokens" since this is in a comment. */
|
|
node.forEachChild(child => {
|
|
children.push(child);
|
|
});
|
|
return children;
|
|
}
|
|
|
|
scanner.setText((sourceFile || node.getSourceFile()).text);
|
|
let pos = node.pos;
|
|
const processNode = (child: Node) => {
|
|
addSyntheticNodes(children, pos, child.pos, node);
|
|
children.push(child);
|
|
pos = child.end;
|
|
};
|
|
const processNodes = (nodes: NodeArray<Node>) => {
|
|
addSyntheticNodes(children, pos, nodes.pos, node);
|
|
children.push(createSyntaxList(nodes, node));
|
|
pos = nodes.end;
|
|
};
|
|
// jsDocComments need to be the first children
|
|
forEach((node as JSDocContainer).jsDoc, processNode);
|
|
// For syntactic classifications, all trivia are classified together, including jsdoc comments.
|
|
// For that to work, the jsdoc comments should still be the leading trivia of the first child.
|
|
// Restoring the scanner position ensures that.
|
|
pos = node.pos;
|
|
node.forEachChild(processNode, processNodes);
|
|
addSyntheticNodes(children, pos, node.end, node);
|
|
scanner.setText(undefined);
|
|
return children;
|
|
}
|
|
|
|
function addSyntheticNodes(nodes: Push<Node>, pos: number, end: number, parent: Node): void {
|
|
scanner.setTextPos(pos);
|
|
while (pos < end) {
|
|
const token = scanner.scan();
|
|
const textPos = scanner.getTextPos();
|
|
if (textPos <= end) {
|
|
if (token === SyntaxKind.Identifier) {
|
|
Debug.fail(`Did not expect ${Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`);
|
|
}
|
|
nodes.push(createNode(token, pos, textPos, parent));
|
|
}
|
|
pos = textPos;
|
|
if (token === SyntaxKind.EndOfFileToken) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function createSyntaxList(nodes: NodeArray<Node>, parent: Node): Node {
|
|
const list = createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as SyntaxList;
|
|
list._children = [];
|
|
let pos = nodes.pos;
|
|
for (const node of nodes) {
|
|
addSyntheticNodes(list._children, pos, node.pos, parent);
|
|
list._children.push(node);
|
|
pos = node.end;
|
|
}
|
|
addSyntheticNodes(list._children, pos, nodes.end, parent);
|
|
return list;
|
|
}
|
|
|
|
class TokenOrIdentifierObject implements Node {
|
|
public kind!: SyntaxKind;
|
|
public pos: number;
|
|
public end: number;
|
|
public flags: NodeFlags;
|
|
public modifierFlagsCache: ModifierFlags;
|
|
public transformFlags: TransformFlags;
|
|
public parent: Node;
|
|
public symbol!: Symbol;
|
|
public jsDocComments?: JSDoc[];
|
|
|
|
constructor(pos: number, end: number) {
|
|
// Set properties in same order as NodeObject
|
|
this.pos = pos;
|
|
this.end = end;
|
|
this.flags = NodeFlags.None;
|
|
this.modifierFlagsCache = ModifierFlags.None;
|
|
this.transformFlags = TransformFlags.None;
|
|
this.parent = undefined!;
|
|
}
|
|
|
|
public getSourceFile(): SourceFile {
|
|
return getSourceFileOfNode(this);
|
|
}
|
|
|
|
public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number {
|
|
return getTokenPosOfNode(this, sourceFile, includeJsDocComment);
|
|
}
|
|
|
|
public getFullStart(): number {
|
|
return this.pos;
|
|
}
|
|
|
|
public getEnd(): number {
|
|
return this.end;
|
|
}
|
|
|
|
public getWidth(sourceFile?: SourceFile): number {
|
|
return this.getEnd() - this.getStart(sourceFile);
|
|
}
|
|
|
|
public getFullWidth(): number {
|
|
return this.end - this.pos;
|
|
}
|
|
|
|
public getLeadingTriviaWidth(sourceFile?: SourceFile): number {
|
|
return this.getStart(sourceFile) - this.pos;
|
|
}
|
|
|
|
public getFullText(sourceFile?: SourceFile): string {
|
|
return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end);
|
|
}
|
|
|
|
public getText(sourceFile?: SourceFile): string {
|
|
if (!sourceFile) {
|
|
sourceFile = this.getSourceFile();
|
|
}
|
|
return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd());
|
|
}
|
|
|
|
public getChildCount(): number {
|
|
return this.getChildren().length;
|
|
}
|
|
|
|
public getChildAt(index: number): Node {
|
|
return this.getChildren()[index];
|
|
}
|
|
|
|
public getChildren(): Node[] {
|
|
return this.kind === SyntaxKind.EndOfFileToken ? (this as EndOfFileToken).jsDoc || emptyArray : emptyArray;
|
|
}
|
|
|
|
public getFirstToken(): Node | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
public getLastToken(): Node | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
public forEachChild<T>(): T | undefined {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class SymbolObject implements Symbol {
|
|
flags: SymbolFlags;
|
|
escapedName: __String;
|
|
declarations!: Declaration[];
|
|
valueDeclaration!: Declaration;
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no doc comment, then the empty array will be returned.
|
|
documentationComment?: SymbolDisplayPart[];
|
|
tags?: JSDocTagInfo[]; // same
|
|
|
|
contextualGetAccessorDocumentationComment?: SymbolDisplayPart[];
|
|
contextualSetAccessorDocumentationComment?: SymbolDisplayPart[];
|
|
|
|
constructor(flags: SymbolFlags, name: __String) {
|
|
this.flags = flags;
|
|
this.escapedName = name;
|
|
}
|
|
|
|
getFlags(): SymbolFlags {
|
|
return this.flags;
|
|
}
|
|
|
|
get name(): string {
|
|
return symbolName(this);
|
|
}
|
|
|
|
getEscapedName(): __String {
|
|
return this.escapedName;
|
|
}
|
|
|
|
getName(): string {
|
|
return this.name;
|
|
}
|
|
|
|
getDeclarations(): Declaration[] | undefined {
|
|
return this.declarations;
|
|
}
|
|
|
|
getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
|
if (!this.documentationComment) {
|
|
this.documentationComment = emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs
|
|
|
|
if (!this.declarations && (this as Symbol as TransientSymbol).target && ((this as Symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration) {
|
|
const labelDecl = ((this as Symbol as TransientSymbol).target as TransientSymbol).tupleLabelDeclaration!;
|
|
this.documentationComment = getDocumentationComment([labelDecl], checker);
|
|
}
|
|
else {
|
|
this.documentationComment = getDocumentationComment(this.declarations, checker);
|
|
}
|
|
}
|
|
return this.documentationComment;
|
|
}
|
|
|
|
getContextualDocumentationComment(context: Node | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
|
switch (context?.kind) {
|
|
case SyntaxKind.GetAccessor:
|
|
if (!this.contextualGetAccessorDocumentationComment) {
|
|
this.contextualGetAccessorDocumentationComment = emptyArray;
|
|
this.contextualGetAccessorDocumentationComment = getDocumentationComment(filter(this.declarations, isGetAccessor), checker);
|
|
}
|
|
return this.contextualGetAccessorDocumentationComment;
|
|
case SyntaxKind.SetAccessor:
|
|
if (!this.contextualSetAccessorDocumentationComment) {
|
|
this.contextualSetAccessorDocumentationComment = emptyArray;
|
|
this.contextualSetAccessorDocumentationComment = getDocumentationComment(filter(this.declarations, isSetAccessor), checker);
|
|
}
|
|
return this.contextualSetAccessorDocumentationComment;
|
|
default:
|
|
return this.getDocumentationComment(checker);
|
|
}
|
|
}
|
|
|
|
getJsDocTags(checker?: TypeChecker): JSDocTagInfo[] {
|
|
if (this.tags === undefined) {
|
|
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations, checker);
|
|
}
|
|
|
|
return this.tags;
|
|
}
|
|
}
|
|
|
|
class TokenObject<TKind extends SyntaxKind> extends TokenOrIdentifierObject implements Token<TKind> {
|
|
public kind: TKind;
|
|
|
|
constructor(kind: TKind, pos: number, end: number) {
|
|
super(pos, end);
|
|
this.kind = kind;
|
|
}
|
|
}
|
|
|
|
class IdentifierObject extends TokenOrIdentifierObject implements Identifier {
|
|
public kind: SyntaxKind.Identifier = SyntaxKind.Identifier;
|
|
public escapedText!: __String;
|
|
public autoGenerateFlags!: GeneratedIdentifierFlags;
|
|
_primaryExpressionBrand: any;
|
|
_memberExpressionBrand: any;
|
|
_leftHandSideExpressionBrand: any;
|
|
_updateExpressionBrand: any;
|
|
_unaryExpressionBrand: any;
|
|
_expressionBrand: any;
|
|
_declarationBrand: any;
|
|
/*@internal*/typeArguments!: NodeArray<TypeNode>;
|
|
constructor(_kind: SyntaxKind.Identifier, pos: number, end: number) {
|
|
super(pos, end);
|
|
}
|
|
|
|
get text(): string {
|
|
return idText(this);
|
|
}
|
|
}
|
|
IdentifierObject.prototype.kind = SyntaxKind.Identifier;
|
|
class PrivateIdentifierObject extends TokenOrIdentifierObject implements PrivateIdentifier {
|
|
public kind!: SyntaxKind.PrivateIdentifier;
|
|
public escapedText!: __String;
|
|
public symbol!: Symbol;
|
|
constructor(_kind: SyntaxKind.PrivateIdentifier, pos: number, end: number) {
|
|
super(pos, end);
|
|
}
|
|
|
|
get text(): string {
|
|
return idText(this);
|
|
}
|
|
}
|
|
PrivateIdentifierObject.prototype.kind = SyntaxKind.PrivateIdentifier;
|
|
|
|
class TypeObject implements Type {
|
|
checker: TypeChecker;
|
|
flags: TypeFlags;
|
|
objectFlags?: ObjectFlags;
|
|
id!: number;
|
|
symbol!: Symbol;
|
|
constructor(checker: TypeChecker, flags: TypeFlags) {
|
|
this.checker = checker;
|
|
this.flags = flags;
|
|
}
|
|
getFlags(): TypeFlags {
|
|
return this.flags;
|
|
}
|
|
getSymbol(): Symbol | undefined {
|
|
return this.symbol;
|
|
}
|
|
getProperties(): Symbol[] {
|
|
return this.checker.getPropertiesOfType(this);
|
|
}
|
|
getProperty(propertyName: string): Symbol | undefined {
|
|
return this.checker.getPropertyOfType(this, propertyName);
|
|
}
|
|
getApparentProperties(): Symbol[] {
|
|
return this.checker.getAugmentedPropertiesOfType(this);
|
|
}
|
|
getCallSignatures(): readonly Signature[] {
|
|
return this.checker.getSignaturesOfType(this, SignatureKind.Call);
|
|
}
|
|
getConstructSignatures(): readonly Signature[] {
|
|
return this.checker.getSignaturesOfType(this, SignatureKind.Construct);
|
|
}
|
|
getStringIndexType(): Type | undefined {
|
|
return this.checker.getIndexTypeOfType(this, IndexKind.String);
|
|
}
|
|
getNumberIndexType(): Type | undefined {
|
|
return this.checker.getIndexTypeOfType(this, IndexKind.Number);
|
|
}
|
|
getBaseTypes(): BaseType[] | undefined {
|
|
return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined;
|
|
}
|
|
isNullableType(): boolean {
|
|
return this.checker.isNullableType(this);
|
|
}
|
|
getNonNullableType(): Type {
|
|
return this.checker.getNonNullableType(this);
|
|
}
|
|
getNonOptionalType(): Type {
|
|
return this.checker.getNonOptionalType(this);
|
|
}
|
|
getConstraint(): Type | undefined {
|
|
return this.checker.getBaseConstraintOfType(this);
|
|
}
|
|
getDefault(): Type | undefined {
|
|
return this.checker.getDefaultFromTypeParameter(this);
|
|
}
|
|
|
|
isUnion(): this is UnionType {
|
|
return !!(this.flags & TypeFlags.Union);
|
|
}
|
|
isIntersection(): this is IntersectionType {
|
|
return !!(this.flags & TypeFlags.Intersection);
|
|
}
|
|
isUnionOrIntersection(): this is UnionOrIntersectionType {
|
|
return !!(this.flags & TypeFlags.UnionOrIntersection);
|
|
}
|
|
isLiteral(): this is LiteralType {
|
|
return !!(this.flags & TypeFlags.StringOrNumberLiteral);
|
|
}
|
|
isStringLiteral(): this is StringLiteralType {
|
|
return !!(this.flags & TypeFlags.StringLiteral);
|
|
}
|
|
isNumberLiteral(): this is NumberLiteralType {
|
|
return !!(this.flags & TypeFlags.NumberLiteral);
|
|
}
|
|
isTypeParameter(): this is TypeParameter {
|
|
return !!(this.flags & TypeFlags.TypeParameter);
|
|
}
|
|
isClassOrInterface(): this is InterfaceType {
|
|
return !!(getObjectFlags(this) & ObjectFlags.ClassOrInterface);
|
|
}
|
|
isClass(): this is InterfaceType {
|
|
return !!(getObjectFlags(this) & ObjectFlags.Class);
|
|
}
|
|
/**
|
|
* This polyfills `referenceType.typeArguments` for API consumers
|
|
*/
|
|
get typeArguments() {
|
|
if (getObjectFlags(this) & ObjectFlags.Reference) {
|
|
return this.checker.getTypeArguments(this as Type as TypeReference);
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class SignatureObject implements Signature {
|
|
flags: SignatureFlags;
|
|
checker: TypeChecker;
|
|
declaration!: SignatureDeclaration;
|
|
typeParameters?: TypeParameter[];
|
|
parameters!: Symbol[];
|
|
thisParameter!: Symbol;
|
|
resolvedReturnType!: Type;
|
|
resolvedTypePredicate: TypePredicate | undefined;
|
|
minTypeArgumentCount!: number;
|
|
minArgumentCount!: number;
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no doc comment, then the empty array will be returned.
|
|
documentationComment?: SymbolDisplayPart[];
|
|
jsDocTags?: JSDocTagInfo[]; // same
|
|
|
|
constructor(checker: TypeChecker, flags: SignatureFlags) {
|
|
this.checker = checker;
|
|
this.flags = flags;
|
|
}
|
|
|
|
getDeclaration(): SignatureDeclaration {
|
|
return this.declaration;
|
|
}
|
|
getTypeParameters(): TypeParameter[] | undefined {
|
|
return this.typeParameters;
|
|
}
|
|
getParameters(): Symbol[] {
|
|
return this.parameters;
|
|
}
|
|
getReturnType(): Type {
|
|
return this.checker.getReturnTypeOfSignature(this);
|
|
}
|
|
|
|
getDocumentationComment(): SymbolDisplayPart[] {
|
|
return this.documentationComment || (this.documentationComment = getDocumentationComment(singleElementArray(this.declaration), this.checker));
|
|
}
|
|
|
|
getJsDocTags(): JSDocTagInfo[] {
|
|
if (this.jsDocTags === undefined) {
|
|
this.jsDocTags = this.declaration ? getJsDocTagsOfSignature(this.declaration, this.checker) : [];
|
|
}
|
|
return this.jsDocTags;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the given node has a JSDoc "inheritDoc" tag on it.
|
|
* @param node the Node in question.
|
|
* @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`.
|
|
*/
|
|
function hasJSDocInheritDocTag(node: Node) {
|
|
return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc");
|
|
}
|
|
|
|
function getJsDocTagsOfSignature(declaration: Declaration, checker: TypeChecker): JSDocTagInfo[] {
|
|
let tags = JsDoc.getJsDocTagsFromDeclarations([declaration], checker);
|
|
if (tags.length === 0 || hasJSDocInheritDocTag(declaration)) {
|
|
const inheritedTags = findBaseOfDeclaration(checker, declaration, symbol => symbol.declarations?.length === 1 ? symbol.getJsDocTags() : undefined);
|
|
if (inheritedTags) {
|
|
tags = [...inheritedTags, ...tags];
|
|
}
|
|
}
|
|
return tags;
|
|
}
|
|
|
|
function getDocumentationComment(declarations: readonly Declaration[] | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
|
if (!declarations) return emptyArray;
|
|
|
|
let doc = JsDoc.getJsDocCommentsFromDeclarations(declarations, checker);
|
|
if (checker && (doc.length === 0 || declarations.some(hasJSDocInheritDocTag))) {
|
|
const seenSymbols = new Set<Symbol>();
|
|
for (const declaration of declarations) {
|
|
const inheritedDocs = findBaseOfDeclaration(checker, declaration, symbol => {
|
|
if (!seenSymbols.has(symbol)) {
|
|
seenSymbols.add(symbol);
|
|
return symbol.getDocumentationComment(checker);
|
|
}
|
|
});
|
|
// TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs
|
|
if (inheritedDocs) doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(lineBreakPart(), doc);
|
|
}
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
function findBaseOfDeclaration<T>(checker: TypeChecker, declaration: Declaration, cb: (symbol: Symbol) => T[] | undefined): T[] | undefined {
|
|
const classOrInterfaceDeclaration = declaration.parent?.kind === SyntaxKind.Constructor ? declaration.parent.parent : declaration.parent;
|
|
if (!classOrInterfaceDeclaration) {
|
|
return;
|
|
}
|
|
return firstDefined(getAllSuperTypeNodes(classOrInterfaceDeclaration), superTypeNode => {
|
|
const symbol = checker.getPropertyOfType(checker.getTypeAtLocation(superTypeNode), declaration.symbol.name);
|
|
return symbol ? cb(symbol) : undefined;
|
|
});
|
|
}
|
|
|
|
class SourceFileObject extends NodeObject implements SourceFile {
|
|
public kind: SyntaxKind.SourceFile = SyntaxKind.SourceFile;
|
|
public _declarationBrand: any;
|
|
public fileName!: string;
|
|
public path!: Path;
|
|
public resolvedPath!: Path;
|
|
public originalFileName!: string;
|
|
public text!: string;
|
|
public scriptSnapshot!: IScriptSnapshot;
|
|
public lineMap!: readonly number[];
|
|
|
|
public statements!: NodeArray<Statement>;
|
|
public endOfFileToken!: Token<SyntaxKind.EndOfFileToken>;
|
|
|
|
public amdDependencies!: { name: string; path: string }[];
|
|
public moduleName!: string;
|
|
public referencedFiles!: FileReference[];
|
|
public typeReferenceDirectives!: FileReference[];
|
|
public libReferenceDirectives!: FileReference[];
|
|
|
|
public syntacticDiagnostics!: DiagnosticWithLocation[];
|
|
public parseDiagnostics!: DiagnosticWithLocation[];
|
|
public bindDiagnostics!: DiagnosticWithLocation[];
|
|
public bindSuggestionDiagnostics?: DiagnosticWithLocation[];
|
|
|
|
public isDeclarationFile!: boolean;
|
|
public isDefaultLib!: boolean;
|
|
public hasNoDefaultLib!: boolean;
|
|
public externalModuleIndicator!: Node; // The first node that causes this file to be an external module
|
|
public commonJsModuleIndicator!: Node; // The first node that causes this file to be a CommonJS module
|
|
public nodeCount!: number;
|
|
public identifierCount!: number;
|
|
public symbolCount!: number;
|
|
public version!: string;
|
|
public scriptKind!: ScriptKind;
|
|
public languageVersion!: ScriptTarget;
|
|
public languageVariant!: LanguageVariant;
|
|
public identifiers!: ESMap<string, string>;
|
|
public nameTable: UnderscoreEscapedMap<number> | undefined;
|
|
public resolvedModules: ESMap<string, ResolvedModuleFull> | undefined;
|
|
public resolvedTypeReferenceDirectiveNames!: ESMap<string, ResolvedTypeReferenceDirective>;
|
|
public imports!: readonly StringLiteralLike[];
|
|
public moduleAugmentations!: StringLiteral[];
|
|
private namedDeclarations: ESMap<string, Declaration[]> | undefined;
|
|
public ambientModuleNames!: string[];
|
|
public checkJsDirective: CheckJsDirective | undefined;
|
|
public errorExpectations: TextRange[] | undefined;
|
|
public possiblyContainDynamicImport?: boolean;
|
|
public pragmas!: PragmaMap;
|
|
public localJsxFactory: EntityName | undefined;
|
|
public localJsxNamespace: __String | undefined;
|
|
|
|
constructor(kind: SyntaxKind, pos: number, end: number) {
|
|
super(kind, pos, end);
|
|
}
|
|
|
|
public update(newText: string, textChangeRange: TextChangeRange): SourceFile {
|
|
return updateSourceFile(this, newText, textChangeRange);
|
|
}
|
|
|
|
public getLineAndCharacterOfPosition(position: number): LineAndCharacter {
|
|
return getLineAndCharacterOfPosition(this, position);
|
|
}
|
|
|
|
public getLineStarts(): readonly number[] {
|
|
return getLineStarts(this);
|
|
}
|
|
|
|
public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number {
|
|
return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits);
|
|
}
|
|
|
|
public getLineEndOfPosition(pos: number): number {
|
|
const { line } = this.getLineAndCharacterOfPosition(pos);
|
|
const lineStarts = this.getLineStarts();
|
|
|
|
let lastCharPos: number | undefined;
|
|
if (line + 1 >= lineStarts.length) {
|
|
lastCharPos = this.getEnd();
|
|
}
|
|
if (!lastCharPos) {
|
|
lastCharPos = lineStarts[line + 1] - 1;
|
|
}
|
|
|
|
const fullText = this.getFullText();
|
|
// if the new line is "\r\n", we should return the last non-new-line-character position
|
|
return fullText[lastCharPos] === "\n" && fullText[lastCharPos - 1] === "\r" ? lastCharPos - 1 : lastCharPos;
|
|
}
|
|
|
|
public getNamedDeclarations(): ESMap<string, Declaration[]> {
|
|
if (!this.namedDeclarations) {
|
|
this.namedDeclarations = this.computeNamedDeclarations();
|
|
}
|
|
|
|
return this.namedDeclarations;
|
|
}
|
|
|
|
private computeNamedDeclarations(): ESMap<string, Declaration[]> {
|
|
const result = createMultiMap<Declaration>();
|
|
|
|
this.forEachChild(visit);
|
|
|
|
return result;
|
|
|
|
function addDeclaration(declaration: Declaration) {
|
|
const name = getDeclarationName(declaration);
|
|
if (name) {
|
|
result.add(name, declaration);
|
|
}
|
|
}
|
|
|
|
function getDeclarations(name: string) {
|
|
let declarations = result.get(name);
|
|
if (!declarations) {
|
|
result.set(name, declarations = []);
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
function getDeclarationName(declaration: Declaration) {
|
|
const name = getNonAssignedNameOfDeclaration(declaration);
|
|
return name && (isComputedPropertyName(name) && isPropertyAccessExpression(name.expression) ? name.expression.name.text
|
|
: isPropertyName(name) ? getNameFromPropertyName(name) : undefined);
|
|
}
|
|
|
|
function visit(node: Node): void {
|
|
switch (node.kind) {
|
|
case SyntaxKind.FunctionDeclaration:
|
|
case SyntaxKind.FunctionExpression:
|
|
case SyntaxKind.MethodDeclaration:
|
|
case SyntaxKind.MethodSignature:
|
|
const functionDeclaration = node as FunctionLikeDeclaration;
|
|
const declarationName = getDeclarationName(functionDeclaration);
|
|
|
|
if (declarationName) {
|
|
const declarations = getDeclarations(declarationName);
|
|
const lastDeclaration = lastOrUndefined(declarations);
|
|
|
|
// Check whether this declaration belongs to an "overload group".
|
|
if (lastDeclaration && functionDeclaration.parent === lastDeclaration.parent && functionDeclaration.symbol === lastDeclaration.symbol) {
|
|
// Overwrite the last declaration if it was an overload
|
|
// and this one is an implementation.
|
|
if (functionDeclaration.body && !(lastDeclaration as FunctionLikeDeclaration).body) {
|
|
declarations[declarations.length - 1] = functionDeclaration;
|
|
}
|
|
}
|
|
else {
|
|
declarations.push(functionDeclaration);
|
|
}
|
|
}
|
|
forEachChild(node, visit);
|
|
break;
|
|
|
|
case SyntaxKind.ClassDeclaration:
|
|
case SyntaxKind.ClassExpression:
|
|
case SyntaxKind.InterfaceDeclaration:
|
|
case SyntaxKind.TypeAliasDeclaration:
|
|
case SyntaxKind.EnumDeclaration:
|
|
case SyntaxKind.ModuleDeclaration:
|
|
case SyntaxKind.ImportEqualsDeclaration:
|
|
case SyntaxKind.ExportSpecifier:
|
|
case SyntaxKind.ImportSpecifier:
|
|
case SyntaxKind.ImportClause:
|
|
case SyntaxKind.NamespaceImport:
|
|
case SyntaxKind.GetAccessor:
|
|
case SyntaxKind.SetAccessor:
|
|
case SyntaxKind.TypeLiteral:
|
|
addDeclaration(node as Declaration);
|
|
forEachChild(node, visit);
|
|
break;
|
|
|
|
case SyntaxKind.Parameter:
|
|
// Only consider parameter properties
|
|
if (!hasSyntacticModifier(node, ModifierFlags.ParameterPropertyModifier)) {
|
|
break;
|
|
}
|
|
// falls through
|
|
|
|
case SyntaxKind.VariableDeclaration:
|
|
case SyntaxKind.BindingElement: {
|
|
const decl = node as VariableDeclaration;
|
|
if (isBindingPattern(decl.name)) {
|
|
forEachChild(decl.name, visit);
|
|
break;
|
|
}
|
|
if (decl.initializer) {
|
|
visit(decl.initializer);
|
|
}
|
|
}
|
|
// falls through
|
|
case SyntaxKind.EnumMember:
|
|
case SyntaxKind.PropertyDeclaration:
|
|
case SyntaxKind.PropertySignature:
|
|
addDeclaration(node as Declaration);
|
|
break;
|
|
|
|
case SyntaxKind.ExportDeclaration:
|
|
// Handle named exports case e.g.:
|
|
// export {a, b as B} from "mod";
|
|
const exportDeclaration = node as ExportDeclaration;
|
|
if (exportDeclaration.exportClause) {
|
|
if (isNamedExports(exportDeclaration.exportClause)) {
|
|
forEach(exportDeclaration.exportClause.elements, visit);
|
|
}
|
|
else {
|
|
visit(exportDeclaration.exportClause.name);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.ImportDeclaration:
|
|
const importClause = (node as ImportDeclaration).importClause;
|
|
if (importClause) {
|
|
// Handle default import case e.g.:
|
|
// import d from "mod";
|
|
if (importClause.name) {
|
|
addDeclaration(importClause.name);
|
|
}
|
|
|
|
// Handle named bindings in imports e.g.:
|
|
// import * as NS from "mod";
|
|
// import {a, b as B} from "mod";
|
|
if (importClause.namedBindings) {
|
|
if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) {
|
|
addDeclaration(importClause.namedBindings);
|
|
}
|
|
else {
|
|
forEach(importClause.namedBindings.elements, visit);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.BinaryExpression:
|
|
if (getAssignmentDeclarationKind(node as BinaryExpression) !== AssignmentDeclarationKind.None) {
|
|
addDeclaration(node as BinaryExpression);
|
|
}
|
|
// falls through
|
|
|
|
default:
|
|
forEachChild(node, visit);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class SourceMapSourceObject implements SourceMapSource {
|
|
lineMap!: number[];
|
|
constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { }
|
|
|
|
public getLineAndCharacterOfPosition(pos: number): LineAndCharacter {
|
|
return getLineAndCharacterOfPosition(this, pos);
|
|
}
|
|
}
|
|
|
|
function getServicesObjectAllocator(): ObjectAllocator {
|
|
return {
|
|
getNodeConstructor: () => NodeObject,
|
|
getTokenConstructor: () => TokenObject,
|
|
|
|
getIdentifierConstructor: () => IdentifierObject,
|
|
getPrivateIdentifierConstructor: () => PrivateIdentifierObject,
|
|
getSourceFileConstructor: () => SourceFileObject,
|
|
getSymbolConstructor: () => SymbolObject,
|
|
getTypeConstructor: () => TypeObject,
|
|
getSignatureConstructor: () => SignatureObject,
|
|
getSourceMapSourceConstructor: () => SourceMapSourceObject,
|
|
};
|
|
}
|
|
|
|
/// Language Service
|
|
|
|
// Information about a specific host file.
|
|
interface HostFileInformation {
|
|
hostFileName: string;
|
|
version: string;
|
|
scriptSnapshot: IScriptSnapshot;
|
|
scriptKind: ScriptKind;
|
|
}
|
|
|
|
/* @internal */
|
|
export interface DisplayPartsSymbolWriter extends EmitTextWriter {
|
|
displayParts(): SymbolDisplayPart[];
|
|
}
|
|
|
|
/* @internal */
|
|
export function toEditorSettings(options: FormatCodeOptions | FormatCodeSettings): FormatCodeSettings;
|
|
export function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings;
|
|
export function toEditorSettings(optionsAsMap: MapLike<any>): MapLike<any> {
|
|
let allPropertiesAreCamelCased = true;
|
|
for (const key in optionsAsMap) {
|
|
if (hasProperty(optionsAsMap, key) && !isCamelCase(key)) {
|
|
allPropertiesAreCamelCased = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allPropertiesAreCamelCased) {
|
|
return optionsAsMap;
|
|
}
|
|
const settings: MapLike<any> = {};
|
|
for (const key in optionsAsMap) {
|
|
if (hasProperty(optionsAsMap, key)) {
|
|
const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1);
|
|
settings[newKey] = optionsAsMap[key];
|
|
}
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
function isCamelCase(s: string) {
|
|
return !s.length || s.charAt(0) === s.charAt(0).toLowerCase();
|
|
}
|
|
|
|
export function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined) {
|
|
if (displayParts) {
|
|
return map(displayParts, displayPart => displayPart.text).join("");
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
export function getDefaultCompilerOptions(): CompilerOptions {
|
|
// Always default to "ScriptTarget.ES5" for the language service
|
|
return {
|
|
target: ScriptTarget.ES5,
|
|
jsx: JsxEmit.Preserve
|
|
};
|
|
}
|
|
|
|
export function getSupportedCodeFixes() {
|
|
return codefix.getSupportedErrorCodes();
|
|
}
|
|
|
|
// Either it will be file name if host doesnt have file or it will be the host's file information
|
|
type CachedHostFileInformation = HostFileInformation | string;
|
|
|
|
// Cache host information about script Should be refreshed
|
|
// at each language service public entry point, since we don't know when
|
|
// the set of scripts handled by the host changes.
|
|
class HostCache {
|
|
private fileNameToEntry: ESMap<Path, CachedHostFileInformation>;
|
|
private currentDirectory: string;
|
|
|
|
constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) {
|
|
// script id => script index
|
|
this.currentDirectory = host.getCurrentDirectory();
|
|
this.fileNameToEntry = new Map();
|
|
|
|
// Initialize the list with the root file names
|
|
const rootFileNames = host.getScriptFileNames();
|
|
for (const fileName of rootFileNames) {
|
|
this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
|
|
}
|
|
}
|
|
|
|
private createEntry(fileName: string, path: Path) {
|
|
let entry: CachedHostFileInformation;
|
|
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
|
|
if (scriptSnapshot) {
|
|
entry = {
|
|
hostFileName: fileName,
|
|
version: this.host.getScriptVersion(fileName),
|
|
scriptSnapshot,
|
|
scriptKind: getScriptKind(fileName, this.host)
|
|
};
|
|
}
|
|
else {
|
|
entry = fileName;
|
|
}
|
|
|
|
this.fileNameToEntry.set(path, entry);
|
|
return entry;
|
|
}
|
|
|
|
public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
|
|
return this.fileNameToEntry.get(path);
|
|
}
|
|
|
|
public getHostFileInformation(path: Path): HostFileInformation | undefined {
|
|
const entry = this.fileNameToEntry.get(path);
|
|
return !isString(entry) ? entry : undefined;
|
|
}
|
|
|
|
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
|
|
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
|
|
return isString(info) ? undefined! : info; // TODO: GH#18217
|
|
}
|
|
|
|
public getRootFileNames(): string[] {
|
|
const names: string[] = [];
|
|
this.fileNameToEntry.forEach(entry => {
|
|
if (isString(entry)) {
|
|
names.push(entry);
|
|
}
|
|
else {
|
|
names.push(entry.hostFileName);
|
|
}
|
|
});
|
|
return names;
|
|
}
|
|
|
|
public getScriptSnapshot(path: Path): IScriptSnapshot {
|
|
const file = this.getHostFileInformation(path);
|
|
return (file && file.scriptSnapshot)!; // TODO: GH#18217
|
|
}
|
|
}
|
|
|
|
class SyntaxTreeCache {
|
|
// For our syntactic only features, we also keep a cache of the syntax tree for the
|
|
// currently edited file.
|
|
private currentFileName: string | undefined;
|
|
private currentFileVersion: string | undefined;
|
|
private currentFileScriptSnapshot: IScriptSnapshot | undefined;
|
|
private currentSourceFile: SourceFile | undefined;
|
|
|
|
constructor(private host: LanguageServiceHost) {
|
|
}
|
|
|
|
public getCurrentSourceFile(fileName: string): SourceFile {
|
|
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
|
|
if (!scriptSnapshot) {
|
|
// The host does not know about this file.
|
|
throw new Error("Could not find file: '" + fileName + "'.");
|
|
}
|
|
|
|
const scriptKind = getScriptKind(fileName, this.host);
|
|
const version = this.host.getScriptVersion(fileName);
|
|
let sourceFile: SourceFile | undefined;
|
|
|
|
if (this.currentFileName !== fileName) {
|
|
// This is a new file, just parse it
|
|
sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind);
|
|
}
|
|
else if (this.currentFileVersion !== version) {
|
|
// This is the same file, just a newer version. Incrementally parse the file.
|
|
const editRange = scriptSnapshot.getChangeRange(this.currentFileScriptSnapshot!);
|
|
sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile!, scriptSnapshot, version, editRange);
|
|
}
|
|
|
|
if (sourceFile) {
|
|
// All done, ensure state is up to date
|
|
this.currentFileVersion = version;
|
|
this.currentFileName = fileName;
|
|
this.currentFileScriptSnapshot = scriptSnapshot;
|
|
this.currentSourceFile = sourceFile;
|
|
}
|
|
|
|
return this.currentSourceFile!;
|
|
}
|
|
}
|
|
|
|
function setSourceFileFields(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string) {
|
|
sourceFile.version = version;
|
|
sourceFile.scriptSnapshot = scriptSnapshot;
|
|
}
|
|
|
|
export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile {
|
|
const sourceFile = createSourceFile(fileName, getSnapshotText(scriptSnapshot), scriptTarget, setNodeParents, scriptKind);
|
|
setSourceFileFields(sourceFile, scriptSnapshot, version);
|
|
return sourceFile;
|
|
}
|
|
|
|
export function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile {
|
|
// If we were given a text change range, and our version or open-ness changed, then
|
|
// incrementally parse this file.
|
|
if (textChangeRange) {
|
|
if (version !== sourceFile.version) {
|
|
let newText: string;
|
|
|
|
// grab the fragment from the beginning of the original text to the beginning of the span
|
|
const prefix = textChangeRange.span.start !== 0
|
|
? sourceFile.text.substr(0, textChangeRange.span.start)
|
|
: "";
|
|
|
|
// grab the fragment from the end of the span till the end of the original text
|
|
const suffix = textSpanEnd(textChangeRange.span) !== sourceFile.text.length
|
|
? sourceFile.text.substr(textSpanEnd(textChangeRange.span))
|
|
: "";
|
|
|
|
if (textChangeRange.newLength === 0) {
|
|
// edit was a deletion - just combine prefix and suffix
|
|
newText = prefix && suffix ? prefix + suffix : prefix || suffix;
|
|
}
|
|
else {
|
|
// it was actual edit, fetch the fragment of new text that correspond to new span
|
|
const changedText = scriptSnapshot.getText(textChangeRange.span.start, textChangeRange.span.start + textChangeRange.newLength);
|
|
// combine prefix, changed text and suffix
|
|
newText = prefix && suffix
|
|
? prefix + changedText + suffix
|
|
: prefix
|
|
? (prefix + changedText)
|
|
: (changedText + suffix);
|
|
}
|
|
|
|
const newSourceFile = updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks);
|
|
setSourceFileFields(newSourceFile, scriptSnapshot, version);
|
|
// after incremental parsing nameTable might not be up-to-date
|
|
// drop it so it can be lazily recreated later
|
|
newSourceFile.nameTable = undefined;
|
|
|
|
// dispose all resources held by old script snapshot
|
|
if (sourceFile !== newSourceFile && sourceFile.scriptSnapshot) {
|
|
if (sourceFile.scriptSnapshot.dispose) {
|
|
sourceFile.scriptSnapshot.dispose();
|
|
}
|
|
|
|
sourceFile.scriptSnapshot = undefined;
|
|
}
|
|
|
|
return newSourceFile;
|
|
}
|
|
}
|
|
|
|
// Otherwise, just create a new source file.
|
|
return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true, sourceFile.scriptKind);
|
|
}
|
|
|
|
const NoopCancellationToken: CancellationToken = {
|
|
isCancellationRequested: returnFalse,
|
|
throwIfCancellationRequested: noop,
|
|
};
|
|
|
|
class CancellationTokenObject implements CancellationToken {
|
|
constructor(private cancellationToken: HostCancellationToken) {
|
|
}
|
|
|
|
public isCancellationRequested(): boolean {
|
|
return this.cancellationToken.isCancellationRequested();
|
|
}
|
|
|
|
public throwIfCancellationRequested(): void {
|
|
if (this.isCancellationRequested()) {
|
|
tracing?.instant(tracing.Phase.Session, "cancellationThrown", { kind: "CancellationTokenObject" });
|
|
throw new OperationCanceledException();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* @internal */
|
|
/** A cancellation that throttles calls to the host */
|
|
export class ThrottledCancellationToken implements CancellationToken {
|
|
// Store when we last tried to cancel. Checking cancellation can be expensive (as we have
|
|
// to marshall over to the host layer). So we only bother actually checking once enough
|
|
// time has passed.
|
|
private lastCancellationCheckTime = 0;
|
|
|
|
constructor(private hostCancellationToken: HostCancellationToken, private readonly throttleWaitMilliseconds = 20) {
|
|
}
|
|
|
|
public isCancellationRequested(): boolean {
|
|
const time = timestamp();
|
|
const duration = Math.abs(time - this.lastCancellationCheckTime);
|
|
if (duration >= this.throttleWaitMilliseconds) {
|
|
// Check no more than once every throttle wait milliseconds
|
|
this.lastCancellationCheckTime = time;
|
|
return this.hostCancellationToken.isCancellationRequested();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public throwIfCancellationRequested(): void {
|
|
if (this.isCancellationRequested()) {
|
|
tracing?.instant(tracing.Phase.Session, "cancellationThrown", { kind: "ThrottledCancellationToken" });
|
|
throw new OperationCanceledException();
|
|
}
|
|
}
|
|
}
|
|
|
|
const invalidOperationsInPartialSemanticMode: readonly (keyof LanguageService)[] = [
|
|
"getSemanticDiagnostics",
|
|
"getSuggestionDiagnostics",
|
|
"getCompilerOptionsDiagnostics",
|
|
"getSemanticClassifications",
|
|
"getEncodedSemanticClassifications",
|
|
"getCodeFixesAtPosition",
|
|
"getCombinedCodeFix",
|
|
"applyCodeActionCommand",
|
|
"organizeImports",
|
|
"getEditsForFileRename",
|
|
"getEmitOutput",
|
|
"getApplicableRefactors",
|
|
"getEditsForRefactor",
|
|
"prepareCallHierarchy",
|
|
"provideCallHierarchyIncomingCalls",
|
|
"provideCallHierarchyOutgoingCalls",
|
|
"provideInlayHints"
|
|
];
|
|
|
|
const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [
|
|
...invalidOperationsInPartialSemanticMode,
|
|
"getCompletionsAtPosition",
|
|
"getCompletionEntryDetails",
|
|
"getCompletionEntrySymbol",
|
|
"getSignatureHelpItems",
|
|
"getQuickInfoAtPosition",
|
|
"getDefinitionAtPosition",
|
|
"getDefinitionAndBoundSpan",
|
|
"getImplementationAtPosition",
|
|
"getTypeDefinitionAtPosition",
|
|
"getReferencesAtPosition",
|
|
"findReferences",
|
|
"getOccurrencesAtPosition",
|
|
"getDocumentHighlights",
|
|
"getNavigateToItems",
|
|
"getRenameInfo",
|
|
"findRenameLocations",
|
|
"getApplicableRefactors",
|
|
];
|
|
export function createLanguageService(
|
|
host: LanguageServiceHost,
|
|
documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()),
|
|
syntaxOnlyOrLanguageServiceMode?: boolean | LanguageServiceMode,
|
|
): LanguageService {
|
|
let languageServiceMode: LanguageServiceMode;
|
|
if (syntaxOnlyOrLanguageServiceMode === undefined) {
|
|
languageServiceMode = LanguageServiceMode.Semantic;
|
|
}
|
|
else if (typeof syntaxOnlyOrLanguageServiceMode === "boolean") {
|
|
// languageServiceMode = SyntaxOnly
|
|
languageServiceMode = syntaxOnlyOrLanguageServiceMode ? LanguageServiceMode.Syntactic : LanguageServiceMode.Semantic;
|
|
}
|
|
else {
|
|
languageServiceMode = syntaxOnlyOrLanguageServiceMode;
|
|
}
|
|
|
|
const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host);
|
|
let program: Program;
|
|
let lastProjectVersion: string;
|
|
let lastTypesRootVersion = 0;
|
|
|
|
const cancellationToken = host.getCancellationToken
|
|
? new CancellationTokenObject(host.getCancellationToken())
|
|
: NoopCancellationToken;
|
|
|
|
const currentDirectory = host.getCurrentDirectory();
|
|
// Check if the localized messages json is set, otherwise query the host for it
|
|
if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) {
|
|
setLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages());
|
|
}
|
|
|
|
function log(message: string) {
|
|
if (host.log) {
|
|
host.log(message);
|
|
}
|
|
}
|
|
|
|
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
|
|
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
|
|
|
|
const sourceMapper = getSourceMapper({
|
|
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
|
|
getCurrentDirectory: () => currentDirectory,
|
|
getProgram,
|
|
fileExists: maybeBind(host, host.fileExists),
|
|
readFile: maybeBind(host, host.readFile),
|
|
getDocumentPositionMapper: maybeBind(host, host.getDocumentPositionMapper),
|
|
getSourceFileLike: maybeBind(host, host.getSourceFileLike),
|
|
log
|
|
});
|
|
|
|
function getValidSourceFile(fileName: string): SourceFile {
|
|
const sourceFile = program.getSourceFile(fileName);
|
|
if (!sourceFile) {
|
|
const error: Error & PossibleProgramFileInfo = new Error(`Could not find source file: '${fileName}'.`);
|
|
|
|
// We've been having trouble debugging this, so attach sidecar data for the tsserver log.
|
|
// See https://github.com/microsoft/TypeScript/issues/30180.
|
|
error.ProgramFiles = program.getSourceFiles().map(f => f.fileName);
|
|
|
|
throw error;
|
|
}
|
|
return sourceFile;
|
|
}
|
|
|
|
function synchronizeHostData(): void {
|
|
Debug.assert(languageServiceMode !== LanguageServiceMode.Syntactic);
|
|
// perform fast check if host supports it
|
|
if (host.getProjectVersion) {
|
|
const hostProjectVersion = host.getProjectVersion();
|
|
if (hostProjectVersion) {
|
|
if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames?.()) {
|
|
return;
|
|
}
|
|
|
|
lastProjectVersion = hostProjectVersion;
|
|
}
|
|
}
|
|
|
|
const typeRootsVersion = host.getTypeRootsVersion ? host.getTypeRootsVersion() : 0;
|
|
if (lastTypesRootVersion !== typeRootsVersion) {
|
|
log("TypeRoots version has changed; provide new program");
|
|
program = undefined!; // TODO: GH#18217
|
|
lastTypesRootVersion = typeRootsVersion;
|
|
}
|
|
|
|
// Get a fresh cache of the host information
|
|
let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName);
|
|
const rootFileNames = hostCache.getRootFileNames();
|
|
const newSettings = host.getCompilationSettings() || getDefaultCompilerOptions();
|
|
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
|
|
const hasChangedAutomaticTypeDirectiveNames = maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames);
|
|
const projectReferences = host.getProjectReferences?.();
|
|
let parsedCommandLines: ESMap<Path, ParsedCommandLine | false> | undefined;
|
|
const parseConfigHost: ParseConfigFileHost = {
|
|
useCaseSensitiveFileNames,
|
|
fileExists,
|
|
readFile,
|
|
readDirectory,
|
|
trace: maybeBind(host, host.trace),
|
|
getCurrentDirectory: () => currentDirectory,
|
|
onUnRecoverableConfigFileDiagnostic: noop,
|
|
};
|
|
|
|
// If the program is already up-to-date, we can reuse it
|
|
if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
|
|
return;
|
|
}
|
|
|
|
// IMPORTANT - It is critical from this moment onward that we do not check
|
|
// cancellation tokens. We are about to mutate source files from a previous program
|
|
// instance. If we cancel midway through, we may end up in an inconsistent state where
|
|
// the program points to old source files that have been invalidated because of
|
|
// incremental parsing.
|
|
|
|
// Now create a new compiler
|
|
const compilerHost: CompilerHost = {
|
|
getSourceFile: getOrCreateSourceFile,
|
|
getSourceFileByPath: getOrCreateSourceFileByPath,
|
|
getCancellationToken: () => cancellationToken,
|
|
getCanonicalFileName,
|
|
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
|
|
getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)),
|
|
getDefaultLibFileName: options => host.getDefaultLibFileName(options),
|
|
writeFile: noop,
|
|
getCurrentDirectory: () => currentDirectory,
|
|
fileExists,
|
|
readFile,
|
|
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
|
|
realpath: maybeBind(host, host.realpath),
|
|
directoryExists: directoryName => {
|
|
return directoryProbablyExists(directoryName, host);
|
|
},
|
|
getDirectories: path => {
|
|
return host.getDirectories ? host.getDirectories(path) : [];
|
|
},
|
|
readDirectory,
|
|
onReleaseOldSourceFile,
|
|
onReleaseParsedCommandLine,
|
|
hasInvalidatedResolution,
|
|
hasChangedAutomaticTypeDirectiveNames,
|
|
trace: parseConfigHost.trace,
|
|
resolveModuleNames: maybeBind(host, host.resolveModuleNames),
|
|
resolveTypeReferenceDirectives: maybeBind(host, host.resolveTypeReferenceDirectives),
|
|
useSourceOfProjectReferenceRedirect: maybeBind(host, host.useSourceOfProjectReferenceRedirect),
|
|
getParsedCommandLine,
|
|
};
|
|
host.setCompilerHost?.(compilerHost);
|
|
|
|
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
|
|
const options: CreateProgramOptions = {
|
|
rootNames: rootFileNames,
|
|
options: newSettings,
|
|
host: compilerHost,
|
|
oldProgram: program,
|
|
projectReferences
|
|
};
|
|
program = createProgram(options);
|
|
|
|
// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
|
|
// It needs to be cleared to allow all collected snapshots to be released
|
|
hostCache = undefined;
|
|
parsedCommandLines = undefined;
|
|
|
|
// We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above,
|
|
// Because it only functions until `hostCache` is cleared, while we'll potentially need the functionality to lazily read sourcemap files during
|
|
// the course of whatever called `synchronizeHostData`
|
|
sourceMapper.clearCache();
|
|
|
|
// Make sure all the nodes in the program are both bound, and have their parent
|
|
// pointers set property.
|
|
program.getTypeChecker();
|
|
return;
|
|
|
|
function getParsedCommandLine(fileName: string): ParsedCommandLine | undefined {
|
|
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
|
const existing = parsedCommandLines?.get(path);
|
|
if (existing !== undefined) return existing || undefined;
|
|
|
|
const result = host.getParsedCommandLine ?
|
|
host.getParsedCommandLine(fileName) :
|
|
getParsedCommandLineOfConfigFileUsingSourceFile(fileName);
|
|
(parsedCommandLines ||= new Map()).set(path, result || false);
|
|
return result;
|
|
}
|
|
|
|
function getParsedCommandLineOfConfigFileUsingSourceFile(configFileName: string): ParsedCommandLine | undefined {
|
|
const result = getOrCreateSourceFile(configFileName, ScriptTarget.JSON) as JsonSourceFile | undefined;
|
|
if (!result) return undefined;
|
|
result.path = toPath(configFileName, currentDirectory, getCanonicalFileName);
|
|
result.resolvedPath = result.path;
|
|
result.originalFileName = result.fileName;
|
|
return parseJsonSourceFileConfigFileContent(
|
|
result,
|
|
parseConfigHost,
|
|
getNormalizedAbsolutePath(getDirectoryPath(configFileName), currentDirectory),
|
|
/*optionsToExtend*/ undefined,
|
|
getNormalizedAbsolutePath(configFileName, currentDirectory),
|
|
);
|
|
}
|
|
|
|
function onReleaseParsedCommandLine(configFileName: string, oldResolvedRef: ResolvedProjectReference | undefined, oldOptions: CompilerOptions) {
|
|
if (host.getParsedCommandLine) {
|
|
host.onReleaseParsedCommandLine?.(configFileName, oldResolvedRef, oldOptions);
|
|
}
|
|
else if (oldResolvedRef) {
|
|
onReleaseOldSourceFile(oldResolvedRef.sourceFile, oldOptions);
|
|
}
|
|
}
|
|
|
|
function fileExists(fileName: string): boolean {
|
|
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
|
const entry = hostCache && hostCache.getEntryByPath(path);
|
|
return entry ?
|
|
!isString(entry) :
|
|
(!!host.fileExists && host.fileExists(fileName));
|
|
}
|
|
|
|
function readFile(fileName: string) {
|
|
// stub missing host functionality
|
|
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
|
const entry = hostCache && hostCache.getEntryByPath(path);
|
|
if (entry) {
|
|
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
|
|
}
|
|
return host.readFile && host.readFile(fileName);
|
|
}
|
|
|
|
function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) {
|
|
Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
|
|
return host.readDirectory!(path, extensions, exclude, include, depth);
|
|
}
|
|
|
|
// Release any files we have acquired in the old program but are
|
|
// not part of the new program.
|
|
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
|
|
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions);
|
|
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey, oldSourceFile.scriptKind);
|
|
}
|
|
|
|
function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
|
|
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
|
|
}
|
|
|
|
function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
|
|
Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
|
|
// The program is asking for this file, check first if the host can locate it.
|
|
// If the host can not locate the file, then it does not exist. return undefined
|
|
// to the program to allow reporting of errors for missing files.
|
|
const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path);
|
|
if (!hostFileInformation) {
|
|
return undefined;
|
|
}
|
|
|
|
// Check if the language version has changed since we last created a program; if they are the same,
|
|
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
|
|
// can not be reused. we have to dump all syntax trees and create new ones.
|
|
if (!shouldCreateNewSourceFile) {
|
|
// Check if the old program had this file already
|
|
const oldSourceFile = program && program.getSourceFileByPath(path);
|
|
if (oldSourceFile) {
|
|
// We already had a source file for this file name. Go to the registry to
|
|
// ensure that we get the right up to date version of it. We need this to
|
|
// address the following race-condition. Specifically, say we have the following:
|
|
//
|
|
// LS1
|
|
// \
|
|
// DocumentRegistry
|
|
// /
|
|
// LS2
|
|
//
|
|
// Each LS has a reference to file 'foo.ts' at version 1. LS2 then updates
|
|
// it's version of 'foo.ts' to version 2. This will cause LS2 and the
|
|
// DocumentRegistry to have version 2 of the document. However, LS1 will
|
|
// have version 1. And *importantly* this source file will be *corrupt*.
|
|
// The act of creating version 2 of the file irrevocably damages the version
|
|
// 1 file.
|
|
//
|
|
// So, later when we call into LS1, we need to make sure that it doesn't use
|
|
// it's source file any more, and instead defers to DocumentRegistry to get
|
|
// either version 1, version 2 (or some other version) depending on what the
|
|
// host says should be used.
|
|
|
|
// We do not support the scenario where a host can modify a registered
|
|
// file's script kind, i.e. in one project some file is treated as ".ts"
|
|
// and in another as ".js"
|
|
if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) {
|
|
return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
|
|
}
|
|
else {
|
|
// Release old source file and fall through to aquire new file with new script kind
|
|
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()), oldSourceFile.scriptKind);
|
|
}
|
|
}
|
|
|
|
// We didn't already have the file. Fall through and acquire it from the registry.
|
|
}
|
|
|
|
// Could not find this file in the old program, create a new SourceFile for it.
|
|
return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
|
|
}
|
|
}
|
|
|
|
// TODO: GH#18217 frequently asserted as defined
|
|
function getProgram(): Program | undefined {
|
|
if (languageServiceMode === LanguageServiceMode.Syntactic) {
|
|
Debug.assert(program === undefined);
|
|
return undefined;
|
|
}
|
|
|
|
synchronizeHostData();
|
|
|
|
return program;
|
|
}
|
|
|
|
function getAutoImportProvider(): Program | undefined {
|
|
return host.getPackageJsonAutoImportProvider?.();
|
|
}
|
|
|
|
function cleanupSemanticCache(): void {
|
|
program = undefined!; // TODO: GH#18217
|
|
}
|
|
|
|
function dispose(): void {
|
|
if (program) {
|
|
// Use paths to ensure we are using correct key and paths as document registry could be created with different current directory than host
|
|
const key = documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions());
|
|
forEach(program.getSourceFiles(), f =>
|
|
documentRegistry.releaseDocumentWithKey(f.resolvedPath, key, f.scriptKind));
|
|
program = undefined!; // TODO: GH#18217
|
|
}
|
|
host = undefined!;
|
|
}
|
|
|
|
/// Diagnostics
|
|
function getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[] {
|
|
synchronizeHostData();
|
|
|
|
return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice();
|
|
}
|
|
|
|
/**
|
|
* getSemanticDiagnostics return array of Diagnostics. If '-d' is not enabled, only report semantic errors
|
|
* If '-d' enabled, report both semantic and emitter errors
|
|
*/
|
|
function getSemanticDiagnostics(fileName: string): Diagnostic[] {
|
|
synchronizeHostData();
|
|
|
|
const targetSourceFile = getValidSourceFile(fileName);
|
|
|
|
// Only perform the action per file regardless of '-out' flag as LanguageServiceHost is expected to call this function per file.
|
|
// Therefore only get diagnostics for given file.
|
|
|
|
const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken);
|
|
if (!getEmitDeclarations(program.getCompilerOptions())) {
|
|
return semanticDiagnostics.slice();
|
|
}
|
|
|
|
// If '-d' is enabled, check for emitter error. One example of emitter error is export class implements non-export interface
|
|
const declarationDiagnostics = program.getDeclarationDiagnostics(targetSourceFile, cancellationToken);
|
|
return [...semanticDiagnostics, ...declarationDiagnostics];
|
|
}
|
|
|
|
function getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[] {
|
|
synchronizeHostData();
|
|
return computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken);
|
|
}
|
|
|
|
function getCompilerOptionsDiagnostics() {
|
|
synchronizeHostData();
|
|
return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)];
|
|
}
|
|
|
|
function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined {
|
|
// Convert from deprecated options names to new names
|
|
const fullPreferences: UserPreferences = {
|
|
...identity<UserPreferences>(options), // avoid excess property check
|
|
includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports,
|
|
includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions,
|
|
};
|
|
synchronizeHostData();
|
|
return Completions.getCompletionsAtPosition(
|
|
host,
|
|
program,
|
|
log,
|
|
getValidSourceFile(fileName),
|
|
position,
|
|
fullPreferences,
|
|
options.triggerCharacter,
|
|
options.triggerKind,
|
|
cancellationToken);
|
|
}
|
|
|
|
function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions, data?: CompletionEntryData): CompletionEntryDetails | undefined {
|
|
synchronizeHostData();
|
|
return Completions.getCompletionEntryDetails(
|
|
program,
|
|
log,
|
|
getValidSourceFile(fileName),
|
|
position,
|
|
{ name, source, data },
|
|
host,
|
|
(formattingOptions && formatting.getFormatContext(formattingOptions, host))!, // TODO: GH#18217
|
|
preferences,
|
|
cancellationToken,
|
|
);
|
|
}
|
|
|
|
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string, preferences: UserPreferences = emptyOptions): Symbol | undefined {
|
|
synchronizeHostData();
|
|
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host, preferences);
|
|
}
|
|
|
|
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const node = getTouchingPropertyName(sourceFile, position);
|
|
if (node === sourceFile) {
|
|
// Avoid giving quickInfo for the sourceFile as a whole.
|
|
return undefined;
|
|
}
|
|
|
|
const typeChecker = program.getTypeChecker();
|
|
const nodeForQuickInfo = getNodeForQuickInfo(node);
|
|
const symbol = getSymbolAtLocationForQuickInfo(nodeForQuickInfo, typeChecker);
|
|
|
|
if (!symbol || typeChecker.isUnknownSymbol(symbol)) {
|
|
const type = shouldGetType(sourceFile, nodeForQuickInfo, position) ? typeChecker.getTypeAtLocation(nodeForQuickInfo) : undefined;
|
|
return type && {
|
|
kind: ScriptElementKind.unknown,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
|
|
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))),
|
|
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
|
|
tags: type.symbol ? type.symbol.getJsDocTags(typeChecker) : undefined
|
|
};
|
|
}
|
|
|
|
const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
|
|
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo)
|
|
);
|
|
return {
|
|
kind: symbolKind,
|
|
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
|
|
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
|
|
displayParts,
|
|
documentation,
|
|
tags,
|
|
};
|
|
}
|
|
|
|
function getNodeForQuickInfo(node: Node): Node {
|
|
if (isNewExpression(node.parent) && node.pos === node.parent.pos) {
|
|
return node.parent.expression;
|
|
}
|
|
if (isNamedTupleMember(node.parent) && node.pos === node.parent.pos) {
|
|
return node.parent;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function shouldGetType(sourceFile: SourceFile, node: Node, position: number): boolean {
|
|
switch (node.kind) {
|
|
case SyntaxKind.Identifier:
|
|
return !isLabelName(node) && !isTagName(node) && !isConstTypeReference(node.parent);
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
case SyntaxKind.QualifiedName:
|
|
// Don't return quickInfo if inside the comment in `a/**/.b`
|
|
return !isInComment(sourceFile, position);
|
|
case SyntaxKind.ThisKeyword:
|
|
case SyntaxKind.ThisType:
|
|
case SyntaxKind.SuperKeyword:
|
|
case SyntaxKind.NamedTupleMember:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Goto definition
|
|
function getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
/// Goto implementation
|
|
|
|
function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined {
|
|
synchronizeHostData();
|
|
return FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
/// References and Occurrences
|
|
function getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined {
|
|
return flatMap(
|
|
getDocumentHighlights(fileName, position, [fileName]),
|
|
entry => entry.highlightSpans.map<ReferenceEntry>(highlightSpan => ({
|
|
fileName: entry.fileName,
|
|
textSpan: highlightSpan.textSpan,
|
|
isWriteAccess: highlightSpan.kind === HighlightSpanKind.writtenReference,
|
|
isDefinition: false,
|
|
...highlightSpan.isInString && { isInString: true },
|
|
...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan }
|
|
}))
|
|
);
|
|
}
|
|
|
|
function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): DocumentHighlights[] | undefined {
|
|
const normalizedFileName = normalizePath(fileName);
|
|
Debug.assert(filesToSearch.some(f => normalizePath(f) === normalizedFileName));
|
|
synchronizeHostData();
|
|
const sourceFilesToSearch = mapDefined(filesToSearch, fileName => program.getSourceFile(fileName));
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch);
|
|
}
|
|
|
|
function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position));
|
|
if (isIdentifier(node) && (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) && isIntrinsicJsxName(node.escapedText)) {
|
|
const { openingElement, closingElement } = node.parent.parent;
|
|
return [openingElement, closingElement].map((node): RenameLocation => {
|
|
const textSpan = createTextSpanFromNode(node.tagName, sourceFile);
|
|
return {
|
|
fileName: sourceFile.fileName,
|
|
textSpan,
|
|
...FindAllReferences.toContextSpan(textSpan, sourceFile, node.parent)
|
|
};
|
|
});
|
|
}
|
|
else {
|
|
return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, use: FindAllReferences.FindReferencesUse.Rename },
|
|
(entry, originalNode, checker) => FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false));
|
|
}
|
|
}
|
|
|
|
function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined {
|
|
synchronizeHostData();
|
|
return getReferencesWorker(getTouchingPropertyName(getValidSourceFile(fileName), position), position, { use: FindAllReferences.FindReferencesUse.References }, FindAllReferences.toReferenceEntry);
|
|
}
|
|
|
|
function getReferencesWorker<T>(node: Node, position: number, options: FindAllReferences.Options, cb: FindAllReferences.ToReferenceOrRenameEntry<T>): T[] | undefined {
|
|
synchronizeHostData();
|
|
|
|
// Exclude default library when renaming as commonly user don't want to change that file.
|
|
const sourceFiles = options && options.use === FindAllReferences.FindReferencesUse.Rename
|
|
? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile))
|
|
: program.getSourceFiles();
|
|
|
|
return FindAllReferences.findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb);
|
|
}
|
|
|
|
function findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined {
|
|
synchronizeHostData();
|
|
return FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getFileReferences(fileName: string): ReferenceEntry[] {
|
|
synchronizeHostData();
|
|
return FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(FindAllReferences.toReferenceEntry);
|
|
}
|
|
|
|
function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] {
|
|
synchronizeHostData();
|
|
const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles();
|
|
return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles);
|
|
}
|
|
|
|
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
|
|
return getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit);
|
|
}
|
|
|
|
// Signature help
|
|
/**
|
|
* This is a semantic operation.
|
|
*/
|
|
function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
|
|
return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken);
|
|
}
|
|
|
|
/// Syntactic features
|
|
function getNonBoundSourceFile(fileName: string): SourceFile {
|
|
return syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
}
|
|
|
|
function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): TextSpan | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
// Get node at the location
|
|
const node = getTouchingPropertyName(sourceFile, startPos);
|
|
|
|
if (node === sourceFile) {
|
|
return undefined;
|
|
}
|
|
|
|
switch (node.kind) {
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
case SyntaxKind.QualifiedName:
|
|
case SyntaxKind.StringLiteral:
|
|
case SyntaxKind.FalseKeyword:
|
|
case SyntaxKind.TrueKeyword:
|
|
case SyntaxKind.NullKeyword:
|
|
case SyntaxKind.SuperKeyword:
|
|
case SyntaxKind.ThisKeyword:
|
|
case SyntaxKind.ThisType:
|
|
case SyntaxKind.Identifier:
|
|
break;
|
|
|
|
// Cant create the text span
|
|
default:
|
|
return undefined;
|
|
}
|
|
|
|
let nodeForStartPos = node;
|
|
while (true) {
|
|
if (isRightSideOfPropertyAccess(nodeForStartPos) || isRightSideOfQualifiedName(nodeForStartPos)) {
|
|
// If on the span is in right side of the the property or qualified name, return the span from the qualified name pos to end of this node
|
|
nodeForStartPos = nodeForStartPos.parent;
|
|
}
|
|
else if (isNameOfModuleDeclaration(nodeForStartPos)) {
|
|
// If this is name of a module declarations, check if this is right side of dotted module name
|
|
// If parent of the module declaration which is parent of this node is module declaration and its body is the module declaration that this node is name of
|
|
// Then this name is name from dotted module
|
|
if (nodeForStartPos.parent.parent.kind === SyntaxKind.ModuleDeclaration &&
|
|
(nodeForStartPos.parent.parent as ModuleDeclaration).body === nodeForStartPos.parent) {
|
|
// Use parent module declarations name for start pos
|
|
nodeForStartPos = (nodeForStartPos.parent.parent as ModuleDeclaration).name;
|
|
}
|
|
else {
|
|
// We have to use this name for start pos
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
// Is not a member expression so we have found the node for start pos
|
|
break;
|
|
}
|
|
}
|
|
|
|
return createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd());
|
|
}
|
|
|
|
function getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
return BreakpointResolver.spanInSourceFileAtLocation(sourceFile, position);
|
|
}
|
|
|
|
function getNavigationBarItems(fileName: string): NavigationBarItem[] {
|
|
return NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken);
|
|
}
|
|
|
|
function getNavigationTree(fileName: string): NavigationTree {
|
|
return NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken);
|
|
}
|
|
|
|
function getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[];
|
|
function getSemanticClassifications(fileName: string, span: TextSpan, format?: SemanticClassificationFormat): ClassifiedSpan[] | ClassifiedSpan2020[] {
|
|
synchronizeHostData();
|
|
|
|
const responseFormat = format || SemanticClassificationFormat.Original;
|
|
if (responseFormat === SemanticClassificationFormat.TwentyTwenty) {
|
|
return classifier.v2020.getSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span);
|
|
}
|
|
else {
|
|
return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span);
|
|
}
|
|
}
|
|
|
|
function getEncodedSemanticClassifications(fileName: string, span: TextSpan, format?: SemanticClassificationFormat): Classifications {
|
|
synchronizeHostData();
|
|
|
|
const responseFormat = format || SemanticClassificationFormat.Original;
|
|
if (responseFormat === SemanticClassificationFormat.Original) {
|
|
return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span);
|
|
}
|
|
else {
|
|
return classifier.v2020.getEncodedSemanticClassifications(program, cancellationToken, getValidSourceFile(fileName), span);
|
|
}
|
|
}
|
|
|
|
function getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span);
|
|
}
|
|
|
|
function getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span);
|
|
}
|
|
|
|
function getOutliningSpans(fileName: string): OutliningSpan[] {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
return OutliningElementsCollector.collectElements(sourceFile, cancellationToken);
|
|
}
|
|
|
|
const braceMatching = new Map(getEntries({
|
|
[SyntaxKind.OpenBraceToken]: SyntaxKind.CloseBraceToken,
|
|
[SyntaxKind.OpenParenToken]: SyntaxKind.CloseParenToken,
|
|
[SyntaxKind.OpenBracketToken]: SyntaxKind.CloseBracketToken,
|
|
[SyntaxKind.GreaterThanToken]: SyntaxKind.LessThanToken,
|
|
}));
|
|
braceMatching.forEach((value, key) => braceMatching.set(value.toString(), Number(key) as SyntaxKind));
|
|
|
|
function getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const token = getTouchingToken(sourceFile, position);
|
|
const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined;
|
|
const match = matchKind && findChildOfKind(token.parent, matchKind, sourceFile);
|
|
// We want to order the braces when we return the result.
|
|
return match ? [createTextSpanFromNode(token, sourceFile), createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : emptyArray;
|
|
}
|
|
|
|
function getIndentationAtPosition(fileName: string, position: number, editorOptions: EditorOptions | EditorSettings) {
|
|
let start = timestamp();
|
|
const settings = toEditorSettings(editorOptions);
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
log("getIndentationAtPosition: getCurrentSourceFile: " + (timestamp() - start));
|
|
|
|
start = timestamp();
|
|
|
|
const result = formatting.SmartIndenter.getIndentation(position, sourceFile, settings);
|
|
log("getIndentationAtPosition: computeIndentation : " + (timestamp() - start));
|
|
|
|
return result;
|
|
}
|
|
|
|
function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
return formatting.formatSelection(start, end, sourceFile, formatting.getFormatContext(toEditorSettings(options), host));
|
|
}
|
|
|
|
function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
return formatting.formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), formatting.getFormatContext(toEditorSettings(options), host));
|
|
}
|
|
|
|
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const formatContext = formatting.getFormatContext(toEditorSettings(options), host);
|
|
|
|
if (!isInComment(sourceFile, position)) {
|
|
switch (key) {
|
|
case "{":
|
|
return formatting.formatOnOpeningCurly(position, sourceFile, formatContext);
|
|
case "}":
|
|
return formatting.formatOnClosingCurly(position, sourceFile, formatContext);
|
|
case ";":
|
|
return formatting.formatOnSemicolon(position, sourceFile, formatContext);
|
|
case "\n":
|
|
return formatting.formatOnEnter(position, sourceFile, formatContext);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly CodeFixAction[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const span = createTextSpanFromBounds(start, end);
|
|
const formatContext = formatting.getFormatContext(formatOptions, host);
|
|
|
|
return flatMap(deduplicate<number>(errorCodes, equateValues, compareValues), errorCode => {
|
|
cancellationToken.throwIfCancellationRequested();
|
|
return codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences });
|
|
});
|
|
}
|
|
|
|
function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): CombinedCodeActions {
|
|
synchronizeHostData();
|
|
Debug.assert(scope.type === "file");
|
|
const sourceFile = getValidSourceFile(scope.fileName);
|
|
const formatContext = formatting.getFormatContext(formatOptions, host);
|
|
|
|
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences });
|
|
}
|
|
|
|
function organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] {
|
|
synchronizeHostData();
|
|
Debug.assert(args.type === "file");
|
|
const sourceFile = getValidSourceFile(args.fileName);
|
|
const formatContext = formatting.getFormatContext(formatOptions, host);
|
|
|
|
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences, args.skipDestructiveCodeActions);
|
|
}
|
|
|
|
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] {
|
|
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions, host), preferences, sourceMapper);
|
|
}
|
|
|
|
function applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult>;
|
|
function applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
|
|
function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrFormatSettingsOrUndefined?: CodeActionCommand | CodeActionCommand[] | FormatCodeSettings): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]> {
|
|
const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as CodeActionCommand | CodeActionCommand[] : fileName as CodeActionCommand[];
|
|
return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action);
|
|
}
|
|
|
|
function applySingleCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult> {
|
|
const getPath = (path: string): Path => toPath(path, currentDirectory, getCanonicalFileName);
|
|
Debug.assertEqual(action.type, "install package");
|
|
return host.installPackage
|
|
? host.installPackage({ fileName: getPath(action.file), packageName: action.packageName })
|
|
: Promise.reject("Host does not implement `installPackage`");
|
|
}
|
|
|
|
function getDocCommentTemplateAtPosition(fileName: string, position: number, options?: DocCommentTemplateOptions): TextInsertion | undefined {
|
|
return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position, options);
|
|
}
|
|
|
|
function isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
|
|
// '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too
|
|
// expensive to do during typing scenarios
|
|
// i.e. whether we're dealing with:
|
|
// var x = new foo<| ( with class foo<T>{} )
|
|
// or
|
|
// var y = 3 <|
|
|
if (openingBrace === CharacterCodes.lessThan) {
|
|
return false;
|
|
}
|
|
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
// Check if in a context where we don't want to perform any insertion
|
|
if (isInString(sourceFile, position)) {
|
|
return false;
|
|
}
|
|
|
|
if (isInsideJsxElementOrAttribute(sourceFile, position)) {
|
|
return openingBrace === CharacterCodes.openBrace;
|
|
}
|
|
|
|
if (isInTemplateString(sourceFile, position)) {
|
|
return false;
|
|
}
|
|
|
|
switch (openingBrace) {
|
|
case CharacterCodes.singleQuote:
|
|
case CharacterCodes.doubleQuote:
|
|
case CharacterCodes.backtick:
|
|
return !isInComment(sourceFile, position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const token = findPrecedingToken(position, sourceFile);
|
|
if (!token) return undefined;
|
|
const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent
|
|
: isJsxText(token) && isJsxElement(token.parent) ? token.parent : undefined;
|
|
if (element && isUnclosedTag(element)) {
|
|
return { newText: `</${element.openingElement.tagName.getText(sourceFile)}>` };
|
|
}
|
|
const fragment = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningFragment(token.parent) ? token.parent.parent
|
|
: isJsxText(token) && isJsxFragment(token.parent) ? token.parent : undefined;
|
|
if (fragment && isUnclosedFragment(fragment)) {
|
|
return { newText: "</>" };
|
|
}
|
|
}
|
|
|
|
function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) {
|
|
return {
|
|
lineStarts: sourceFile.getLineStarts(),
|
|
firstLine: sourceFile.getLineAndCharacterOfPosition(textRange.pos).line,
|
|
lastLine: sourceFile.getLineAndCharacterOfPosition(textRange.end).line
|
|
};
|
|
}
|
|
|
|
function toggleLineComment(fileName: string, textRange: TextRange, insertComment?: boolean): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const textChanges: TextChange[] = [];
|
|
const { lineStarts, firstLine, lastLine } = getLinesForRange(sourceFile, textRange);
|
|
|
|
let isCommenting = insertComment || false;
|
|
let leftMostPosition = Number.MAX_VALUE;
|
|
const lineTextStarts = new Map<string, number>();
|
|
const firstNonWhitespaceCharacterRegex = new RegExp(/\S/);
|
|
const isJsx = isInsideJsxElement(sourceFile, lineStarts[firstLine]);
|
|
const openComment = isJsx ? "{/*" : "//";
|
|
|
|
// Check each line before any text changes.
|
|
for (let i = firstLine; i <= lastLine; i++) {
|
|
const lineText = sourceFile.text.substring(lineStarts[i], sourceFile.getLineEndOfPosition(lineStarts[i]));
|
|
|
|
// Find the start of text and the left-most character. No-op on empty lines.
|
|
const regExec = firstNonWhitespaceCharacterRegex.exec(lineText);
|
|
if (regExec) {
|
|
leftMostPosition = Math.min(leftMostPosition, regExec.index);
|
|
lineTextStarts.set(i.toString(), regExec.index);
|
|
|
|
if (lineText.substr(regExec.index, openComment.length) !== openComment) {
|
|
isCommenting = insertComment === undefined || insertComment;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push all text changes.
|
|
for (let i = firstLine; i <= lastLine; i++) {
|
|
// If the range is multiline and ends on a beginning of a line, don't comment/uncomment.
|
|
if (firstLine !== lastLine && lineStarts[i] === textRange.end) {
|
|
continue;
|
|
}
|
|
|
|
const lineTextStart = lineTextStarts.get(i.toString());
|
|
|
|
// If the line is not an empty line; otherwise no-op.
|
|
if (lineTextStart !== undefined) {
|
|
if (isJsx) {
|
|
textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { pos: lineStarts[i] + leftMostPosition, end: sourceFile.getLineEndOfPosition(lineStarts[i]) }, isCommenting, isJsx));
|
|
}
|
|
else if (isCommenting) {
|
|
textChanges.push({
|
|
newText: openComment,
|
|
span: {
|
|
length: 0,
|
|
start: lineStarts[i] + leftMostPosition
|
|
}
|
|
});
|
|
}
|
|
else if (sourceFile.text.substr(lineStarts[i] + lineTextStart, openComment.length) === openComment) {
|
|
textChanges.push({
|
|
newText: "",
|
|
span: {
|
|
length: openComment.length,
|
|
start: lineStarts[i] + lineTextStart
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return textChanges;
|
|
}
|
|
|
|
function toggleMultilineComment(fileName: string, textRange: TextRange, insertComment?: boolean, isInsideJsx?: boolean): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const textChanges: TextChange[] = [];
|
|
const { text } = sourceFile;
|
|
|
|
let hasComment = false;
|
|
let isCommenting = insertComment || false;
|
|
const positions = [] as number[] as SortedArray<number>;
|
|
|
|
let { pos } = textRange;
|
|
const isJsx = isInsideJsx !== undefined ? isInsideJsx : isInsideJsxElement(sourceFile, pos);
|
|
|
|
const openMultiline = isJsx ? "{/*" : "/*";
|
|
const closeMultiline = isJsx ? "*/}" : "*/";
|
|
const openMultilineRegex = isJsx ? "\\{\\/\\*" : "\\/\\*";
|
|
const closeMultilineRegex = isJsx ? "\\*\\/\\}" : "\\*\\/";
|
|
|
|
// Get all comment positions
|
|
while (pos <= textRange.end) {
|
|
// Start of comment is considered inside comment.
|
|
const offset = text.substr(pos, openMultiline.length) === openMultiline ? openMultiline.length : 0;
|
|
const commentRange = isInComment(sourceFile, pos + offset);
|
|
|
|
// If position is in a comment add it to the positions array.
|
|
if (commentRange) {
|
|
// Comment range doesn't include the brace character. Increase it to include them.
|
|
if (isJsx) {
|
|
commentRange.pos--;
|
|
commentRange.end++;
|
|
}
|
|
|
|
positions.push(commentRange.pos);
|
|
if (commentRange.kind === SyntaxKind.MultiLineCommentTrivia) {
|
|
positions.push(commentRange.end);
|
|
}
|
|
|
|
hasComment = true;
|
|
pos = commentRange.end + 1;
|
|
}
|
|
else { // If it's not in a comment range, then we need to comment the uncommented portions.
|
|
const newPos = text.substring(pos, textRange.end).search(`(${openMultilineRegex})|(${closeMultilineRegex})`);
|
|
|
|
isCommenting = insertComment !== undefined
|
|
? insertComment
|
|
: isCommenting || !isTextWhiteSpaceLike(text, pos, newPos === -1 ? textRange.end : pos + newPos); // If isCommenting is already true we don't need to check whitespace again.
|
|
pos = newPos === -1 ? textRange.end + 1 : pos + newPos + closeMultiline.length;
|
|
}
|
|
}
|
|
|
|
// If it didn't found a comment and isCommenting is false means is only empty space.
|
|
// We want to insert comment in this scenario.
|
|
if (isCommenting || !hasComment) {
|
|
if (isInComment(sourceFile, textRange.pos)?.kind !== SyntaxKind.SingleLineCommentTrivia) {
|
|
insertSorted(positions, textRange.pos, compareValues);
|
|
}
|
|
insertSorted(positions, textRange.end, compareValues);
|
|
|
|
// Insert open comment if the first position is not a comment already.
|
|
const firstPos = positions[0];
|
|
if (text.substr(firstPos, openMultiline.length) !== openMultiline) {
|
|
textChanges.push({
|
|
newText: openMultiline,
|
|
span: {
|
|
length: 0,
|
|
start: firstPos
|
|
}
|
|
});
|
|
}
|
|
|
|
// Insert open and close comment to all positions between first and last. Exclusive.
|
|
for (let i = 1; i < positions.length - 1; i++) {
|
|
if (text.substr(positions[i] - closeMultiline.length, closeMultiline.length) !== closeMultiline) {
|
|
textChanges.push({
|
|
newText: closeMultiline,
|
|
span: {
|
|
length: 0,
|
|
start: positions[i]
|
|
}
|
|
});
|
|
}
|
|
|
|
if (text.substr(positions[i], openMultiline.length) !== openMultiline) {
|
|
textChanges.push({
|
|
newText: openMultiline,
|
|
span: {
|
|
length: 0,
|
|
start: positions[i]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Insert open comment if the last position is not a comment already.
|
|
if (textChanges.length % 2 !== 0) {
|
|
textChanges.push({
|
|
newText: closeMultiline,
|
|
span: {
|
|
length: 0,
|
|
start: positions[positions.length - 1]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// If is not commenting then remove all comments found.
|
|
for (const pos of positions) {
|
|
const from = pos - closeMultiline.length > 0 ? pos - closeMultiline.length : 0;
|
|
const offset = text.substr(from, closeMultiline.length) === closeMultiline ? closeMultiline.length : 0;
|
|
textChanges.push({
|
|
newText: "",
|
|
span: {
|
|
length: openMultiline.length,
|
|
start: pos - offset
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return textChanges;
|
|
}
|
|
|
|
function commentSelection(fileName: string, textRange: TextRange): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const { firstLine, lastLine } = getLinesForRange(sourceFile, textRange);
|
|
|
|
// If there is a selection that is on the same line, add multiline.
|
|
return firstLine === lastLine && textRange.pos !== textRange.end
|
|
? toggleMultilineComment(fileName, textRange, /*insertComment*/ true)
|
|
: toggleLineComment(fileName, textRange, /*insertComment*/ true);
|
|
}
|
|
|
|
function uncommentSelection(fileName: string, textRange: TextRange): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const textChanges: TextChange[] = [];
|
|
const { pos } = textRange;
|
|
let { end } = textRange;
|
|
|
|
// If cursor is not a selection we need to increase the end position
|
|
// to include the start of the comment.
|
|
if (pos === end) {
|
|
end += isInsideJsxElement(sourceFile, pos) ? 2 : 1;
|
|
}
|
|
|
|
for (let i = pos; i <= end; i++) {
|
|
const commentRange = isInComment(sourceFile, i);
|
|
if (commentRange) {
|
|
switch (commentRange.kind) {
|
|
case SyntaxKind.SingleLineCommentTrivia:
|
|
textChanges.push.apply(textChanges, toggleLineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false));
|
|
break;
|
|
case SyntaxKind.MultiLineCommentTrivia:
|
|
textChanges.push.apply(textChanges, toggleMultilineComment(fileName, { end: commentRange.end, pos: commentRange.pos + 1 }, /*insertComment*/ false));
|
|
}
|
|
|
|
i = commentRange.end + 1;
|
|
}
|
|
}
|
|
|
|
return textChanges;
|
|
}
|
|
|
|
function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean {
|
|
return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) ||
|
|
isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent);
|
|
}
|
|
|
|
function isUnclosedFragment({ closingFragment, parent }: JsxFragment): boolean {
|
|
return !!(closingFragment.flags & NodeFlags.ThisNodeHasError) || (isJsxFragment(parent) && isUnclosedFragment(parent));
|
|
}
|
|
|
|
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const range = formatting.getRangeOfEnclosingComment(sourceFile, position);
|
|
return range && (!onlyMultiLine || range.kind === SyntaxKind.MultiLineCommentTrivia) ? createTextSpanFromRange(range) : undefined;
|
|
}
|
|
|
|
function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
|
|
// Note: while getting todo comments seems like a syntactic operation, we actually
|
|
// treat it as a semantic operation here. This is because we expect our host to call
|
|
// this on every single file. If we treat this syntactically, then that will cause
|
|
// us to populate and throw away the tree in our syntax tree cache for each file. By
|
|
// treating this as a semantic operation, we can access any tree without throwing
|
|
// anything away.
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
|
|
cancellationToken.throwIfCancellationRequested();
|
|
|
|
const fileContents = sourceFile.text;
|
|
const result: TodoComment[] = [];
|
|
|
|
// Exclude node_modules files as we don't want to show the todos of external libraries.
|
|
if (descriptors.length > 0 && !isNodeModulesFile(sourceFile.fileName)) {
|
|
const regExp = getTodoCommentsRegExp();
|
|
|
|
let matchArray: RegExpExecArray | null;
|
|
while (matchArray = regExp.exec(fileContents)) {
|
|
cancellationToken.throwIfCancellationRequested();
|
|
|
|
// If we got a match, here is what the match array will look like. Say the source text is:
|
|
//
|
|
// " // hack 1"
|
|
//
|
|
// The result array with the regexp: will be:
|
|
//
|
|
// ["// hack 1", "// ", "hack 1", undefined, "hack"]
|
|
//
|
|
// Here are the relevant capture groups:
|
|
// 0) The full match for the entire regexp.
|
|
// 1) The preamble to the message portion.
|
|
// 2) The message portion.
|
|
// 3...N) The descriptor that was matched - by index. 'undefined' for each
|
|
// descriptor that didn't match. an actual value if it did match.
|
|
//
|
|
// i.e. 'undefined' in position 3 above means TODO(jason) didn't match.
|
|
// "hack" in position 4 means HACK did match.
|
|
const firstDescriptorCaptureIndex = 3;
|
|
Debug.assert(matchArray.length === descriptors.length + firstDescriptorCaptureIndex);
|
|
|
|
const preamble = matchArray[1];
|
|
const matchPosition = matchArray.index + preamble.length;
|
|
|
|
// OK, we have found a match in the file. This is only an acceptable match if
|
|
// it is contained within a comment.
|
|
if (!isInComment(sourceFile, matchPosition)) {
|
|
continue;
|
|
}
|
|
|
|
let descriptor: TodoCommentDescriptor | undefined;
|
|
for (let i = 0; i < descriptors.length; i++) {
|
|
if (matchArray[i + firstDescriptorCaptureIndex]) {
|
|
descriptor = descriptors[i];
|
|
}
|
|
}
|
|
if (descriptor === undefined) return Debug.fail();
|
|
|
|
// We don't want to match something like 'TODOBY', so we make sure a non
|
|
// letter/digit follows the match.
|
|
if (isLetterOrDigit(fileContents.charCodeAt(matchPosition + descriptor.text.length))) {
|
|
continue;
|
|
}
|
|
|
|
const message = matchArray[2];
|
|
result.push({ descriptor, message, position: matchPosition });
|
|
}
|
|
}
|
|
|
|
return result;
|
|
|
|
function escapeRegExp(str: string): string {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
|
}
|
|
|
|
function getTodoCommentsRegExp(): RegExp {
|
|
// NOTE: `?:` means 'non-capture group'. It allows us to have groups without having to
|
|
// filter them out later in the final result array.
|
|
|
|
// TODO comments can appear in one of the following forms:
|
|
//
|
|
// 1) // TODO or /////////// TODO
|
|
//
|
|
// 2) /* TODO or /********** TODO
|
|
//
|
|
// 3) /*
|
|
// * TODO
|
|
// */
|
|
//
|
|
// The following three regexps are used to match the start of the text up to the TODO
|
|
// comment portion.
|
|
const singleLineCommentStart = /(?:\/\/+\s*)/.source;
|
|
const multiLineCommentStart = /(?:\/\*+\s*)/.source;
|
|
const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
|
|
|
|
// Match any of the above three TODO comment start regexps.
|
|
// Note that the outermost group *is* a capture group. We want to capture the preamble
|
|
// so that we can determine the starting position of the TODO comment match.
|
|
const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
|
|
|
|
// Takes the descriptors and forms a regexp that matches them as if they were literals.
|
|
// For example, if the descriptors are "TODO(jason)" and "HACK", then this will be:
|
|
//
|
|
// (?:(TODO\(jason\))|(HACK))
|
|
//
|
|
// Note that the outermost group is *not* a capture group, but the innermost groups
|
|
// *are* capture groups. By capturing the inner literals we can determine after
|
|
// matching which descriptor we are dealing with.
|
|
const literals = "(?:" + map(descriptors, d => "(" + escapeRegExp(d.text) + ")").join("|") + ")";
|
|
|
|
// After matching a descriptor literal, the following regexp matches the rest of the
|
|
// text up to the end of the line (or */).
|
|
const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
|
|
const messageRemainder = /(?:.*?)/.source;
|
|
|
|
// This is the portion of the match we'll return as part of the TODO comment result. We
|
|
// match the literal portion up to the end of the line or end of comment.
|
|
const messagePortion = "(" + literals + messageRemainder + ")";
|
|
const regExpString = preamble + messagePortion + endOfLineOrEndOfComment;
|
|
|
|
// The final regexp will look like this:
|
|
// /((?:\/\/+\s*)|(?:\/\*+\s*)|(?:^(?:\s|\*)*))((?:(TODO\(jason\))|(HACK))(?:.*?))(?:$|\*\/)/gim
|
|
|
|
// The flags of the regexp are important here.
|
|
// 'g' is so that we are doing a global search and can find matches several times
|
|
// in the input.
|
|
//
|
|
// 'i' is for case insensitivity (We do this to match C# TODO comment code).
|
|
//
|
|
// 'm' is so we can find matches in a multi-line input.
|
|
return new RegExp(regExpString, "gim");
|
|
}
|
|
|
|
function isLetterOrDigit(char: number): boolean {
|
|
return (char >= CharacterCodes.a && char <= CharacterCodes.z) ||
|
|
(char >= CharacterCodes.A && char <= CharacterCodes.Z) ||
|
|
(char >= CharacterCodes._0 && char <= CharacterCodes._9);
|
|
}
|
|
|
|
function isNodeModulesFile(path: string): boolean {
|
|
return stringContains(path, "/node_modules/");
|
|
}
|
|
}
|
|
|
|
function getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo {
|
|
synchronizeHostData();
|
|
return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options);
|
|
}
|
|
|
|
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings, triggerReason?: RefactorTriggerReason, kind?: string): RefactorContext {
|
|
const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end];
|
|
return {
|
|
file,
|
|
startPosition,
|
|
endPosition,
|
|
program: getProgram()!,
|
|
host,
|
|
formatContext: formatting.getFormatContext(formatOptions!, host), // TODO: GH#18217
|
|
cancellationToken,
|
|
preferences,
|
|
triggerReason,
|
|
kind
|
|
};
|
|
}
|
|
|
|
function getInlayHintsContext(file: SourceFile, span: TextSpan, preferences: UserPreferences): InlayHintsContext {
|
|
return {
|
|
file,
|
|
program: getProgram()!,
|
|
host,
|
|
span,
|
|
preferences,
|
|
cancellationToken,
|
|
};
|
|
}
|
|
|
|
function getSmartSelectionRange(fileName: string, position: number): SelectionRange {
|
|
return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName));
|
|
}
|
|
|
|
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): ApplicableRefactorInfo[] {
|
|
synchronizeHostData();
|
|
const file = getValidSourceFile(fileName);
|
|
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind));
|
|
}
|
|
|
|
function getEditsForRefactor(
|
|
fileName: string,
|
|
formatOptions: FormatCodeSettings,
|
|
positionOrRange: number | TextRange,
|
|
refactorName: string,
|
|
actionName: string,
|
|
preferences: UserPreferences = emptyOptions,
|
|
): RefactorEditInfo | undefined {
|
|
synchronizeHostData();
|
|
const file = getValidSourceFile(fileName);
|
|
return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName);
|
|
}
|
|
|
|
function toLineColumnOffset(fileName: string, position: number): LineAndCharacter {
|
|
// Go to Definition supports returning a zero-length span at position 0 for
|
|
// non-existent files. We need to special-case the conversion of position 0
|
|
// to avoid a crash trying to get the text for that file, since this function
|
|
// otherwise assumes that 'fileName' is the name of a file that exists.
|
|
if (position === 0) {
|
|
return { line: 0, character: 0 };
|
|
}
|
|
return sourceMapper.toLineColumnOffset(fileName, position);
|
|
}
|
|
|
|
function prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined {
|
|
synchronizeHostData();
|
|
const declarations = CallHierarchy.resolveCallHierarchyDeclaration(program, getTouchingPropertyName(getValidSourceFile(fileName), position));
|
|
return declarations && mapOneOrMany(declarations, declaration => CallHierarchy.createCallHierarchyItem(program, declaration));
|
|
}
|
|
|
|
function provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)));
|
|
return declaration ? CallHierarchy.getIncomingCalls(program, declaration, cancellationToken) : [];
|
|
}
|
|
|
|
function provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)));
|
|
return declaration ? CallHierarchy.getOutgoingCalls(program, declaration) : [];
|
|
}
|
|
|
|
function provideInlayHints(fileName: string, span: TextSpan, preferences: InlayHintsOptions = emptyOptions): InlayHint[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
return InlayHints.provideInlayHints(getInlayHintsContext(sourceFile, span, preferences));
|
|
}
|
|
|
|
const ls: LanguageService = {
|
|
dispose,
|
|
cleanupSemanticCache,
|
|
getSyntacticDiagnostics,
|
|
getSemanticDiagnostics,
|
|
getSuggestionDiagnostics,
|
|
getCompilerOptionsDiagnostics,
|
|
getSyntacticClassifications,
|
|
getSemanticClassifications,
|
|
getEncodedSyntacticClassifications,
|
|
getEncodedSemanticClassifications,
|
|
getCompletionsAtPosition,
|
|
getCompletionEntryDetails,
|
|
getCompletionEntrySymbol,
|
|
getSignatureHelpItems,
|
|
getQuickInfoAtPosition,
|
|
getDefinitionAtPosition,
|
|
getDefinitionAndBoundSpan,
|
|
getImplementationAtPosition,
|
|
getTypeDefinitionAtPosition,
|
|
getReferencesAtPosition,
|
|
findReferences,
|
|
getFileReferences,
|
|
getOccurrencesAtPosition,
|
|
getDocumentHighlights,
|
|
getNameOrDottedNameSpan,
|
|
getBreakpointStatementAtPosition,
|
|
getNavigateToItems,
|
|
getRenameInfo,
|
|
getSmartSelectionRange,
|
|
findRenameLocations,
|
|
getNavigationBarItems,
|
|
getNavigationTree,
|
|
getOutliningSpans,
|
|
getTodoComments,
|
|
getBraceMatchingAtPosition,
|
|
getIndentationAtPosition,
|
|
getFormattingEditsForRange,
|
|
getFormattingEditsForDocument,
|
|
getFormattingEditsAfterKeystroke,
|
|
getDocCommentTemplateAtPosition,
|
|
isValidBraceCompletionAtPosition,
|
|
getJsxClosingTagAtPosition,
|
|
getSpanOfEnclosingComment,
|
|
getCodeFixesAtPosition,
|
|
getCombinedCodeFix,
|
|
applyCodeActionCommand,
|
|
organizeImports,
|
|
getEditsForFileRename,
|
|
getEmitOutput,
|
|
getNonBoundSourceFile,
|
|
getProgram,
|
|
getAutoImportProvider,
|
|
getApplicableRefactors,
|
|
getEditsForRefactor,
|
|
toLineColumnOffset,
|
|
getSourceMapper: () => sourceMapper,
|
|
clearSourceMapperCache: () => sourceMapper.clearCache(),
|
|
prepareCallHierarchy,
|
|
provideCallHierarchyIncomingCalls,
|
|
provideCallHierarchyOutgoingCalls,
|
|
toggleLineComment,
|
|
toggleMultilineComment,
|
|
commentSelection,
|
|
uncommentSelection,
|
|
provideInlayHints,
|
|
};
|
|
|
|
switch (languageServiceMode) {
|
|
case LanguageServiceMode.Semantic:
|
|
break;
|
|
case LanguageServiceMode.PartialSemantic:
|
|
invalidOperationsInPartialSemanticMode.forEach(key =>
|
|
ls[key] = () => {
|
|
throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.PartialSemantic`);
|
|
}
|
|
);
|
|
break;
|
|
case LanguageServiceMode.Syntactic:
|
|
invalidOperationsInSyntacticMode.forEach(key =>
|
|
ls[key] = () => {
|
|
throw new Error(`LanguageService Operation: ${key} not allowed in LanguageServiceMode.Syntactic`);
|
|
}
|
|
);
|
|
break;
|
|
default:
|
|
Debug.assertNever(languageServiceMode);
|
|
}
|
|
return ls;
|
|
}
|
|
|
|
/* @internal */
|
|
/** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */
|
|
export function getNameTable(sourceFile: SourceFile): UnderscoreEscapedMap<number> {
|
|
if (!sourceFile.nameTable) {
|
|
initializeNameTable(sourceFile);
|
|
}
|
|
|
|
return sourceFile.nameTable!; // TODO: GH#18217
|
|
}
|
|
|
|
function initializeNameTable(sourceFile: SourceFile): void {
|
|
const nameTable = sourceFile.nameTable = new Map();
|
|
sourceFile.forEachChild(function walk(node) {
|
|
if (isIdentifier(node) && !isTagName(node) && node.escapedText || isStringOrNumericLiteralLike(node) && literalIsName(node)) {
|
|
const text = getEscapedTextOfIdentifierOrLiteral(node);
|
|
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
|
|
}
|
|
else if (isPrivateIdentifier(node)) {
|
|
const text = node.escapedText;
|
|
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
|
|
}
|
|
|
|
forEachChild(node, walk);
|
|
if (hasJSDocNodes(node)) {
|
|
for (const jsDoc of node.jsDoc!) {
|
|
forEachChild(jsDoc, walk);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* We want to store any numbers/strings if they were a name that could be
|
|
* related to a declaration. So, if we have 'import x = require("something")'
|
|
* then we want 'something' to be in the name table. Similarly, if we have
|
|
* "a['propname']" then we want to store "propname" in the name table.
|
|
*/
|
|
function literalIsName(node: StringLiteralLike | NumericLiteral): boolean {
|
|
return isDeclarationName(node) ||
|
|
node.parent.kind === SyntaxKind.ExternalModuleReference ||
|
|
isArgumentOfElementAccessExpression(node) ||
|
|
isLiteralComputedPropertyDeclarationName(node);
|
|
}
|
|
|
|
/**
|
|
* Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 }
|
|
*/
|
|
/* @internal */
|
|
export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElementWithName | undefined {
|
|
const element = getContainingObjectLiteralElementWorker(node);
|
|
return element && (isObjectLiteralExpression(element.parent) || isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined;
|
|
}
|
|
function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined {
|
|
switch (node.kind) {
|
|
case SyntaxKind.StringLiteral:
|
|
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
case SyntaxKind.NumericLiteral:
|
|
if (node.parent.kind === SyntaxKind.ComputedPropertyName) {
|
|
return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined;
|
|
}
|
|
// falls through
|
|
|
|
case SyntaxKind.Identifier:
|
|
return isObjectLiteralElement(node.parent) &&
|
|
(node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) &&
|
|
node.parent.name === node ? node.parent : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/* @internal */
|
|
export type ObjectLiteralElementWithName = ObjectLiteralElement & { name: PropertyName; parent: ObjectLiteralExpression | JsxAttributes };
|
|
|
|
function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined {
|
|
const object = getContainingObjectLiteralElement(node);
|
|
if (object) {
|
|
const contextualType = checker.getContextualType(object.parent);
|
|
const properties = contextualType && getPropertySymbolsFromContextualType(object, checker, contextualType, /*unionSymbolOk*/ false);
|
|
if (properties && properties.length === 1) {
|
|
return first(properties);
|
|
}
|
|
}
|
|
return checker.getSymbolAtLocation(node);
|
|
}
|
|
|
|
/** Gets all symbols for one property. Does not get symbols for every property. */
|
|
/* @internal */
|
|
export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker, contextualType: Type, unionSymbolOk: boolean): readonly Symbol[] {
|
|
const name = getNameFromPropertyName(node.name);
|
|
if (!name) return emptyArray;
|
|
if (!contextualType.isUnion()) {
|
|
const symbol = contextualType.getProperty(name);
|
|
return symbol ? [symbol] : emptyArray;
|
|
}
|
|
|
|
const discriminatedPropertySymbols = mapDefined(contextualType.types, t => (isObjectLiteralExpression(node.parent)|| isJsxAttributes(node.parent)) && checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent) ? undefined : t.getProperty(name));
|
|
if (unionSymbolOk && (discriminatedPropertySymbols.length === 0 || discriminatedPropertySymbols.length === contextualType.types.length)) {
|
|
const symbol = contextualType.getProperty(name);
|
|
if (symbol) return [symbol];
|
|
}
|
|
if (discriminatedPropertySymbols.length === 0) {
|
|
// Bad discriminant -- do again without discriminating
|
|
return mapDefined(contextualType.types, t => t.getProperty(name));
|
|
}
|
|
return discriminatedPropertySymbols;
|
|
}
|
|
|
|
function isArgumentOfElementAccessExpression(node: Node) {
|
|
return node &&
|
|
node.parent &&
|
|
node.parent.kind === SyntaxKind.ElementAccessExpression &&
|
|
(node.parent as ElementAccessExpression).argumentExpression === node;
|
|
}
|
|
|
|
/// getDefaultLibraryFilePath
|
|
declare const __dirname: string;
|
|
|
|
/**
|
|
* Get the path of the default library files (lib.d.ts) as distributed with the typescript
|
|
* node package.
|
|
* The functionality is not supported if the ts module is consumed outside of a node module.
|
|
*/
|
|
export function getDefaultLibFilePath(options: CompilerOptions): string {
|
|
// Check __dirname is defined and that we are on a node.js system.
|
|
if (typeof __dirname !== "undefined") {
|
|
return __dirname + directorySeparator + getDefaultLibFileName(options);
|
|
}
|
|
|
|
throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. ");
|
|
}
|
|
|
|
setObjectAllocator(getServicesObjectAllocator());
|
|
}
|