UNPKG

alm

Version:

The best IDE for TypeScript

191 lines (169 loc) 6.57 kB
/** * TODO: THIS ISNT USED YET. * This is used to provide autocomplete / error handling for config files like * alm.ts // Planned * gruntfile.ts // Planned * gulpfile.js // Planned * webpack.config.js // Planned */ import * as utils from "../../../common/utils"; import { Types } from "../../../socket/socketContract"; import * as lsh from "../../../languageServiceHost/languageServiceHost"; import fuzzaldrin = require('fuzzaldrin'); type SupportedFileConfig = { // For `.json` files we add some *var* declaration as a prefix into the code we feed to the langauge service prelude: string; } /** * A simpler project, wraps LanguageServiceHost and LanguageService * with default options we need for config purposes */ class Project { languageService: ts.LanguageService; languageServiceHost: lsh.LanguageServiceHost; constructor() { this.languageServiceHost = new lsh.LanguageServiceHost(undefined, { allowNonTsExtensions: true, allowJs: true, noLib: true, // Will result in lots of errors but we don't want lib.d.ts completions }); this.languageService = ts.createLanguageService(this.languageServiceHost, ts.createDocumentRegistry()); } isSupportedFile = (filePath: string): SupportedFileConfig | null => { const supportedFileNames: { [filename: string]: SupportedFileConfig } = { 'tsconfig.json': { prelude: 'var prelude: {bar: string, bas: "a" | "b"} = ', } } const fileName = utils.getFileName(filePath); return supportedFileNames[fileName]; } } export const project = new Project(); import * as fmc from "../../disk/fileModelCache"; // On open add // On edit edit // On save reload fmc.didOpenFile.on(e => { const config = project.isSupportedFile(e.filePath); if (config) { const prelude = config.prelude + '\n'; project.languageServiceHost.addScript(e.filePath, prelude + e.contents); } }); fmc.didEdits.on(e => { const config = project.isSupportedFile(e.filePath); if (config) { const prelude = config.prelude; const { filePath, edits: codeEdits } = e; codeEdits.forEach(codeEdit => { const from = { line: codeEdit.from.line + 1, ch: codeEdit.from.ch }; const to = { line: codeEdit.to.line + 1, ch: codeEdit.to.ch }; project.languageServiceHost.applyCodeEdit(filePath, from, to, codeEdit.newText); debouncedErrorUpdate(filePath); }); } }); fmc.savedFileChangedOnDisk.on(e => { const config = project.isSupportedFile(e.filePath); if (config) { const prelude = config.prelude + '\n'; project.languageServiceHost.setContents(e.filePath, prelude + e.contents); } }); // TODO: // On edit debounce error update // Provide autocomplete const debouncedErrorUpdate = utils.debounce((filePath: string) => { // Unlike the big brother Project this one only does live linting on the current file }, 500); /** * Provide autocomplete */ export function getCompletionsAtPosition(query: Types.GetCompletionsAtPositionQuery): Promise<Types.GetCompletionsAtPositionResponse> { /** * Customizations for json lookup */ const config = project.isSupportedFile(query.filePath); const position = query.position + config.prelude.length; /** The rest is conventional get completions logic: */ const { filePath, prefix } = query; const service = project.languageService; const completions: ts.CompletionInfo = service.getCompletionsAtPosition(filePath, position, undefined); let completionList = completions ? completions.entries.filter(x => !!x) : []; const endsInPunctuation = utils.prefixEndsInPunctuation(prefix); if (prefix.length && prefix.trim().length && !endsInPunctuation) { // Didn't work good for punctuation completionList = fuzzaldrin.filter(completionList, prefix.trim(), { key: 'name' }); } /** Doing too many suggestions is slowing us down in some cases */ let maxSuggestions = 50; /** Doc comments slow us down tremendously */ let maxDocComments = 10; // limit to maxSuggestions if (completionList.length > maxSuggestions) completionList = completionList.slice(0, maxSuggestions); // Potentially use it more aggresively at some point // This queries the langauge service so its a bit slow function docComment(c: ts.CompletionEntry): { /** The display parts e.g. (a:number)=>string */ display: string; /** The doc comment */ comment: string; } { const completionDetails = project.languageService.getCompletionEntryDetails(filePath, position, c.name, undefined, undefined); const comment = ts.displayPartsToString(completionDetails.documentation || []); // Show the signatures for methods / functions var display: string; if (c.kind == "method" || c.kind == "function" || c.kind == "property") { let parts = completionDetails.displayParts || []; // don't show `(method)` or `(function)` as that is taken care of by `kind` if (parts.length > 3) { parts = parts.splice(3); } display = ts.displayPartsToString(parts); } else { display = ''; } display = display.trim(); return { display: display, comment: comment }; } let completionsToReturn: Types.Completion[] = completionList.map((c, index) => { if (index < maxDocComments) { var details = docComment(c); } else { details = { display: '', comment: '' } } return { name: c.name, kind: c.kind, comment: details.comment, display: details.display }; }); // console.log('here2', completionsToReturn); /** * More json customizations */ completionsToReturn = completionsToReturn /** remove keywords */ .filter(c => c.kind != "keyword") /** Remove any types introduced by the prelude */ .filter(c => c.kind != "var"); /** * Make completions json friendly */ completionsToReturn.forEach(c => { if (c.name !== "false" && c.name !== "true") { c.name = `"${c.name}"` } }); return utils.resolve({ completions: completionsToReturn, endsInPunctuation: endsInPunctuation }); }