alm
Version:
The best IDE for TypeScript
173 lines (148 loc) • 6.18 kB
text/typescript
import * as path from "path";
import {Project} from "../core/project";
import * as utils from "../../../../common/utils";
import * as fsu from "../../../utils/fsu";
import * as types from "../../../../common/types";
import {Types} from "../../../../socket/socketContract";
import fuzzaldrin = require('fuzzaldrin');
/** From https://github.com/Microsoft/TypeScript/pull/2173/files */
function getExternalModuleNames(program: ts.Program): string[] {
var entries: string[] = [];
program.getSourceFiles().forEach(sourceFile => {
// Look for ambient external module declarations
ts.forEachChild(sourceFile, child => {
if (child.kind === ts.SyntaxKind.ModuleDeclaration && (<ts.ModuleDeclaration>child).name.kind === ts.SyntaxKind.StringLiteral) {
let name = (<ts.ModuleDeclaration>child).name.text;
if (name.endsWith('/index')) {
name = utils.getDirectory(name);
}
entries.push(name);
}
});
});
return entries;
}
/** This is great for auto import */
export interface GetPathCompletions {
prefix: string;
project: Project;
filePath: string;
}
/** This is great for autocomplete */
export interface GetPathCompletionsForAutocomplete extends GetPathCompletions {
position: number;
}
function isStringLiteralInES6ImportDeclaration(node: ts.Node) {
if (node.kind !== ts.SyntaxKind.StringLiteral) return false;
while (node.parent.kind !== ts.SyntaxKind.SourceFile
&& node.parent.kind !== ts.SyntaxKind.ImportDeclaration) {
node = node.parent;
}
return node.parent && node.parent.kind === ts.SyntaxKind.ImportDeclaration;
}
function isStringLiteralInImportRequireDeclaration(node: ts.Node) {
if (node.kind !== ts.SyntaxKind.StringLiteral) return false;
while (node.parent.kind !== ts.SyntaxKind.SourceFile
&& node.parent.kind !== ts.SyntaxKind.ImportEqualsDeclaration) {
node = node.parent;
}
return node.parent && node.parent.kind === ts.SyntaxKind.ImportEqualsDeclaration;
}
/** Removes the quote characters / `.` and `/` as they cause fuzzaldrin to break */
function sanitizePrefix(prefix: string){
const result = prefix.replace(/\.|\/|\'|\"|/g, '');
return result;
}
export function getPathCompletionsForImport(query: GetPathCompletions): types.PathCompletion[] {
var project = query.project;
var sourceDir = path.dirname(query.filePath);
var filePaths = project.configFile.project.files.filter(p => p !== query.filePath && !p.endsWith('.json'));
var files: {
fileName: string;
relativePath: string;
fullPath: string;
}[] = [];
var externalModules = getExternalModuleNames(project.languageService.getProgram());
externalModules.forEach(e => files.push({
fileName: `${e}`,
relativePath: e,
fullPath: e
}));
filePaths.forEach(p=> {
files.push({
fileName: fsu.removeExt(utils.getFileName(p)),
relativePath: fsu.removeExt(fsu.makeRelativePath(sourceDir, p)),
fullPath: p
});
});
const sanitizedPrefix = sanitizePrefix(query.prefix);
const endsInPunctuation: boolean = utils.prefixEndsInPunctuation(sanitizedPrefix);
if (!endsInPunctuation)
files = fuzzaldrin.filter(files, sanitizedPrefix, { key: 'fileName' });
return files;
}
/**
* Very similar to above. But
* - aborts if position not valid to autocomplete
* - automatically excludes `externalModules` if position is reference tag
*/
export function getPathCompletionsForAutocomplete(query: GetPathCompletionsForAutocomplete): types.PathCompletionForAutocomplete[] {
const sourceFile = query.project.languageService.getNonBoundSourceFile(query.filePath);
const positionNode = ts.getTokenAtPosition(sourceFile, query.position, true);
/** Note: in referenceTag is not supported yet */
const inReferenceTagPath = false;
const inES6ModuleImportString = isStringLiteralInES6ImportDeclaration(positionNode);
const inImportRequireString = isStringLiteralInImportRequireDeclaration(positionNode);
if (!inReferenceTagPath && !inES6ModuleImportString && !inImportRequireString){
return [];
}
/** We have to be in a string literal (as reference tag isn't supproted yet) */
const pathStringText = positionNode.getFullText();
const leadingText = pathStringText.match(/^\s+['|"]/g);
const trailingText = pathStringText.match(/['|"]$/g);
const from = leadingText ? positionNode.pos + leadingText[0].length : positionNode.pos;
const to = trailingText ? positionNode.end - trailingText[0].length : positionNode.end;
const pathStringRange = {
from,
to
};
// console.log({textThatWillBeReplaced:sourceFile.getFullText().substr(from, to-from)}); // DEBUG
var project = query.project;
var sourceDir = path.dirname(query.filePath);
var filePaths = project.configFile.project.files.filter(p=> p !== query.filePath && !p.endsWith('.json'));
var files: {
fileName: string;
relativePath: string;
fullPath: string;
pathStringRange: {
from: number,
to: number,
}
}[] = [];
if (!inReferenceTagPath) {
var externalModules = getExternalModuleNames(project.languageService.getProgram());
externalModules.forEach(e=> files.push({
fileName: `${e}`,
relativePath: e,
fullPath: e,
pathStringRange,
}));
}
filePaths.forEach(p => {
const lowerCaseExtRemoved = fsu.removeExt(p.toLowerCase());
if (
lowerCaseExtRemoved.endsWith('/index')
|| lowerCaseExtRemoved.endsWith('/index.d')
) {
p = utils.getDirectory(p);
}
files.push({
fileName: fsu.removeExt(utils.getFileName(p)),
relativePath: fsu.removeExt(fsu.makeRelativePath(sourceDir, p)),
fullPath: p,
pathStringRange,
});
});
const sanitizedPrefix = sanitizePrefix(query.prefix);
return files;
}