UNPKG

alm

Version:

The best IDE for TypeScript

681 lines (680 loc) 29.4 kB
"use strict"; /** * The stuff from project that the frontend queries. * Stuff like autocomplete is a good example. * Errors etc are pushed automatically from `activeProject` and do not belong here :) */ Object.defineProperty(exports, "__esModule", { value: true }); var activeProject = require("./activeProject"); var getProject = activeProject.GetProject.ifCurrentOrErrorOut; var socketContract_1 = require("../../../socket/socketContract"); var types = require("../../../common/types"); var utils = require("../../../common/utils"); var resolve = utils.resolve; var fsu = require("../../utils/fsu"); var tsErrorsCache_1 = require("./cache/tsErrorsCache"); var getPathCompletions_1 = require("./modules/getPathCompletions"); function getCompletionsAtPosition(query) { var filePath = query.filePath, position = query.position, prefix = query.prefix; var project = getProject(query.filePath); var service = project.languageService; var languageServiceHost = project.languageServiceHost; var completions = service.getCompletionsAtPosition(filePath, position, undefined); var completionList = completions ? completions.entries.filter(function (x) { return !!x; }) : []; var endsInPunctuation = utils.prefixEndsInPunctuation(prefix); /** Doing too many suggestions is slowing us down in some cases */ var maxSuggestions = 10000; // limit to maxSuggestions if (completionList.length > maxSuggestions) completionList = completionList.slice(0, maxSuggestions); var completionsToReturn = completionList.map(function (c, index) { return { name: c.name, kind: c.kind, comment: '', display: '' }; }); /** * Add function signature help */ if (query.prefix == '(') { var signatures_1 = service.getSignatureHelpItems(query.filePath, query.position); if (signatures_1 && signatures_1.items) { signatures_1.items.forEach(function (item, index) { var template = item.parameters.map(function (p, i) { var display = '{{' + (i + 1) + ':' + ts.displayPartsToString(p.displayParts) + '}}'; return display; }).join(ts.displayPartsToString(item.separatorDisplayParts)); var name = item.parameters.map(function (p) { return ts.displayPartsToString(p.displayParts); }) .join(ts.displayPartsToString(item.separatorDisplayParts)); // e.g. test(something:string):any; // prefix: test( // template: {{something}} // suffix: ): any; var description = ts.displayPartsToString(item.prefixDisplayParts) + template + ts.displayPartsToString(item.suffixDisplayParts); completionsToReturn.unshift({ kind: types.completionKindSnippet, /** We use `(sig)` prefix to make sure its sorted by monaco to be at the top */ name: '(sig) ' + name, insertText: template, display: 'function signature', comment: "Overload " + (index + 1) + " of " + signatures_1.items.length }); }); } } /** * Add file path completions */ var pathCompletions = getPathCompletions_1.getPathCompletionsForAutocomplete({ position: position, project: project, filePath: filePath, prefix: prefix }); if (pathCompletions.length) { completionsToReturn = pathCompletions.map(function (f) { var result = { kind: types.completionKindPath, name: f.relativePath, display: f.fileName, comment: f.fullPath, textEdit: { from: languageServiceHost.getLineAndCharacterOfPosition(filePath, f.pathStringRange.from), to: languageServiceHost.getLineAndCharacterOfPosition(filePath, f.pathStringRange.to), newText: f.relativePath } }; return result; }).concat(completionsToReturn); } return resolve({ completions: completionsToReturn, endsInPunctuation: endsInPunctuation }); } exports.getCompletionsAtPosition = getCompletionsAtPosition; function getCompletionEntryDetails(query) { var project = getProject(query.filePath); var service = project.languageService; var filePath = query.filePath, position = query.position, label = query.label; var completionDetails = project.languageService.getCompletionEntryDetails(filePath, position, label, undefined, undefined); /** * For JS Projects, TS will add all sorts of globals as members (because it cannot know for sure) * However if you try to `getCompletionEntryDetails` for them, you will get `undefined`. */ if (!completionDetails) return resolve({ display: label, comment: '' }); var comment = ts.displayPartsToString(completionDetails.documentation || []); var display = ts.displayPartsToString(completionDetails.displayParts || []); var result = { display: display, comment: comment }; return resolve(result); } exports.getCompletionEntryDetails = getCompletionEntryDetails; function quickInfo(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost; var errors = positionErrors({ filePath: query.filePath, position: query.position }); var info = project.languageService.getQuickInfoAtPosition(query.filePath, query.position); if (!info && !errors.length) { return Promise.resolve({ valid: false }); } else { return resolve({ valid: true, info: info && { name: ts.displayPartsToString(info.displayParts || []), comment: ts.displayPartsToString(info.documentation || []), range: { from: project.languageServiceHost.getLineAndCharacterOfPosition(query.filePath, info.textSpan.start), to: project.languageServiceHost.getLineAndCharacterOfPosition(query.filePath, info.textSpan.start + info.textSpan.length), } }, errors: errors }); } } exports.quickInfo = quickInfo; /** Utility */ function positionErrors(query) { var project = getProject(query.filePath); if (!project.includesSourceFile(query.filePath)) { return []; } var editorPos = project.languageServiceHost.getLineAndCharacterOfPosition(query.filePath, query.position); var errors = tsErrorsCache_1.errorsCache.getErrorsForFilePath(query.filePath); errors = errors.filter(function (e) { // completely contained in the multiline return (e.from.line < editorPos.line && e.to.line > editorPos.line) // error is single line and on the same line and characters match || (e.from.line == e.to.line && e.from.line == editorPos.line && e.from.ch <= editorPos.ch && e.to.ch >= editorPos.ch); }); return errors; } function getRenameInfo(query) { var project = getProject(query.filePath); var findInStrings = false, findInComments = false; var info = project.languageService.getRenameInfo(query.filePath, query.position); if (info && info.canRename) { var locations = {}; project.languageService.findRenameLocations(query.filePath, query.position, findInStrings, findInComments) .forEach(function (loc) { if (!locations[loc.fileName]) locations[loc.fileName] = []; // Using unshift makes them with maximum value on top ;) locations[loc.fileName].unshift(loc.textSpan); }); return resolve({ canRename: true, localizedErrorMessage: info.localizedErrorMessage, displayName: info.displayName, fullDisplayName: info.fullDisplayName, kind: info.kind, kindModifiers: info.kindModifiers, triggerSpan: info.triggerSpan, locations: locations }); } else { return resolve({ canRename: false }); } } exports.getRenameInfo = getRenameInfo; function getDefinitionsAtPosition(query) { var project = getProject(query.filePath); var definitions = project.languageService.getDefinitionAtPosition(query.filePath, query.position); var projectFileDirectory = project.configFile.projectFileDirectory; if (!definitions || !definitions.length) return resolve({ projectFileDirectory: projectFileDirectory, definitions: [] }); return resolve({ projectFileDirectory: projectFileDirectory, definitions: definitions.map(function (d) { // If we can get the filename *we are in the same program :P* var pos = project.languageServiceHost.getLineAndCharacterOfPosition(d.fileName, d.textSpan.start); return { filePath: d.fileName, position: pos, span: d.textSpan, }; }) }); } exports.getDefinitionsAtPosition = getDefinitionsAtPosition; var langHelp_1 = require("./modules/langHelp"); function getDoctorInfo(query) { var project = getProject(query.filePath); var filePath = query.filePath; var position = project.languageServiceHost.getPositionOfLineAndCharacter(query.filePath, query.editorPosition.line, query.editorPosition.ch); // Get langHelp var program = project.languageService.getProgram(); var sourceFile = program.getSourceFile(query.filePath); var positionNode = ts.getTokenAtPosition(sourceFile, position, true); var langHelp = langHelp_1.getLangHelp(positionNode); // Just collect other responses var defPromised = getDefinitionsAtPosition({ filePath: filePath, position: position }); var quickInfoPromised = quickInfo({ filePath: filePath, position: position }); return defPromised.then(function (defRes) { return quickInfoPromised.then(function (infoRes) { return getReferences({ filePath: filePath, position: position }).then(function (refRes) { var valid = !!defRes.definitions.length || infoRes.valid || !!refRes.references.length || !!langHelp; return { valid: valid, definitions: defRes.definitions, quickInfo: infoRes.valid && infoRes.info.name ? { name: infoRes.info.name, comment: infoRes.info.comment } : null, langHelp: langHelp, references: refRes.references }; }); }); }); } exports.getDoctorInfo = getDoctorInfo; function getReferences(query) { var project = getProject(query.filePath); var languageService = project.languageService; var references = []; var refs = languageService.getReferencesAtPosition(query.filePath, query.position) || []; references = refs.map(function (r) { var position = project.languageServiceHost.getLineAndCharacterOfPosition(r.fileName, r.textSpan.start); return { filePath: r.fileName, position: position, span: r.textSpan }; }); return resolve({ references: references }); } exports.getReferences = getReferences; /** * Formatting */ var formatting = require("./modules/formatting"); function formatDocument(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost, languageService = project.languageService; var tsresult = formatting.formatDocument(project, query.filePath, query.editorOptions); var edits = tsresult.map(function (res) { var result = { from: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start), to: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start + res.span.length), newText: res.newText }; return result; }); return resolve({ edits: edits }); } exports.formatDocument = formatDocument; function formatDocumentRange(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost, languageService = project.languageService; var tsresult = formatting.formatDocumentRange(project, query.filePath, query.from, query.to, query.editorOptions); var edits = tsresult.map(function (res) { var result = { from: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start), to: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start + res.span.length), newText: res.newText }; return result; }); return resolve({ edits: edits }); } exports.formatDocumentRange = formatDocumentRange; function getFormattingEditsAfterKeystroke(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost, languageService = project.languageService; var position = languageServiceHost.getPositionOfLineAndCharacter(query.filePath, query.editorPosition.line, query.editorPosition.ch); var options = formatting.completeFormatCodeOptions(query.editorOptions, project.configFile.project.formatCodeOptions); var tsresult = languageService.getFormattingEditsAfterKeystroke(query.filePath, position, query.key, options) || []; var edits = tsresult.map(function (res) { var result = { from: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start), to: languageServiceHost.getLineAndCharacterOfPosition(query.filePath, res.span.start + res.span.length), newText: res.newText }; return result; }); return resolve({ edits: edits }); } exports.getFormattingEditsAfterKeystroke = getFormattingEditsAfterKeystroke; var removeUnusedImports_1 = require("./modules/removeUnusedImports"); function removeUnusedImports(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost, languageService = project.languageService; return resolve(removeUnusedImports_1.removeUnusedImports(query.filePath, languageService)); } exports.removeUnusedImports = removeUnusedImports; /** * Symbol search */ //-------------------------------------------------------------------------- // getNavigateToItems //-------------------------------------------------------------------------- // Look at // https://github.com/Microsoft/TypeScript/blob/master/src/services/navigateTo.ts // for inspiration // Reason for forking: // didn't give all results // gave results from lib.d.ts // I wanted the practice function getSymbolsForFile(project, sourceFile) { var getNodeKind = ts.getNodeKind; function getDeclarationName(declaration) { var result = ts.getNameOfDeclaration(declaration); if (result === undefined) { return ''; } return result.getText(); } function getTextOfIdentifierOrLiteral(node) { if (node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral) { return node.text; } return undefined; } var items = []; var declarations = sourceFile.getNamedDeclarations(); declarations.forEach(function (value, key) { value.forEach(function (declaration) { var item = { name: getDeclarationName(declaration), kind: getNodeKind(declaration), filePath: sourceFile.fileName, fileName: utils.getFileName(sourceFile.fileName), position: project.languageServiceHost.getLineAndCharacterOfPosition(sourceFile.fileName, declaration.getStart()) }; items.push(item); }); }); return items; } function getNavigateToItems(query) { var project = activeProject.GetProject.getCurrentIfAny(); var languageService = project.languageService; var items = []; for (var _i = 0, _a = project.getProjectSourceFiles(); _i < _a.length; _i++) { var file = _a[_i]; getSymbolsForFile(project, file).forEach(function (i) { return items.push(i); }); } return utils.resolve({ items: items }); } exports.getNavigateToItems = getNavigateToItems; function getNavigateToItemsForFilePath(query) { var project = activeProject.GetProject.getCurrentIfAny(); var languageService = project.languageService; var file = project.getSourceFile(query.filePath); var items = getSymbolsForFile(project, file); return utils.resolve({ items: items }); } exports.getNavigateToItemsForFilePath = getNavigateToItemsForFilePath; /** * Dependency View */ var programDependencies_1 = require("./modules/programDependencies"); function getDependencies(query) { var project = activeProject.GetProject.getCurrentIfAny(); var links = programDependencies_1.getProgramDependencies(project.configFile, project.languageService.getProgram()); return resolve({ links: links }); } exports.getDependencies = getDependencies; /** * AST View */ var astToText_1 = require("./modules/astToText"); function getAST(query) { var project = getProject(query.filePath); var service = project.languageService; var files = service.getProgram().getSourceFiles().filter(function (x) { return x.fileName == query.filePath; }); if (!files.length) resolve({}); var sourceFile = files[0]; var root = query.mode === socketContract_1.Types.ASTMode.visitor ? astToText_1.astToText(sourceFile) : astToText_1.astToTextFull(sourceFile); return resolve({ root: root }); } exports.getAST = getAST; /** * JS Ouput */ var building_1 = require("./modules/building"); function getJSOutputStatus(query, autoEmit) { if (autoEmit === void 0) { autoEmit = true; } var project = activeProject.GetProject.ifCurrent(query.filePath); if (!project) { return { inActiveProject: false }; } var jsFile = building_1.getRawJsOutput(project, query.filePath); /** * We just read/write from disk for now * Would be better if it interacted with master */ var getContents = function (filePath) { return fsu.existsSync(filePath) ? fsu.readFile(filePath) : ''; }; var setContents = fsu.writeFile; /** * Note: If we have compileOnSave as false then the output status isn't relevant */ var noJsFile = (project.configFile.project.compileOnSave === false) || (project.configFile.inMemory === true) || !jsFile; var state = noJsFile ? types.JSOutputState.NoJSFile : getContents(jsFile.filePath) === jsFile.contents ? types.JSOutputState.JSUpToDate : types.JSOutputState.JSOutOfDate; /** * If the state is JSOutOfDate we can easily fix that to bring it up to date for `compileOnSave` */ if (autoEmit && state === types.JSOutputState.JSOutOfDate && project.configFile.project.compileOnSave !== false) { setContents(jsFile.filePath, jsFile.contents); state = types.JSOutputState.JSUpToDate; } var outputStatus = { inputFilePath: query.filePath, state: state, outputFilePath: jsFile && jsFile.filePath }; return { inActiveProject: true, outputStatus: outputStatus }; } exports.getJSOutputStatus = getJSOutputStatus; var qf = require("./quickFix/quickFix"); var quickFixRegistry_1 = require("./quickFix/quickFixRegistry"); function getDiagnositcsByFilePath(query) { var project = getProject(query.filePath); var diagnostics = project.languageService.getSyntacticDiagnostics(query.filePath); if (diagnostics.length === 0) { diagnostics = project.languageService.getSemanticDiagnostics(query.filePath); } return diagnostics; } function getInfoForQuickFixAnalysis(query) { var project = getProject(query.filePath); var program = project.languageService.getProgram(); var sourceFile = program.getSourceFile(query.filePath); var sourceFileText, fileErrors, positionErrors, positionErrorMessages, positionNode; if (project.includesSourceFile(query.filePath)) { sourceFileText = sourceFile.getFullText(); fileErrors = getDiagnositcsByFilePath(query); /** We want errors that are *touching* and thefore expand the query position by one */ positionErrors = fileErrors.filter(function (e) { return ((e.start - 1) < query.position) && (e.start + e.length + 1) > query.position; }); positionErrorMessages = positionErrors.map(function (e) { return ts.flattenDiagnosticMessageText(e.messageText, '\n'); }); positionNode = ts.getTokenAtPosition(sourceFile, query.position, true); } else { sourceFileText = ""; fileErrors = []; positionErrors = []; positionErrorMessages = []; positionNode = undefined; } var service = project.languageService; var typeChecker = program.getTypeChecker(); return { project: project, program: program, sourceFile: sourceFile, sourceFileText: sourceFileText, fileErrors: fileErrors, positionErrors: positionErrors, positionErrorMessages: positionErrorMessages, position: query.position, positionNode: positionNode, service: service, typeChecker: typeChecker, filePath: query.filePath, formatOptions: { indentSize: query.indentSize } }; } var tsCodefixPrefix = 'CodeFix:'; function getQuickFixes(query) { var project = getProject(query.filePath); var info = getInfoForQuickFixAnalysis(query); // We let the quickFix determine if it wants provide any fixes for this file var fixes = quickFixRegistry_1.allQuickFixes .map(function (x) { var canProvide = x.canProvideFix(info); if (!canProvide) return; else return { key: x.key, display: canProvide.display }; }) .filter(function (x) { return !!x; }); /** * TS Code fixes * They comes with the `changes` on query. So we use that on `get` as well as `apply` */ var tsCodeFixes = project.languageService.getCodeFixesAtPosition(query.filePath, query.position, query.position, info.positionErrors.map(function (e) { return e.code; }), info.formatOptions); if (tsCodeFixes.length) { tsCodeFixes.forEach(function (fix, i) { fixes.unshift({ key: "" + tsCodefixPrefix + i, display: fix.description }); }); } return resolve({ fixes: fixes }); } exports.getQuickFixes = getQuickFixes; function applyQuickFix(query) { var info = getInfoForQuickFixAnalysis(query); /** * If TS Code fix */ if (query.key.startsWith(tsCodefixPrefix)) { /** Find the code fix */ var project = getProject(query.filePath); var tsCodeFixes = project.languageService.getCodeFixesAtPosition(query.filePath, query.position, query.position, info.positionErrors.map(function (e) { return e.code; }), info.formatOptions); var index = +query.key.substr(tsCodefixPrefix.length); var tsCodeFix = tsCodeFixes[index]; /** Map code fix to refactoring */ var refactorings_1 = []; tsCodeFix.changes.forEach(function (change) { change.textChanges.forEach(function (tc) { var res = { filePath: change.fileName, newText: tc.newText, span: tc.span }; refactorings_1.push(res); }); }); return resolve({ refactorings: qf.getRefactoringsByFilePath(refactorings_1) }); } var fix = quickFixRegistry_1.allQuickFixes.filter(function (x) { return x.key == query.key; })[0]; var res = fix.provideFix(info); var refactorings = qf.getRefactoringsByFilePath(res); return resolve({ refactorings: refactorings }); } exports.applyQuickFix = applyQuickFix; /** * Semantic Tree */ function sortNavbarItemsBySpan(items) { items.sort(function (a, b) { return a.spans[0].start - b.spans[0].start; }); // sort children recursively for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; if (item.childItems) { sortNavbarItemsBySpan(item.childItems); } } } function flattenNavBarItems(items) { if (!items.length) return []; var root = items[0]; /** * Where we store the final good ones */ var results = []; /** Just to remove the dupes for different keys */ var resultMapBig = Object.create(null); /** * The same items apprear with differnt indent, and different `spans` * But at least one span seems to match, hence this key(s) function */ var getKeys = function (item) { return item.spans.map(function (span) { return span.start + "-" + item.text; }); }; /** This is used to unflatten the resulting map */ var getParentMapKey = function (item) { return item.spans[0].start + "-" + item.text; }; var parentMap = {}; /** * First create a map of everything */ var addToMap = function (item, parent) { var keys = getKeys(item); var previous = keys.some(function (key) { return !!resultMapBig[key]; }); // If we already have it no need to add it. // This is because the first time it gets added the parent then is the best one if (!previous) { keys.forEach(function (key) { return resultMapBig[key] = item; }); results.push(item); if (item !== root) { var parentMapKey = getParentMapKey(item); parentMap[parentMapKey] = parent; } } // Whatever is in the final map is the version we want to use for parent pointers var itemToUseAsParent = resultMapBig[getParentMapKey(item)]; // Also visit all children item.childItems && item.childItems.forEach(function (child) { return addToMap(child, itemToUseAsParent); }); // Now delete the childItems as they are supposed to be restored by `parentMap` delete item.childItems; }; // Flatten into the map items.forEach(function (item) { return addToMap(item, root); }); // Now restore based on child pointers results.forEach(function (item) { if (item == root) return; var key = getParentMapKey(item); var parent = parentMap[key]; if (!parent.childItems) parent.childItems = []; parent.childItems.push(item); }); // Now we only need the children of the root :) return results[0].childItems || []; } function navigationBarItemToSemanticTreeNode(item, project, query) { var toReturn = { text: item.text, kind: item.kind, kindModifiers: item.kindModifiers, start: project.languageServiceHost.getLineAndCharacterOfPosition(query.filePath, item.spans[0].start), end: project.languageServiceHost.getLineAndCharacterOfPosition(query.filePath, item.spans[0].start + item.spans[0].length), subNodes: item.childItems ? item.childItems.map(function (ci) { return navigationBarItemToSemanticTreeNode(ci, project, query); }) : [] }; return toReturn; } function getSemanticTree(query) { var project = getProject(query.filePath); var navBarItems = project.languageService.getNavigationBarItems(query.filePath); // The nav bar from the language service has nodes at various levels (with duplication) // We want a flat version navBarItems = flattenNavBarItems(navBarItems); // Sort items by first spans: sortNavbarItemsBySpan(navBarItems); // convert to SemanticTreeNodes var nodes = navBarItems.map(function (nbi) { return navigationBarItemToSemanticTreeNode(nbi, project, query); }); return resolve({ nodes: nodes }); } exports.getSemanticTree = getSemanticTree; /** * Document highlights */ function getOccurrencesAtPosition(query) { var project = getProject(query.filePath); var languageServiceHost = project.languageServiceHost; var position = languageServiceHost.getPositionOfLineAndCharacter(query.filePath, query.editorPosition.line, query.editorPosition.ch); var tsresults = project.languageService.getOccurrencesAtPosition(query.filePath, position) || []; var results = tsresults.map(function (res) { var result = { filePath: res.fileName, isWriteAccess: res.isWriteAccess, start: project.languageServiceHost.getLineAndCharacterOfPosition(res.fileName, res.textSpan.start), end: project.languageServiceHost.getLineAndCharacterOfPosition(res.fileName, res.textSpan.start + res.textSpan.length), }; return result; }); return resolve({ results: results }); } exports.getOccurrencesAtPosition = getOccurrencesAtPosition;