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;
}