Skip to content

Commit

Permalink
jsdoc import type completions
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Jan 30, 2024
1 parent df23ce3 commit 0b0029d
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 16 deletions.
1 change: 1 addition & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47625,6 +47625,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (
(isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) ||
((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent as ImportDeclaration).moduleSpecifier === node) ||
(isInJSFile(node) && isJSDocImportTypeTag(node.parent) && node.parent.moduleSpecifier === node) ||
((isInJSFile(node) && isRequireCall(node.parent, /*requireStringLiteralLikeArgument*/ false)) || isImportCall(node.parent)) ||
(isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)
) {
Expand Down
39 changes: 23 additions & 16 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3172,6 +3172,7 @@ function getCompletionData(
log("getCompletionData: Is inside comment: " + (timestamp() - start));

let insideJsDocTagTypeExpression = false;
let insideJsDocImportTypeTag = false;
let isInSnippetScope = false;
if (insideComment) {
if (hasDocComment(sourceFile, position)) {
Expand Down Expand Up @@ -3212,25 +3213,30 @@ function getCompletionData(
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
return { kind: CompletionDataKind.JsDocTagName };
}
const typeExpression = tryGetTypeExpressionFromTag(tag);
if (typeExpression) {
currentToken = getTokenAtPosition(sourceFile, position);
if (
!currentToken ||
(!isDeclarationName(currentToken) &&
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
(currentToken.parent as JSDocPropertyTag).name !== currentToken))
) {
// Use as type location if inside tag's type expression
insideJsDocTagTypeExpression = isCurrentlyEditingNode(typeExpression);
}
if (isJSDocImportTypeTag(tag)) {
insideJsDocImportTypeTag = true;
}
if (!insideJsDocTagTypeExpression && isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
return { kind: CompletionDataKind.JsDocParameterName, tag };
else {
const typeExpression = tryGetTypeExpressionFromTag(tag);
if (typeExpression) {
currentToken = getTokenAtPosition(sourceFile, position);
if (
!currentToken ||
(!isDeclarationName(currentToken) &&
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
(currentToken.parent as JSDocPropertyTag).name !== currentToken))
) {
// Use as type location if inside tag's type expression
insideJsDocTagTypeExpression = isCurrentlyEditingNode(typeExpression);
}
}
if (!insideJsDocTagTypeExpression && isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
return { kind: CompletionDataKind.JsDocParameterName, tag };
}
}
}

if (!insideJsDocTagTypeExpression) {
if (!insideJsDocTagTypeExpression && !insideJsDocImportTypeTag) {
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
// comment or the plain text part of a jsDoc comment, so no completion should be available
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
Expand All @@ -3241,7 +3247,7 @@ function getCompletionData(
start = timestamp();
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
const isJsOnlyLocation = !insideJsDocTagTypeExpression && isSourceFileJS(sourceFile);
const isJsOnlyLocation = !insideJsDocTagTypeExpression && !insideJsDocImportTypeTag && isSourceFileJS(sourceFile);
const tokens = getRelevantTokens(position, sourceFile);
const previousToken = tokens.previousToken!;
let contextToken = tokens.contextToken!;
Expand Down Expand Up @@ -3922,6 +3928,7 @@ function getCompletionData(

function isTypeOnlyCompletion(): boolean {
return insideJsDocTagTypeExpression
|| insideJsDocImportTypeTag
|| !!importStatementCompletion && isTypeOnlyImportOrExportDeclaration(location.parent)
|| !isContextTokenValueLocation(contextToken) &&
(isPossiblyTypeArgumentPosition(contextToken, sourceFile, typeChecker)
Expand Down
1 change: 1 addition & 0 deletions src/services/stringCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
case SyntaxKind.ImportDeclaration:
case SyntaxKind.ExportDeclaration:
case SyntaxKind.ExternalModuleReference:
case SyntaxKind.JSDocImportTypeTag:
// Get all known external module names or complete a path to a module
// i.e. import * as ns from "/*completion position*/";
// var y = import("/*completion position*/");
Expand Down
48 changes: 48 additions & 0 deletions tests/baselines/reference/jsdocImportTypeTagCompletion2.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// === Completions ===
=== /b.js ===
// /**
// * @importType { } from "./a"
// ^
// | ----------------------------------------------------------------------
// | interface A
// | ----------------------------------------------------------------------
// */

[
{
"marker": {
"fileName": "/b.js",
"position": 21,
"name": ""
},
"item": {
"flags": 0,
"isGlobalCompletion": false,
"isMemberCompletion": true,
"isNewIdentifierLocation": false,
"entries": [
{
"name": "A",
"kind": "interface",
"kindModifiers": "export",
"sortText": "11",
"displayParts": [
{
"text": "interface",
"kind": "keyword"
},
{
"text": " ",
"kind": "space"
},
{
"text": "A",
"kind": "interfaceName"
}
],
"documentation": []
}
]
}
}
]
51 changes: 51 additions & 0 deletions tests/baselines/reference/jsdocImportTypeTagCompletion3.baseline
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// === Completions ===
=== /tests/cases/fourslash/./c.js ===
// /**
// * @importType * as types from "./"
// ^
// | ----------------------------------------------------------------------
// | a
// | b
// | ----------------------------------------------------------------------
// */

[
{
"marker": {
"fileName": "/tests/cases/fourslash/./c.js",
"position": 38,
"name": ""
},
"item": {
"isGlobalCompletion": false,
"isMemberCompletion": false,
"isNewIdentifierLocation": true,
"entries": [
{
"name": "a",
"kind": "script",
"kindModifiers": ".ts",
"sortText": "11",
"displayParts": [
{
"text": "a",
"kind": "text"
}
]
},
{
"name": "b",
"kind": "script",
"kindModifiers": ".ts",
"sortText": "11",
"displayParts": [
{
"text": "b",
"kind": "text"
}
]
}
]
}
}
]
14 changes: 14 additions & 0 deletions tests/cases/fourslash/jsdocImportTypeTagCompletion2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
///<reference path="fourslash.ts" />

// @allowJS: true
// @checkJs: true

// @filename: /a.ts
////export interface A {}

// @filename: /b.js
/////**
//// * @importType { /**/ } from "./a"
//// */

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

// @allowJS: true
// @checkJs: true
// @module: esnext

// @filename: ./a.ts
////export interface A {}

// @filename: ./b.ts
////export interface B {}

// @filename: ./c.js
/////**
//// * @importType * as types from ".//**/"
//// */

verify.baselineCompletions();

0 comments on commit 0b0029d

Please sign in to comment.