export function getCompletions()

in pxtcompiler/emitter/languageservice.ts [292:617]


    export function getCompletions(v: OpArg) {
        const { fileName, fileContent, position, wordStartPos, wordEndPos, runtime } = v
        let src: string = fileContent
        if (fileContent) {
            host.setFile(fileName, fileContent);
        }

        const tsFilename = filenameWithExtension(fileName, "ts");

        const span: PosSpan = { startPos: wordStartPos, endPos: wordEndPos }

        const isPython = /\.py$/.test(fileName);

        const r: CompletionInfo = {
            entries: [],
            isMemberCompletion: false,
            isNewIdentifierLocation: true,
            isTypeLocation: false,
            namespace: [],
        };

        // get line text
        let lastNl = src.lastIndexOf("\n", position - 1)
        lastNl = Math.max(0, lastNl)
        const lineText = src.substring(lastNl + 1, position)

        // are we on a line comment, if so don't show completions
        // NOTE: multi-line comments and string literals are handled
        //  later as they require parsing
        const lineCommentStr = isPython ? "#" : "//"
        if (lineText.trim().startsWith(lineCommentStr)) {
            return r;
        }

        let dotIdx = -1
        let complPosition = -1
        for (let i = position - 1; i >= 0; --i) {
            if (src[i] == ".") {
                dotIdx = i
                break
            }
            if (!/\w/.test(src[i]))
                break
            if (complPosition == -1)
                complPosition = i
        }

        if (dotIdx == position - 1) {
            // "foo.|" -> we add "_" as field name to minimize the risk of a parse error
            src = src.slice(0, position) + "_" + src.slice(position)
        } else if (complPosition == -1) {
            src = src.slice(0, position) + "_" + src.slice(position)
            complPosition = position
        }

        const isMemberCompletion = dotIdx !== -1
        r.isMemberCompletion = isMemberCompletion

        const partialWord = isMemberCompletion ? src.slice(dotIdx + 1, wordEndPos) : src.slice(wordStartPos, wordEndPos)

        const MAX_SYMBOLS_BEFORE_FILTER = 50
        const MAX_SYMBOLS = 100

        if (isMemberCompletion)
            complPosition = dotIdx

        const entries: pxt.Map<CompletionSymbol> = {};

        let opts = cloneCompileOpts(host.opts)
        opts.fileSystem[fileName] = src
        addApiInfo(opts);
        opts.syntaxInfo = {
            position: complPosition,
            type: r.isMemberCompletion ? "memberCompletion" : "identifierCompletion"
        };

        let resultSymbols: CompletionSymbol[] = []

        let tsPos: number;
        if (isPython) {
            // for Python, we need to transpile into TS and map our location into
            // TS
            const res = transpile.pyToTs(opts)
            if (res.syntaxInfo && res.syntaxInfo.symbols) {
                resultSymbols = completionSymbols(res.syntaxInfo.symbols, COMPLETION_DEFAULT_WEIGHT);
            }
            if (res.globalNames)
                lastGlobalNames = res.globalNames

            if (!resultSymbols.length && res.globalNames) {
                resultSymbols = completionSymbols(pxt.U.values(res.globalNames), COMPLETION_DEFAULT_WEIGHT)
            }

            // update our language host
            Object.keys(res.outfiles)
                .forEach(k => {
                    if (k === tsFilename) {
                        host.setFile(k, res.outfiles[k])
                    }
                })

            // convert our location from python to typescript
            if (res.sourceMap) {
                const pySrc = src
                const tsSrc = res.outfiles[tsFilename] || ""
                const srcMap = pxtc.BuildSourceMapHelpers(res.sourceMap, tsSrc, pySrc)

                const smallest = srcMap.py.smallestOverlap(span)
                if (smallest) {
                    tsPos = smallest.ts.startPos
                }
            }

            // filter based on word match if we get too many (ideally we'd leave this filtering for monaco as it's
            // better at fuzzy matching and fluidly changing but for performance reasons we want to do it here)
            if (!isMemberCompletion && resultSymbols.length > MAX_SYMBOLS_BEFORE_FILTER) {
                resultSymbols = resultSymbols
                    .filter(s => (isPython ? s.symbol.pyQName : s.symbol.qName).toLowerCase().indexOf(partialWord.toLowerCase()) >= 0)
            }

            opts.ast = true;
            const ts2asm = compile(opts, service);
        } else {
            tsPos = position
            opts.ast = true;
            host.setOpts(opts)
            const res = runConversionsAndCompileUsingService()
        }

        const prog = service.getProgram()
        const tsAst = prog.getSourceFile(tsFilename)
        const tc = prog.getTypeChecker()
        let tsNode = findInnerMostNodeAtPosition(tsAst, tsPos);
        const commentMap = pxtc.decompiler.buildCommentMap(tsAst);

        // abort if we're in a comment
        const inComment = commentMap.some(range => range.start <= position && position <= range.end)
        if (inComment) {
            return r;
        }

        // abort if we're in a string literal
        if (tsNode) {
            const stringLiteralKinds = [SK.StringLiteral, SK.FirstTemplateToken, SK.NoSubstitutionTemplateLiteral];
            const inLiteral = stringLiteralKinds.some(k => tsNode.kind === k)
            if (inLiteral) {
                return r;
            }
        }

        // determine the current namespace
        r.namespace = getCurrentNamespaces(tsNode)

        // special handing for member completion
        let didFindMemberCompletions = false;
        if (isMemberCompletion) {
            const propertyAccessTarget = findInnerMostNodeAtPosition(tsAst, isPython ? tsPos : dotIdx - 1)

            if (propertyAccessTarget) {
                let type: Type;

                const symbol = tc.getSymbolAtLocation(propertyAccessTarget);
                if (symbol?.members?.size > 0) {
                    // Some symbols for nodes like "this" are directly the symbol for the type (e.g. "this" gives "Foo" class symbol)
                    type = tc.getDeclaredTypeOfSymbol(symbol)
                }
                else if (symbol) {
                    // Otherwise we use the typechecker to lookup the symbol type
                    type = tc.getTypeOfSymbolAtLocation(symbol, propertyAccessTarget);
                }
                else {
                    type = tc.getTypeAtLocation(propertyAccessTarget);
                }

                if (type) {
                    const qname = type.symbol ? tc.getFullyQualifiedName(type.symbol) : tsTypeToPxtTypeString(type, tc);

                    if (qname) {
                        const props = type.getApparentProperties()
                            .map(prop => qname + "." + prop.getName())
                            .map(propQname => lastApiInfo.apis.byQName[propQname])
                            .filter(prop => !!prop)
                            .map(prop => completionSymbol(prop, COMPLETION_DEFAULT_WEIGHT));

                        resultSymbols = props;
                        didFindMemberCompletions = true;
                    }
                }
            }
        }

        const allSymbols = pxt.U.values(lastApiInfo.apis.byQName)

        if (resultSymbols.length === 0) {
            // if by this point we don't yet have a specialized set of results (like those for member completion), use all global api symbols as the start and filter by matching prefix if possible
            let wordMatching = allSymbols.filter(s => (isPython ? s.pyQName : s.qName).toLowerCase().indexOf(partialWord.toLowerCase()) >= 0)
            resultSymbols = completionSymbols(wordMatching, COMPLETION_DEFAULT_WEIGHT)
        }

        // gather local variables that won't have pxt symbol info
        if (!isPython && !didFindMemberCompletions) {
            // TODO: share this with the "syntaxinfo" service

            // use the typescript service to get symbols in scope
            tsNode = findInnerMostNodeAtPosition(tsAst, wordStartPos);
            if (!tsNode)
                tsNode = tsAst.getSourceFile()
            let symSearch = SymbolFlags.Variable;
            let inScopeTsSyms = tc.getSymbolsInScope(tsNode, symSearch);
            // filter these to just what's at the cursor, otherwise we get things
            //  like JS Array methods we don't support
            let matchStr = tsNode.getText()
            if (matchStr !== "_") // if have a real identifier ("_" is a placeholder we added), filter to prefix matches
                inScopeTsSyms = inScopeTsSyms.filter(s => s.name.indexOf(matchStr) >= 0)

            // convert these to pxt symbols
            let inScopePxtSyms = inScopeTsSyms
                .map(t => {
                    let pxtSym = getPxtSymbolFromTsSymbol(t, lastApiInfo.apis, tc)
                    if (!pxtSym) {
                        let tsType = tc.getTypeOfSymbolAtLocation(t, tsNode);
                        pxtSym = makePxtSymbolFromTsSymbol(t, tsType)
                    }
                    return pxtSym
                })
                .filter(s => !!s)
                .map(s => completionSymbol(s, COMPLETION_DEFAULT_WEIGHT))

            // in scope locals should be weighter higher
            inScopePxtSyms.forEach(s => s.weight += COMPLETION_IN_SCOPE_VAR_WEIGHT)

            resultSymbols = [...resultSymbols, ...inScopePxtSyms]
        }

        // special handling for call expressions
        const call = getParentCallExpression(tsNode)
        if (call) {
            // which argument are we ?
            let paramIdx = findCurrentCallArgIdx(call, tsNode, tsPos)

            // if we're not one of the arguments, are we at the
            // determine parameter idx

            if (paramIdx >= 0) {
                const blocksInfo = blocksInfoOp(lastApiInfo.apis, runtime.bannedCategories);
                const callSym = getCallSymbol(call)
                if (callSym) {
                    if (paramIdx >= callSym.parameters.length)
                        paramIdx = callSym.parameters.length - 1
                    const param = getParameter(callSym, paramIdx, blocksInfo) // shakao get param type
                    if (param) {
                        // weight the results higher if they return the correct type for the parameter
                        const matchingApis = getApisForTsType(param.type, call, tc, resultSymbols, param.isEnum);
                        matchingApis.forEach(match => match.weight = COMPLETION_MATCHING_PARAM_TYPE_WEIGHT);
                    }
                }
            }
        }

        // add in keywords
        if (!isMemberCompletion) {
            // TODO: use more context to filter keywords
            //      e.g. "while" shouldn't show up in an expression
            let keywords: string[];
            if (isPython) {
                let keywordsMap = (pxt as any).py.keywords as Map<boolean>
                keywords = Object.keys(keywordsMap)
            } else {
                keywords = [...ts.pxtc.reservedWords, ...ts.pxtc.keywordTypes]
            }
            let keywordSymbols = keywords
                .filter(k => k.indexOf(partialWord) >= 0)
                .map(makePxtSymbolFromKeyword)
                .map(s => completionSymbol(s, COMPLETION_KEYWORD_WEIGHT))
            resultSymbols = [...resultSymbols, ...keywordSymbols]
        }

        // determine which names are taken for auto-generated variable names
        let takenNames: pxt.Map<SymbolInfo> = {}
        if (isPython && lastGlobalNames) {
            takenNames = lastGlobalNames
        } else {
            takenNames = lastApiInfo.apis.byQName
        }

        // swap aliases, filter symbols
        resultSymbols
            .map(sym => {
                // skip for enum member completions (eg "AnimalMob."" should have "Chicken", not "CHICKEN")
                if (sym.symbol.attributes.alias && !(isMemberCompletion && sym.symbol.kind === SymbolKind.EnumMember)) {
                    return completionSymbol(lastApiInfo.apis.byQName[sym.symbol.attributes.alias], sym.weight);
                } else {
                    return sym;
                }
            })
            .filter(shouldUseSymbol)
            .forEach(sym => {
                entries[sym.symbol.qName] = sym
            })
        resultSymbols = pxt.Util.values(entries)
            .filter(a => !!a && !!a.symbol)

        // sort entries
        resultSymbols.sort(compareCompletionSymbols);

        // limit the number of entries
        if (v.light && resultSymbols.length > MAX_SYMBOLS) {
            resultSymbols = resultSymbols.splice(0, MAX_SYMBOLS)
        }

        // add in snippets if not present already
        const { bannedCategories, screenSize } = v.runtime;
        const blocksInfo = blocksInfoOp(lastApiInfo.apis, bannedCategories)
        const context: SnippetContext = {
            takenNames,
            blocksInfo,
            screenSize,
            apis: lastApiInfo.apis,
            checker: service?.getProgram()?.getTypeChecker()
        }
        resultSymbols.forEach(sym => patchSymbolWithSnippet(sym.symbol, isPython, context))

        r.entries = resultSymbols.map(sym => sym.symbol);

        return r;
    }