Skip to content

Commit 961805c

Browse files
committed
Merge pull request microsoft#4006 from Microsoft/JSDocCommentScaffolding
Js doc comment templates
2 parents 74dc5a2 + b3f0a2a commit 961805c

22 files changed

+366
-45
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ namespace ts {
7474
// If the current node is a container that also container that also contains locals. Examples:
7575
//
7676
// Functions, Methods, Modules, Source-files.
77-
IsContainerWithLocals = IsContainer | HasLocals
77+
IsContainerWithLocals = IsContainer | HasLocals
7878
}
7979

8080
export function bindSourceFile(file: SourceFile) {
@@ -1062,4 +1062,4 @@ namespace ts {
10621062
: declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes);
10631063
}
10641064
}
1065-
}
1065+
}

src/compiler/core.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ namespace ts {
193193
return array[array.length - 1];
194194
}
195195

196+
/**
197+
* Performs a binary search, finding the index at which 'value' occurs in 'array'.
198+
* If no such index is found, returns the 2's-complement of first index at which
199+
* number[index] exceeds number.
200+
* @param array A sorted array whose first element must be no larger than number
201+
* @param number The value to be searched for in the array.
202+
*/
196203
export function binarySearch(array: number[], value: number): number {
197204
let low = 0;
198205
let high = array.length - 1;

src/compiler/scanner.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,21 @@ namespace ts {
319319
}
320320

321321
/* @internal */
322+
/**
323+
* We assume the first line starts at position 0 and 'position' is non-negative.
324+
*/
322325
export function computeLineAndCharacterOfPosition(lineStarts: number[], position: number) {
323326
let lineNumber = binarySearch(lineStarts, position);
324327
if (lineNumber < 0) {
325328
// If the actual position was not found,
326-
// the binary search returns the negative value of the next line start
329+
// the binary search returns the 2's-complement of the next line start
327330
// e.g. if the line starts at [5, 10, 23, 80] and the position requested was 20
328-
// then the search will return -2
331+
// then the search will return -2.
332+
//
333+
// We want the index of the previous line start, so we subtract 1.
334+
// Review 2's-complement if this is confusing.
329335
lineNumber = ~lineNumber - 1;
336+
Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file");
330337
}
331338
return {
332339
line: lineNumber,
@@ -552,13 +559,17 @@ namespace ts {
552559
return pos;
553560
}
554561

555-
// Extract comments from the given source text starting at the given position. If trailing is
556-
// false, whitespace is skipped until the first line break and comments between that location
557-
// and the next token are returned.If trailing is true, comments occurring between the given
558-
// position and the next line break are returned.The return value is an array containing a
559-
// TextRange for each comment. Single-line comment ranges include the beginning '//' characters
560-
// but not the ending line break. Multi - line comment ranges include the beginning '/* and
561-
// ending '*/' characters.The return value is undefined if no comments were found.
562+
/**
563+
* Extract comments from text prefixing the token closest following `pos`.
564+
* The return value is an array containing a TextRange for each comment.
565+
* Single-line comment ranges include the beginning '//' characters but not the ending line break.
566+
* Multi - line comment ranges include the beginning '/* and ending '<asterisk>/' characters.
567+
* The return value is undefined if no comments were found.
568+
* @param trailing
569+
* If false, whitespace is skipped until the first line break and comments between that location
570+
* and the next token are returned.
571+
* If true, comments occurring between the given position and the next line break are returned.
572+
*/
562573
function getCommentRanges(text: string, pos: number, trailing: boolean): CommentRange[] {
563574
let result: CommentRange[];
564575
let collecting = trailing || pos === 0;

src/compiler/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -586,9 +586,9 @@ namespace ts {
586586
* Several node kinds share function-like features such as a signature,
587587
* a name, and a body. These nodes should extend FunctionLikeDeclaration.
588588
* Examples:
589-
* FunctionDeclaration
590-
* MethodDeclaration
591-
* AccessorDeclaration
589+
* - FunctionDeclaration
590+
* - MethodDeclaration
591+
* - AccessorDeclaration
592592
*/
593593
export interface FunctionLikeDeclaration extends SignatureDeclaration {
594594
_functionLikeDeclarationBrand: any;

src/harness/fourslash.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1941,6 +1941,32 @@ module FourSlash {
19411941
}
19421942
}
19431943

1944+
public verifyDocCommentTemplate(expected?: ts.TextInsertion) {
1945+
const name = "verifyDocCommentTemplate";
1946+
let actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1947+
1948+
if (expected === undefined) {
1949+
if (actual) {
1950+
this.raiseError(name + ' failed - expected no template but got {newText: \"' + actual.newText + '\" caretOffset: ' + actual.caretOffset + '}');
1951+
}
1952+
1953+
return;
1954+
}
1955+
else {
1956+
if (actual === undefined) {
1957+
this.raiseError(name + ' failed - expected the template {newText: \"' + actual.newText + '\" caretOffset: ' + actual.caretOffset + '} but got nothing instead');
1958+
}
1959+
1960+
if (actual.newText !== expected.newText) {
1961+
this.raiseError(name + ' failed - expected insertion:\n' + expected.newText + '\nactual insertion:\n' + actual.newText);
1962+
}
1963+
1964+
if (actual.caretOffset !== expected.caretOffset) {
1965+
this.raiseError(name + ' failed - expected caretOffset: ' + expected.caretOffset + ',\nactual caretOffset:' + actual.caretOffset);
1966+
}
1967+
}
1968+
}
1969+
19441970
public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
19451971
this.taoInvalidReason = "verifyMatchingBracePosition NYI";
19461972

@@ -2808,4 +2834,4 @@ module FourSlash {
28082834
fileName: fileName
28092835
};
28102836
}
2811-
}
2837+
}

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,9 @@ module Harness.LanguageService {
381381
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions): ts.TextChange[] {
382382
return unwrapJSONCallResult(this.shim.getFormattingEditsAfterKeystroke(fileName, position, key, JSON.stringify(options)));
383383
}
384+
getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion {
385+
return unwrapJSONCallResult(this.shim.getDocCommentTemplateAtPosition(fileName, position));
386+
}
384387
getEmitOutput(fileName: string): ts.EmitOutput {
385388
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
386389
}

src/harness/typeWriter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ class TypeWriterWalker {
7171
symbol: symbolString
7272
});
7373
}
74-
}
74+
}

