diff --git a/python-impl/src/main/java/com/jetbrains/python/impl/PyParameterInfoHandler.java b/python-impl/src/main/java/com/jetbrains/python/impl/PyParameterInfoHandler.java index 1dc0f406..3d7dece2 100644 --- a/python-impl/src/main/java/com/jetbrains/python/impl/PyParameterInfoHandler.java +++ b/python-impl/src/main/java/com/jetbrains/python/impl/PyParameterInfoHandler.java @@ -47,346 +47,368 @@ */ @ExtensionImpl public class PyParameterInfoHandler implements ParameterInfoHandler { + @Override + public boolean couldShowInLookup() { + return true; + } - @Override - public boolean couldShowInLookup() { - return true; - } - - @Override - public Object[] getParametersForLookup(LookupElement item, ParameterInfoContext context) { - return ArrayUtil.EMPTY_OBJECT_ARRAY; - } - - @Override - public PyArgumentList findElementForParameterInfo(@Nonnull CreateParameterInfoContext context) { - PyArgumentList argumentList = findArgumentList(context, -1); - if (argumentList != null) { - PyCallExpression callExpr = argumentList.getCallExpression(); - if (callExpr != null) { - TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(argumentList.getProject(), argumentList.getContainingFile()); - PyResolveContext resolveContext = PyResolveContext.noImplicits().withRemote().withTypeEvalContext(typeEvalContext); - PyCallExpression.PyArgumentsMapping mapping = callExpr.mapArguments(resolveContext); - if (mapping.getMarkedCallee() != null) { - context.setItemsToShow(new Object[]{mapping}); - return argumentList; + @Override + public Object[] getParametersForLookup(LookupElement item, ParameterInfoContext context) { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + @Override + @RequiredReadAction + public PyArgumentList findElementForParameterInfo(@Nonnull CreateParameterInfoContext context) { + PyArgumentList argumentList = findArgumentList(context, -1); + if (argumentList != null) { + PyCallExpression callExpr = argumentList.getCallExpression(); + if (callExpr != null) { + TypeEvalContext typeEvalContext = + TypeEvalContext.userInitiated(argumentList.getProject(), argumentList.getContainingFile()); + PyResolveContext resolveContext = PyResolveContext.noImplicits().withRemote().withTypeEvalContext(typeEvalContext); + PyCallExpression.PyArgumentsMapping mapping = callExpr.mapArguments(resolveContext); + if (mapping.getMarkedCallee() != null) { + context.setItemsToShow(new Object[]{mapping}); + return argumentList; + } + } } - } + return null; } - return null; - } - - private static PyArgumentList findArgumentList(ParameterInfoContext context, int parameterListStart) { - int offset = context.getOffset(); - PyArgumentList argumentList = ParameterInfoUtils.findParentOfType(context.getFile(), offset - 1, PyArgumentList.class); - if (argumentList != null) { - TextRange range = argumentList.getTextRange(); - if (parameterListStart >= 0 && range.getStartOffset() != parameterListStart) { - argumentList = PsiTreeUtil.getParentOfType(argumentList, PyArgumentList.class); - } + + @RequiredReadAction + private static PyArgumentList findArgumentList(ParameterInfoContext context, int parameterListStart) { + int offset = context.getOffset(); + PyArgumentList argumentList = ParameterInfoUtils.findParentOfType(context.getFile(), offset - 1, PyArgumentList.class); + if (argumentList != null) { + TextRange range = argumentList.getTextRange(); + if (parameterListStart >= 0 && range.getStartOffset() != parameterListStart) { + argumentList = PsiTreeUtil.getParentOfType(argumentList, PyArgumentList.class); + } + } + return argumentList; } - return argumentList; - } - - @Override - public void showParameterInfo(@Nonnull PyArgumentList element, @Nonnull CreateParameterInfoContext context) { - context.showHint(element, element.getTextOffset(), this); - } - - @Override - public PyArgumentList findElementForUpdatingParameterInfo(@Nonnull UpdateParameterInfoContext context) { - return findArgumentList(context, context.getParameterListStart()); - } - - /** - * Note: instead of parameter index, we directly store parameter's offset for later use.
- * We cannot store an index since we cannot determine what is an argument until we actually map arguments to parameters. - * This is because a tuple in arguments may be a whole argument or map to a tuple parameter. - */ - @Override - @RequiredReadAction - public void updateParameterInfo(@Nonnull PyArgumentList argumentList, @Nonnull UpdateParameterInfoContext context) { - if (context.getParameterOwner() != argumentList) { - context.removeHint(); - return; + + @Override + public void showParameterInfo(@Nonnull PyArgumentList element, @Nonnull CreateParameterInfoContext context) { + context.showHint(element, element.getTextOffset(), this); } - // align offset to nearest expression; context may point to a space, etc. - List flat_args = PyUtil.flattenedParensAndLists(argumentList.getArguments()); - int alleged_cursor_offset = context.getOffset(); // this is already shifted backwards to skip spaces - - TextRange argListTextRange = argumentList.getTextRange(); - if (!argListTextRange.contains(alleged_cursor_offset) && argumentList.getText().endsWith(")")) { - context.removeHint(); - return; + + @Override + @RequiredReadAction + public PyArgumentList findElementForUpdatingParameterInfo(@Nonnull UpdateParameterInfoContext context) { + return findArgumentList(context, context.getParameterListStart()); } - PsiFile file = context.getFile(); - CharSequence chars = file.getViewProvider().getContents(); - int offset = -1; - for (PyExpression arg : flat_args) { - TextRange range = arg.getTextRange(); - // widen the range to include all whitespace around the arg - int left = CharArrayUtil.shiftBackward(chars, range.getStartOffset() - 1, " \t\r\n"); - int right = CharArrayUtil.shiftForwardCarefully(chars, range.getEndOffset(), " \t\r\n"); - if (arg.getParent() instanceof PyListLiteralExpression || arg.getParent() instanceof PyTupleExpression) { - right = CharArrayUtil.shiftForward(chars, range.getEndOffset(), " \t\r\n])"); - } - - if (left <= alleged_cursor_offset && right >= alleged_cursor_offset) { - offset = range.getStartOffset(); - break; - } + + /** + * Note: instead of parameter index, we directly store parameter's offset for later use.
+ * We cannot store an index since we cannot determine what is an argument until we actually map arguments to parameters. + * This is because a tuple in arguments may be a whole argument or map to a tuple parameter. + */ + @Override + @RequiredReadAction + public void updateParameterInfo(@Nonnull PyArgumentList argumentList, @Nonnull UpdateParameterInfoContext context) { + if (context.getParameterOwner() != argumentList) { + context.removeHint(); + return; + } + // align offset to nearest expression; context may point to a space, etc. + List flat_args = PyUtil.flattenedParensAndLists(argumentList.getArguments()); + int alleged_cursor_offset = context.getOffset(); // this is already shifted backwards to skip spaces + + TextRange argListTextRange = argumentList.getTextRange(); + if (!argListTextRange.contains(alleged_cursor_offset) && argumentList.getText().endsWith(")")) { + context.removeHint(); + return; + } + PsiFile file = context.getFile(); + CharSequence chars = file.getViewProvider().getContents(); + int offset = -1; + for (PyExpression arg : flat_args) { + TextRange range = arg.getTextRange(); + // widen the range to include all whitespace around the arg + int left = CharArrayUtil.shiftBackward(chars, range.getStartOffset() - 1, " \t\r\n"); + int right = CharArrayUtil.shiftForwardCarefully(chars, range.getEndOffset(), " \t\r\n"); + if (arg.getParent() instanceof PyListLiteralExpression || arg.getParent() instanceof PyTupleExpression) { + right = CharArrayUtil.shiftForward(chars, range.getEndOffset(), " \t\r\n])"); + } + + if (left <= alleged_cursor_offset && right >= alleged_cursor_offset) { + offset = range.getStartOffset(); + break; + } + } + context.setCurrentParameter(offset); } - context.setCurrentParameter(offset); - } - - @Override - public String getParameterCloseChars() { - return ",()"; // lpar may mean a nested tuple param, so it's included - } - - @Override - public boolean tracksParameterIndex() { - return false; - } - - @Override - public void updateUI(PyCallExpression.PyArgumentsMapping oldMapping, @Nonnull ParameterInfoUIContext context) { - if (oldMapping == null) { - return; + + @Override + public String getParameterCloseChars() { + return ",()"; // lpar may mean a nested tuple param, so it's included } - PyCallExpression callExpression = oldMapping.getCallExpression(); - PyPsiUtils.assertValid(callExpression); - // really we need to redo analysis every UI update; findElementForParameterInfo isn't called while typing - TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(callExpression.getProject(), callExpression.getContainingFile()); - PyResolveContext resolveContext = PyResolveContext.noImplicits().withRemote().withTypeEvalContext(typeEvalContext); - PyCallExpression.PyArgumentsMapping mapping = callExpression.mapArguments(resolveContext); - PyMarkedCallee marked = mapping.getMarkedCallee(); - if (marked == null) { - return; // resolution failed + + @Override + public boolean tracksParameterIndex() { + return false; } - PyCallable callable = marked.getCallable(); - List parameterList = PyUtil.getParameters(callable, typeEvalContext); - List namedParameters = new ArrayList<>(parameterList.size()); + @Override + @RequiredReadAction + public void updateUI(PyCallExpression.PyArgumentsMapping oldMapping, @Nonnull ParameterInfoUIContext context) { + if (oldMapping == null) { + return; + } + PyCallExpression callExpression = oldMapping.getCallExpression(); + PyPsiUtils.assertValid(callExpression); + // really we need to redo analysis every UI update; findElementForParameterInfo isn't called while typing + TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(callExpression.getProject(), callExpression.getContainingFile()); + PyResolveContext resolveContext = PyResolveContext.noImplicits().withRemote().withTypeEvalContext(typeEvalContext); + PyCallExpression.PyArgumentsMapping mapping = callExpression.mapArguments(resolveContext); + PyMarkedCallee marked = mapping.getMarkedCallee(); + if (marked == null) { + return; // resolution failed + } + PyCallable callable = marked.getCallable(); + + List parameterList = PyUtil.getParameters(callable, typeEvalContext); + List namedParameters = new ArrayList<>(parameterList.size()); - // param -> hint index. indexes are not contiguous, because some hints are parentheses. - Map parameterToIndex = new HashMap<>(); - // formatting of hints: hint index -> flags. this includes flags for parens. - Map> hintFlags = new HashMap<>(); + // param -> hint index. indexes are not contiguous, because some hints are parentheses. + Map parameterToIndex = new HashMap<>(); + // formatting of hints: hint index -> flags. this includes flags for parens. + Map> hintFlags = new HashMap<>(); - List hintsList = buildParameterListHint(parameterList, namedParameters, parameterToIndex, hintFlags); + List hintsList = buildParameterListHint(parameterList, namedParameters, parameterToIndex, hintFlags); - int currentParamOffset = context.getCurrentParameterIndex(); // in Python mode, we get an offset here, not an index! + int currentParamOffset = context.getCurrentParameterIndex(); // in Python mode, we get an offset here, not an index! - // gray out enough first parameters as implicit (self, cls, ...) - for (int i = 0; i < marked.getImplicitOffset(); i += 1) { - hintFlags.get(parameterToIndex.get(namedParameters.get(i))).add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent - } + // gray out enough first parameters as implicit (self, cls, ...) + for (int i = 0; i < marked.getImplicitOffset(); i += 1) { + hintFlags.get(parameterToIndex.get(namedParameters.get(i))) + .add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent + } - List flattenedArgs = PyUtil.flattenedParensAndLists(callExpression.getArguments()); - int lastParamIndex = collectHighlights(mapping, parameterList, parameterToIndex, hintFlags, flattenedArgs, currentParamOffset); + List flattenedArgs = PyUtil.flattenedParensAndLists(callExpression.getArguments()); + int lastParamIndex = collectHighlights(mapping, parameterList, parameterToIndex, hintFlags, flattenedArgs, currentParamOffset); - highlightNext(marked, parameterList, namedParameters, parameterToIndex, hintFlags, flattenedArgs.isEmpty(), lastParamIndex); + highlightNext(marked, parameterList, namedParameters, parameterToIndex, hintFlags, flattenedArgs.isEmpty(), lastParamIndex); - String[] hints = ArrayUtil.toStringArray(hintsList); - if (context instanceof ParameterInfoUIContextEx pic) { - EnumSet[] flags = new EnumSet[hintFlags.size()]; - for (int i = 0; i < flags.length; i += 1) { - flags[i] = hintFlags.get(i); - } - if (hints.length < 1) { - hints = new String[]{CodeInsightLocalize.parameterInfoNoParameters().get()}; - flags = new EnumSet[]{EnumSet.of(ParameterInfoUIContextEx.Flag.DISABLE)}; - } + String[] hints = ArrayUtil.toStringArray(hintsList); + if (context instanceof ParameterInfoUIContextEx pic) { + EnumSet[] flags = new EnumSet[hintFlags.size()]; + for (int i = 0; i < flags.length; i += 1) { + flags[i] = hintFlags.get(i); + } + if (hints.length < 1) { + hints = new String[]{CodeInsightLocalize.parameterInfoNoParameters().get()}; + flags = new EnumSet[]{EnumSet.of(ParameterInfoUIContextEx.Flag.DISABLE)}; + } - //noinspection unchecked - pic.setupUIComponentPresentation(hints, flags, context.getDefaultParameterColor()); - } - else { // fallback, no highlight - StringBuilder signatureBuilder = new StringBuilder(); - if (hints.length > 1) { - for (String s : hints) { - signatureBuilder.append(s); + //noinspection unchecked + pic.setupUIComponentPresentation(hints, flags, context.getDefaultParameterColor()); } - } - else { - signatureBuilder.append(XmlStringUtil.escapeString(CodeInsightLocalize.parameterInfoNoParameters().get())); - } - context.setupUIComponentPresentation(signatureBuilder.toString(), -1, 0, false, false, false, context.getDefaultParameterColor()); - } - } - - @RequiredReadAction - private static void highlightNext(@Nonnull PyMarkedCallee marked, - @Nonnull List parameterList, - @Nonnull List namedParameters, - @Nonnull Map parameterToIndex, - @Nonnull Map> hintFlags, - boolean isArgsEmpty, - int lastParamIndex) { - boolean canOfferNext = true; // can we highlight next unfilled parameter - for (EnumSet set : hintFlags.values()) { - if (set.contains(ParameterInfoUIContextEx.Flag.HIGHLIGHT)) { - canOfferNext = false; - } - } - // highlight the next parameter to be filled - if (canOfferNext) { - int highlightIndex = Integer.MAX_VALUE; // initially beyond reason = no highlight - if (isArgsEmpty) { - highlightIndex = marked.getImplicitOffset(); // no args, highlight first (PY-3690) - } - else if (lastParamIndex < parameterList.size() - 1) { // lastParamIndex not at end, or no args - if (namedParameters.get(lastParamIndex).isPositionalContainer()) { - highlightIndex = lastParamIndex; // stick to *arg + else { // fallback, no highlight + StringBuilder signatureBuilder = new StringBuilder(); + if (hints.length > 1) { + for (String s : hints) { + signatureBuilder.append(s); + } + } + else { + XmlStringUtil.escapeText(CodeInsightLocalize.parameterInfoNoParameters().get(), signatureBuilder); + } + context.setupUIComponentPresentation( + signatureBuilder.toString(), + -1, + 0, + false, + false, + false, + context.getDefaultParameterColor() + ); } - else { - highlightIndex = lastParamIndex + 1; // highlight next + } + + @RequiredReadAction + private static void highlightNext( + @Nonnull PyMarkedCallee marked, + @Nonnull List parameterList, + @Nonnull List namedParameters, + @Nonnull Map parameterToIndex, + @Nonnull Map> hintFlags, + boolean isArgsEmpty, + int lastParamIndex + ) { + boolean canOfferNext = true; // can we highlight next unfilled parameter + for (EnumSet set : hintFlags.values()) { + if (set.contains(ParameterInfoUIContextEx.Flag.HIGHLIGHT)) { + canOfferNext = false; + } } - } - else if (lastParamIndex == parameterList.size() - 1) { // we're right after the end of param list - if (namedParameters.get(lastParamIndex).isPositionalContainer() || namedParameters.get(lastParamIndex).isKeywordContainer()) { - highlightIndex = lastParamIndex; // stick to *arg + // highlight the next parameter to be filled + if (canOfferNext) { + int highlightIndex = Integer.MAX_VALUE; // initially beyond reason = no highlight + if (isArgsEmpty) { + highlightIndex = marked.getImplicitOffset(); // no args, highlight first (PY-3690) + } + else if (lastParamIndex < parameterList.size() - 1) { // lastParamIndex not at end, or no args + if (namedParameters.get(lastParamIndex).isPositionalContainer()) { + highlightIndex = lastParamIndex; // stick to *arg + } + else { + highlightIndex = lastParamIndex + 1; // highlight next + } + } + else if (lastParamIndex == parameterList.size() - 1) { // we're right after the end of param list + if (namedParameters.get(lastParamIndex).isPositionalContainer() || namedParameters.get(lastParamIndex) + .isKeywordContainer()) { + highlightIndex = lastParamIndex; // stick to *arg + } + } + if (highlightIndex < namedParameters.size()) { + hintFlags.get(parameterToIndex.get(namedParameters.get(highlightIndex))).add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); + } } - } - if (highlightIndex < namedParameters.size()) { - hintFlags.get(parameterToIndex.get(namedParameters.get(highlightIndex))).add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); - } } - } - - /** - * match params to available args, highlight current param(s) - * - * @return index of last parameter - */ - @RequiredReadAction - private static int collectHighlights(@Nonnull PyCallExpression.PyArgumentsMapping mapping, - @Nonnull List parameterList, - @Nonnull Map parameterToIndex, - @Nonnull Map> hintFlags, - @Nonnull List flatArgs, - int currentParamOffset) { - PyMarkedCallee callee = mapping.getMarkedCallee(); - assert callee != null; - int lastParamIndex = callee.getImplicitOffset(); - Map mappedParameters = mapping.getMappedParameters(); - Map mappedTupleParameters = mapping.getMappedTupleParameters(); - for (PyExpression arg : flatArgs) { - boolean mustHighlight = arg.getTextRange().contains(currentParamOffset); - PsiElement seeker = arg; - // An argument tuple may have been flattened; find it - while (!(seeker instanceof PyArgumentList) && seeker instanceof PyExpression && !mappedParameters.containsKey(seeker)) { - seeker = seeker.getParent(); - } - if (seeker instanceof PyExpression) { - PyNamedParameter parameter = mappedParameters.get((PyExpression)seeker); - lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); - if (parameter != null) { - highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); - } - } - else if (PyCallExpressionHelper.isVariadicPositionalArgument(arg)) { - for (PyNamedParameter parameter : mapping.getParametersMappedToVariadicPositionalArguments()) { - lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); - highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + + /** + * match params to available args, highlight current param(s) + * + * @return index of last parameter + */ + @RequiredReadAction + private static int collectHighlights( + @Nonnull PyCallExpression.PyArgumentsMapping mapping, + @Nonnull List parameterList, + @Nonnull Map parameterToIndex, + @Nonnull Map> hintFlags, + @Nonnull List flatArgs, + int currentParamOffset + ) { + PyMarkedCallee callee = mapping.getMarkedCallee(); + assert callee != null; + int lastParamIndex = callee.getImplicitOffset(); + Map mappedParameters = mapping.getMappedParameters(); + Map mappedTupleParameters = mapping.getMappedTupleParameters(); + for (PyExpression arg : flatArgs) { + boolean mustHighlight = arg.getTextRange().contains(currentParamOffset); + PsiElement seeker = arg; + // An argument tuple may have been flattened; find it + while (!(seeker instanceof PyArgumentList) && seeker instanceof PyExpression && !mappedParameters.containsKey(seeker)) { + seeker = seeker.getParent(); + } + if (seeker instanceof PyExpression) { + PyNamedParameter parameter = mappedParameters.get(seeker); + lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); + if (parameter != null) { + highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + } + } + else if (PyCallExpressionHelper.isVariadicPositionalArgument(arg)) { + for (PyNamedParameter parameter : mapping.getParametersMappedToVariadicPositionalArguments()) { + lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); + highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + } + } + else if (PyCallExpressionHelper.isVariadicKeywordArgument(arg)) { + for (PyNamedParameter parameter : mapping.getParametersMappedToVariadicKeywordArguments()) { + lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); + highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + } + } + else { + PyTupleParameter tupleParameter = mappedTupleParameters.get(arg); + if (tupleParameter != null) { + for (PyNamedParameter parameter : getFlattenedTupleParameterComponents(tupleParameter)) { + lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); + highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + } + } + } } - } - else if (PyCallExpressionHelper.isVariadicKeywordArgument(arg)) { - for (PyNamedParameter parameter : mapping.getParametersMappedToVariadicKeywordArguments()) { - lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); - highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); + return lastParamIndex; + } + + @Nonnull + private static List getFlattenedTupleParameterComponents(@Nonnull PyTupleParameter parameter) { + List results = new ArrayList<>(); + for (PyParameter component : parameter.getContents()) { + if (component instanceof PyNamedParameter namedParam) { + results.add(namedParam); + } + else if (component instanceof PyTupleParameter tupleParam) { + results.addAll(getFlattenedTupleParameterComponents(tupleParam)); + } } - } - else { - PyTupleParameter tupleParameter = mappedTupleParameters.get(arg); - if (tupleParameter != null) { - for (PyNamedParameter parameter : getFlattenedTupleParameterComponents(tupleParameter)) { - lastParamIndex = Math.max(lastParamIndex, parameterList.indexOf(parameter)); - highlightParameter(parameter, parameterToIndex, hintFlags, mustHighlight); - } + return results; + } + + private static void highlightParameter( + @Nonnull PyNamedParameter parameter, + @Nonnull Map parameterToIndex, + @Nonnull Map> hintFlags, + boolean mustHighlight + ) { + Integer parameterIndex = parameterToIndex.get(parameter); + if (mustHighlight && parameterIndex != null && parameterIndex < hintFlags.size()) { + hintFlags.get(parameterIndex).add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); } - } } - return lastParamIndex; - } - - @Nonnull - private static List getFlattenedTupleParameterComponents(@Nonnull PyTupleParameter parameter) { - List results = new ArrayList<>(); - for (PyParameter component : parameter.getContents()) { - if (component instanceof PyNamedParameter) { - results.add((PyNamedParameter)component); - } - else if (component instanceof PyTupleParameter) { - results.addAll(getFlattenedTupleParameterComponents((PyTupleParameter)component)); - } + + /** + * builds the textual picture and the list of named parameters + * + * @param parameters parameters of a callable + * @param namedParameters used to collect all named parameters of callable + * @param parameterToIndex used to collect info about parameter indexes + * @param hintFlags mark parameter as deprecated/highlighted/strikeout + */ + private static List buildParameterListHint( + @Nonnull List parameters, + @Nonnull final List namedParameters, + @Nonnull final Map parameterToIndex, + @Nonnull final Map> hintFlags + ) { + final List hintsList = new ArrayList<>(); + ParamHelper.walkDownParamArray(parameters.toArray(new PyParameter[parameters.size()]), new ParamHelper.ParamWalker() { + @Override + public void enterTupleParameter(PyTupleParameter param, boolean first, boolean last) { + hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); + hintsList.add("("); + } + + @Override + public void leaveTupleParameter(PyTupleParameter param, boolean first, boolean last) { + hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); + hintsList.add(last ? ")" : "), "); + } + + @Override + public void visitNamedParameter(PyNamedParameter param, boolean first, boolean last) { + namedParameters.add(param); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(param.getRepr(true)); + if (!last) { + stringBuilder.append(", "); + } + int hintIndex = hintsList.size(); + parameterToIndex.put(param, hintIndex); + hintFlags.put(hintIndex, EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); + hintsList.add(stringBuilder.toString()); + } + + @Override + public void visitSingleStarParameter(PySingleStarParameter param, boolean first, boolean last) { + hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); + hintsList.add(last ? "*" : "*, "); + } + }); + return hintsList; } - return results; - } - - private static void highlightParameter(@Nonnull PyNamedParameter parameter, - @Nonnull Map parameterToIndex, - @Nonnull Map> hintFlags, - boolean mustHighlight) { - Integer parameterIndex = parameterToIndex.get(parameter); - if (mustHighlight && parameterIndex != null && parameterIndex < hintFlags.size()) { - hintFlags.get(parameterIndex).add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); + + @Nonnull + @Override + public Language getLanguage() { + return PythonLanguage.INSTANCE; } - } - - /** - * builds the textual picture and the list of named parameters - * - * @param parameters parameters of a callable - * @param namedParameters used to collect all named parameters of callable - * @param parameterToIndex used to collect info about parameter indexes - * @param hintFlags mark parameter as deprecated/highlighted/strikeout - */ - private static List buildParameterListHint(@Nonnull List parameters, - @Nonnull final List namedParameters, - @Nonnull final Map parameterToIndex, - @Nonnull final Map> hintFlags) { - final List hintsList = new ArrayList<>(); - ParamHelper.walkDownParamArray(parameters.toArray(new PyParameter[parameters.size()]), new ParamHelper.ParamWalker() { - @Override - public void enterTupleParameter(PyTupleParameter param, boolean first, boolean last) { - hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); - hintsList.add("("); - } - - @Override - public void leaveTupleParameter(PyTupleParameter param, boolean first, boolean last) { - hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); - hintsList.add(last ? ")" : "), "); - } - - @Override - public void visitNamedParameter(PyNamedParameter param, boolean first, boolean last) { - namedParameters.add(param); - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(param.getRepr(true)); - if (!last) { - stringBuilder.append(", "); - } - int hintIndex = hintsList.size(); - parameterToIndex.put(param, hintIndex); - hintFlags.put(hintIndex, EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); - hintsList.add(stringBuilder.toString()); - } - - @Override - public void visitSingleStarParameter(PySingleStarParameter param, boolean first, boolean last) { - hintFlags.put(hintsList.size(), EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class)); - hintsList.add(last ? "*" : "*, "); - } - }); - return hintsList; - } - - @Nonnull - @Override - public Language getLanguage() { - return PythonLanguage.INSTANCE; - } } diff --git a/python-impl/src/main/java/com/jetbrains/python/impl/documentation/DocumentationBuilderKit.java b/python-impl/src/main/java/com/jetbrains/python/impl/documentation/DocumentationBuilderKit.java index ce328d0a..d8848507 100644 --- a/python-impl/src/main/java/com/jetbrains/python/impl/documentation/DocumentationBuilderKit.java +++ b/python-impl/src/main/java/com/jetbrains/python/impl/documentation/DocumentationBuilderKit.java @@ -16,99 +16,90 @@ package com.jetbrains.python.impl.documentation; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -import org.jetbrains.annotations.NonNls; -import consulo.language.editor.documentation.DocumentationManagerProtocol; -import consulo.util.lang.xml.XmlStringUtil; -import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.impl.psi.PyUtil; import com.jetbrains.python.impl.toolbox.ChainIterable; import com.jetbrains.python.impl.toolbox.FP; +import com.jetbrains.python.psi.PyExpression; +import consulo.language.editor.documentation.DocumentationManagerProtocol; +import consulo.util.lang.StringUtil; +import consulo.util.lang.xml.XmlStringUtil; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; public class DocumentationBuilderKit { - static final TagWrapper TagBold = new TagWrapper("b"); - static final TagWrapper TagItalic = new TagWrapper("i"); - static final TagWrapper TagSmall = new TagWrapper("small"); - static final TagWrapper TagCode = new TagWrapper("code"); - - static final FP.Lambda1 LCombUp = new FP.Lambda1() { - public String apply(String argname) { - return combUp(argname); - } - }; - final static @NonNls String BR = "
"; - static final FP.Lambda1 LSame1 = new FP.Lambda1() { - public String apply(String name) { - return name; - } - }; - static final FP.Lambda1, Iterable> LSame2 = new FP.Lambda1, Iterable>() { - public Iterable apply(Iterable what) { - return what; - } - }; - public static FP.Lambda1 LReadableRepr = new FP.Lambda1() { - public String apply(PyExpression arg) { - return PyUtil.getReadableRepr(arg, true); + static final TagWrapper TagBold = new TagWrapper("b"); + static final TagWrapper TagItalic = new TagWrapper("i"); + static final TagWrapper TagSmall = new TagWrapper("small"); + static final TagWrapper TagCode = new TagWrapper("code"); + + static final FP.Lambda1 LCombUp = DocumentationBuilderKit::combUp; + final static String BR = "
"; + static final FP.Lambda1 LSame1 = name -> name; + static final FP.Lambda1, Iterable> LSame2 = what -> what; + public static FP.Lambda1 LReadableRepr = arg -> PyUtil.getReadableRepr(arg, true); + + private DocumentationBuilderKit() { } - }; - - private DocumentationBuilderKit() { - } - - static ChainIterable wrapInTag(String tag, Iterable content) { - return new ChainIterable("<" + tag + ">").add(content).addItem(""); - } - - @NonNls - static String combUp(@NonNls String what) { - return XmlStringUtil.escapeString(what).replace("\n", BR).replace(" ", " "); - } - - static ChainIterable $(String... content) { - return new ChainIterable(Arrays.asList(content)); - } - - static Iterable interleave(Iterable source, T filler) { - List ret = new LinkedList(); - boolean is_next = false; - for (T what : source) { - if (is_next) ret.add(filler); - else is_next = true; - ret.add(what); + + static ChainIterable wrapInTag(String tag, Iterable content) { + return new ChainIterable<>("<" + tag + ">").add(content).addItem(""); } - return ret; - } - // make a first-order curried objects out of wrapInTag() - static class TagWrapper implements FP.Lambda1, Iterable> { - private final String myTag; + private static final String[] COMB_UP_REPLACE_FROM = {"\n", " "}; + private static final String[] COMB_UP_REPLACE_TO = {BR, " "}; + + static String combUp(String what) { + return StringUtil.replace(XmlStringUtil.escapeText(what), COMB_UP_REPLACE_FROM, COMB_UP_REPLACE_TO); + } - TagWrapper(String tag) { - myTag = tag; + static ChainIterable $(String... content) { + return new ChainIterable<>(Arrays.asList(content)); } - public Iterable apply(Iterable contents) { - return wrapInTag(myTag, contents); + static Iterable interleave(Iterable source, T filler) { + List ret = new LinkedList<>(); + boolean is_next = false; + for (T what : source) { + if (is_next) { + ret.add(filler); + } + else { + is_next = true; + } + ret.add(what); + } + return ret; } - } + // make a first-order curried objects out of wrapInTag() + static class TagWrapper implements FP.Lambda1, Iterable> { + private final String myTag; - static class LinkWrapper implements FP.Lambda1, Iterable> { - private String myLink; + TagWrapper(String tag) { + myTag = tag; + } - LinkWrapper(String link) { - myLink = link; + @Override + public Iterable apply(Iterable contents) { + return wrapInTag(myTag, contents); + } } - public Iterable apply(Iterable contents) { - return new ChainIterable() - .addItem("") - .add(contents).addItem("") - ; + static class LinkWrapper implements FP.Lambda1, Iterable> { + private String myLink; + + LinkWrapper(String link) { + myLink = link; + } + + @Override + public Iterable apply(Iterable contents) { + return new ChainIterable() + .addItem("") + .add(contents) + .addItem(""); + } } - } } diff --git a/python-impl/src/main/java/com/jetbrains/python/impl/documentation/docstrings/EpydocString.java b/python-impl/src/main/java/com/jetbrains/python/impl/documentation/docstrings/EpydocString.java index 4b8d375e..13b20501 100644 --- a/python-impl/src/main/java/com/jetbrains/python/impl/documentation/docstrings/EpydocString.java +++ b/python-impl/src/main/java/com/jetbrains/python/impl/documentation/docstrings/EpydocString.java @@ -15,15 +15,15 @@ */ package com.jetbrains.python.impl.documentation.docstrings; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - +import com.jetbrains.python.toolbox.Substring; import consulo.util.lang.xml.XmlStringUtil; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import consulo.util.lang.StringUtil; -import com.jetbrains.python.toolbox.Substring; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; /** * @author yole @@ -254,7 +254,7 @@ public String result() { private static class HTMLConverter extends MarkupConverter { @Override protected void appendText(String text) { - myResult.append(joinLines(XmlStringUtil.escapeString(text, false, true), true)); + myResult.append(joinLines(XmlStringUtil.escapeText(text), true)); } @Override @@ -274,7 +274,7 @@ protected void appendMarkup(char markupChar, String markupContent) { appendTagPair(markupContent, "code"); break; default: - myResult.append(StringUtil.escapeXml(markupContent)); + XmlStringUtil.escapeText(markupContent, myResult); break; } } @@ -285,21 +285,25 @@ private void appendTagPair(String markupContent, String tagName) { myResult.append(""); } + private static final Pattern URL_HAS_PROTOCOL = Pattern.compile("^[a-z]+:"); + private void appendLink(String markupContent) { - String linkText = StringUtil.escapeXml(markupContent); + String linkText = markupContent; String linkUrl = linkText; int pos = markupContent.indexOf('<'); if (pos >= 0 && markupContent.endsWith(">")) { - linkText = StringUtil.escapeXml(markupContent.substring(0, pos).trim()); - linkUrl = joinLines(StringUtil.escapeXml(markupContent.substring(pos + 1, markupContent.length() - 1)), false); + linkText = markupContent.substring(0, pos).trim(); + linkUrl = joinLines(markupContent.substring(pos + 1, markupContent.length() - 1), false); } myResult.append("").append(linkText).append(""); + XmlStringUtil.escapeAttr(linkUrl, '"', myResult); + myResult.append("\">"); + XmlStringUtil.escapeText(linkText, myResult); + myResult.append(""); } - } private static int findMatchingEndBrace(String s, int bracePos) { @@ -319,6 +323,7 @@ else if (c == '}') { return -1; } + // TODO: optimize using StringBuilder private static String joinLines(String s, boolean addSpace) { while (true) { int lineBreakStart = s.indexOf('\n');