src/server/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,10 @@ namespace ts.server {
563563
getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
564564
throw new Error("Not Implemented Yet.");
565565
}
566+
567+
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion {
568+
throw new Error("Not Implemented Yet.");
569+
}
566570

567571
getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
568572
var lineOffset = this.positionToOneBasedLineOffset(fileName, position);

src/services/services.ts

Lines changed: 89 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,8 @@ namespace ts {
10401040
getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[];
10411041
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[];
10421042

1043+
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion;
1044+
10431045
getEmitOutput(fileName: string): EmitOutput;
10441046

10451047
getProgram(): Program;
@@ -1086,6 +1088,12 @@ namespace ts {
10861088
newText: string;
10871089
}
10881090

1091+
export interface TextInsertion {
1092+
newText: string;
1093+
/** The position in newText the caret should point to after the insertion. */
1094+
caretOffset: number;
1095+
}
1096+
10891097
export interface RenameLocation {
10901098
textSpan: TextSpan;
10911099
fileName: string;
@@ -5486,7 +5494,7 @@ namespace ts {
54865494
symbolToIndex: number[]): void {
54875495

54885496
let sourceFile = container.getSourceFile();
5489-
let tripleSlashDirectivePrefixRegex = /^\/\/\/\s*</
5497+
let tripleSlashDirectivePrefixRegex = /^\/\/\/\s*</;
54905498

54915499
let possiblePositions = getPossibleSymbolReferencePositions(sourceFile, searchText, container.getStart(), container.getEnd());
54925500

@@ -5502,8 +5510,8 @@ namespace ts {
55025510
// This wasn't the start of a token. Check to see if it might be a
55035511
// match in a comment or string if that's what the caller is asking
55045512
// for.
5505-
if ((findInStrings && isInString(position)) ||
5506-
(findInComments && isInComment(position))) {
5513+
if ((findInStrings && isInString(sourceFile, position)) ||
5514+
(findInComments && isInNonReferenceComment(sourceFile, position))) {
55075515

55085516
// In the case where we're looking inside comments/strings, we don't have
55095517
// an actual definition. So just use 'undefined' here. Features like
@@ -5567,30 +5575,13 @@ namespace ts {
55675575
return result[index];
55685576
}
55695577

5570-
function isInString(position: number) {
5571-
let token = getTokenAtPosition(sourceFile, position);
5572-
return token && token.kind === SyntaxKind.StringLiteral && position > token.getStart();
5573-
}
5578+
function isInNonReferenceComment(sourceFile: SourceFile, position: number): boolean {
5579+
return isInCommentHelper(sourceFile, position, isNonReferenceComment);
55745580

5575-
function isInComment(position: number) {
5576-
let token = getTokenAtPosition(sourceFile, position);
5577-
if (token && position < token.getStart()) {
5578-
// First, we have to see if this position actually landed in a comment.
5579-
let commentRanges = getLeadingCommentRanges(sourceFile.text, token.pos);
5580-
5581-
// Then we want to make sure that it wasn't in a "///<" directive comment
5582-
// We don't want to unintentionally update a file name.
5583-
return forEach(commentRanges, c => {
5584-
if (c.pos < position && position < c.end) {
5585-
let commentText = sourceFile.text.substring(c.pos, c.end);
5586-
if (!tripleSlashDirectivePrefixRegex.test(commentText)) {
5587-
return true;
5588-
}
5589-
}
5590-
});
5581+
function isNonReferenceComment(c: CommentRange): boolean {
5582+
let commentText = sourceFile.text.substring(c.pos, c.end);
5583+
return !tripleSlashDirectivePrefixRegex.test(commentText);
55915584
}
5592-
5593-
return false;
55945585
}
55955586
}
55965587

@@ -6817,6 +6808,78 @@ namespace ts {
68176808
return [];
68186809
}
68196810

6811+
/**
6812+
* Checks if position points to a valid position to add JSDoc comments, and if so,
6813+
* returns the appropriate template. Otherwise returns an empty string.
6814+
* Valid positions are
6815+
* - outside of comments, statements, and expressions, and
6816+
* - preceding a function declaration.
6817+
*
6818+
* Hosts should ideally check that:
6819+
* - The line is all whitespace up to 'position' before performing the insertion.
6820+
* - If the keystroke sequence "/\*\*" induced the call, we also check that the next
6821+
* non-whitespace character is '*', which (approximately) indicates whether we added
6822+
* the second '*' to complete an existing (JSDoc) comment.
6823+
* @param fileName The file in which to perform the check.
6824+
* @param position The (character-indexed) position in the file where the check should
6825+
* be performed.
6826+
*/
6827+
function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion {
6828+
let start = new Date().getTime();
6829+
let sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
6830+
6831+
// Check if in a context where we don't want to perform any insertion
6832+
if (isInString(sourceFile, position) || isInComment(sourceFile, position) || hasDocComment(sourceFile, position)) {
6833+
return undefined;
6834+
}
6835+
6836+
let tokenAtPos = getTokenAtPosition(sourceFile, position);
6837+
let tokenStart = tokenAtPos.getStart()
6838+
if (!tokenAtPos || tokenStart < position) {
6839+
return undefined;
6840+
}
6841+
6842+
// TODO: add support for:
6843+
// - methods
6844+
// - constructors
6845+
// - class decls
6846+
let containingFunction = <FunctionDeclaration>getAncestor(tokenAtPos, SyntaxKind.FunctionDeclaration);
6847+
6848+
if (!containingFunction || containingFunction.getStart() < position) {
6849+
return undefined;
6850+
}
6851+
6852+
let parameters = containingFunction.parameters;
6853+
let posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position);
6854+
let lineStart = sourceFile.getLineStarts()[posLineAndChar.line];
6855+
6856+
let indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character);
6857+
6858+
// TODO: call a helper method instead once PR #4133 gets merged in.
6859+
const newLine = host.getNewLine ? host.getNewLine() : "\r\n";
6860+
6861+
let docParams = parameters.reduce((prev, cur, index) =>
6862+
prev +
6863+
indentationStr + " * @param " + (cur.name.kind === SyntaxKind.Identifier ? (<Identifier>cur.name).text : "param" + index) + newLine, "");
6864+
6865+
// A doc comment consists of the following
6866+
// * The opening comment line
6867+
// * the first line (without a param) for the object's untagged info (this is also where the caret ends up)
6868+
// * the '@param'-tagged lines
6869+
// * TODO: other tags.
6870+
// * the closing comment line
6871+
// * if the caret was directly in front of the object, then we add an extra line and indentation.
6872+
const preamble = "/**" + newLine +
6873+
indentationStr + " * ";
6874+
let result =
6875+
preamble + newLine +
6876+
docParams +
6877+
indentationStr + " */" +
6878+
(tokenStart === position ? newLine + indentationStr : "");
6879+
6880+
return { newText: result, caretOffset: preamble.length };
6881+
}
6882+
68206883
function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] {
68216884
// Note: while getting todo comments seems like a syntactic operation, we actually
68226885
// treat it as a semantic operation here. This is because we expect our host to call
@@ -7058,6 +7121,7 @@ namespace ts {
70587121
getFormattingEditsForRange,
70597122
getFormattingEditsForDocument,
70607123
getFormattingEditsAfterKeystroke,
7124+
getDocCommentTemplateAtPosition,
70617125
getEmitOutput,
70627126
getSourceFile,
70637127
getProgram

src/services/shims.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ namespace ts {
205205
getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string;
206206
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string;
207207

208+
/**
209+
* Returns JSON-encoded value of the type TextInsertion.
210+
*/
211+
getDocCommentTemplateAtPosition(fileName: string, position: number): string;
212+
208213
getEmitOutput(fileName: string): string;
209214
}
210215

@@ -811,6 +816,13 @@ namespace ts {
811816
});
812817
}
813818

819+
public getDocCommentTemplateAtPosition(fileName: string, position: number): string {
820+
return this.forwardJSONCall(
821+
"getDocCommentTemplateAtPosition('" + fileName + "', " + position + ")",
822+
() => this.languageService.getDocCommentTemplateAtPosition(fileName, position)
823+
);
824+
}
825+
814826
/// NAVIGATE TO
815827

816828
/** Return a list of symbols that are interesting to navigate to */

0 commit comments

Comments
 (0)