// returns true if processor is run or is going to be run after showing popup
  public static boolean chooseAmbiguousTarget(
      @NotNull Editor editor,
      int offset,
      @NotNull PsiElementProcessor<PsiElement> processor,
      @NotNull String titlePattern,
      @Nullable PsiElement[] elements) {
    if (TargetElementUtil.inVirtualSpace(editor, offset)) {
      return false;
    }

    final PsiReference reference = TargetElementUtil.findReference(editor, offset);

    if (elements == null || elements.length == 0) {
      elements =
          reference == null
              ? PsiElement.EMPTY_ARRAY
              : PsiUtilCore.toPsiElementArray(
                  underModalProgress(
                      reference.getElement().getProject(),
                      "Resolving Reference...",
                      () -> suggestCandidates(reference)));
    }

    if (elements.length == 1) {
      PsiElement element = elements[0];
      LOG.assertTrue(element != null);
      processor.execute(element);
      return true;
    }
    if (elements.length > 1) {
      String title;

      if (reference == null) {
        title = titlePattern;
      } else {
        final TextRange range = reference.getRangeInElement();
        final String elementText = reference.getElement().getText();
        LOG.assertTrue(
            range.getStartOffset() >= 0 && range.getEndOffset() <= elementText.length(),
            Arrays.toString(elements) + ";" + reference);
        final String refText = range.substring(elementText);
        title = MessageFormat.format(titlePattern, refText);
      }

      NavigationUtil.getPsiElementPopup(
              elements, new DefaultPsiElementCellRenderer(), title, processor)
          .showInBestPositionFor(editor);
      return true;
    }
    return false;
  }
  private static boolean processTagsInNamespaceInner(
      @NotNull final XmlTag rootTag,
      final String[] tagNames,
      final PsiElementProcessor<XmlTag> processor,
      Set<XmlTag> visitedTags) {
    if (visitedTags == null) visitedTags = new HashSet<XmlTag>(3);
    else if (visitedTags.contains(rootTag)) return true;

    visitedTags.add(rootTag);
    XmlTag[] tags = rootTag.getSubTags();

    NextTag:
    for (XmlTag tag : tags) {
      for (String tagName : tagNames) {
        if (equalsToSchemaName(tag, tagName)) {
          final String name = tag.getAttributeValue("name");

          if (name != null) {
            if (!processor.execute(tag)) {
              return false;
            }
          }

          continue NextTag;
        }
      }

      if (equalsToSchemaName(tag, INCLUDE_TAG_NAME)) {
        final String schemaLocation = tag.getAttributeValue("schemaLocation");

        if (schemaLocation != null) {
          final XmlFile xmlFile =
              XmlUtil.findNamespaceByLocation(rootTag.getContainingFile(), schemaLocation);

          if (xmlFile != null) {
            final XmlDocument includedDocument = xmlFile.getDocument();

            if (includedDocument != null) {
              if (!processTagsInNamespaceInner(
                  includedDocument.getRootTag(), tagNames, processor, visitedTags)) return false;
            }
          }
        }
      }
    }

    return true;
  }
  @NotNull
  public static <T extends PsiElement> JBPopup getPsiElementPopup(
      @NotNull T[] elements,
      @NotNull final PsiElementListCellRenderer<T> renderer,
      @Nullable final String title,
      @NotNull final PsiElementProcessor<T> processor,
      @Nullable final T selection) {
    final JList list =
        new JBListWithHintProvider(elements) {
          @Nullable
          @Override
          protected PsiElement getPsiElementForHint(Object selectedValue) {
            return (PsiElement) selectedValue;
          }
        };
    list.setCellRenderer(renderer);

    list.setFont(EditorUtil.getEditorFont());

    if (selection != null) {
      list.setSelectedValue(selection, true);
    }

    final Runnable runnable =
        () -> {
          int[] ids = list.getSelectedIndices();
          if (ids == null || ids.length == 0) return;
          for (Object element : list.getSelectedValues()) {
            if (element != null) {
              processor.execute((T) element);
            }
          }
        };

    PopupChooserBuilder builder = new PopupChooserBuilder(list);
    if (title != null) {
      builder.setTitle(title);
    }
    renderer.installSpeedSearch(builder, true);

    JBPopup popup = builder.setItemChoosenCallback(runnable).createPopup();

    builder.getScrollPane().setBorder(null);
    builder.getScrollPane().setViewportBorder(null);

    return popup;
  }
  @Override
  public Object[] getFileReferenceCompletionVariants(final FileReference reference) {
    final String s = reference.getText();
    if (s != null && s.equals("/")) {
      return ArrayUtil.EMPTY_OBJECT_ARRAY;
    }

    final CommonProcessors.CollectUniquesProcessor<PsiFileSystemItem> collector =
        new CommonProcessors.CollectUniquesProcessor<PsiFileSystemItem>();
    final PsiElementProcessor<PsiFileSystemItem> processor =
        new PsiElementProcessor<PsiFileSystemItem>() {
          @Override
          public boolean execute(@NotNull PsiFileSystemItem fileSystemItem) {
            return new FilteringProcessor<PsiFileSystemItem>(
                    reference.getFileReferenceSet().getReferenceCompletionFilter(), collector)
                .process(FileReference.getOriginalFile(fileSystemItem));
          }
        };

    List<Object> additionalItems = ContainerUtil.newArrayList();
    for (PsiFileSystemItem context : reference.getContexts()) {
      for (final PsiElement child : context.getChildren()) {
        if (child instanceof PsiFileSystemItem) {
          processor.execute((PsiFileSystemItem) child);
        }
      }
      if (context instanceof FileReferenceResolver) {
        additionalItems.addAll(((FileReferenceResolver) context).getVariants(reference));
      }
    }

    final FileType[] types = reference.getFileReferenceSet().getSuitableFileTypes();
    final THashSet<PsiElement> set =
        new THashSet<PsiElement>(collector.getResults(), VARIANTS_HASHING_STRATEGY);
    final PsiElement[] candidates = PsiUtilCore.toPsiElementArray(set);

    final Object[] variants = new Object[candidates.length + additionalItems.size()];
    for (int i = 0; i < candidates.length; i++) {
      PsiElement candidate = candidates[i];
      Object item = reference.createLookupItem(candidate);
      if (item == null) {
        item = FileInfoManager.getFileLookupItem(candidate);
      }
      if (candidate instanceof PsiFile
          && item instanceof LookupElement
          && types.length > 0
          && ArrayUtil.contains(((PsiFile) candidate).getFileType(), types)) {
        item = PrioritizedLookupElement.withPriority((LookupElement) item, Double.MAX_VALUE);
      }
      variants[i] = item;
    }

    for (int i = 0; i < additionalItems.size(); i++) {
      variants[i + candidates.length] = additionalItems.get(i);
    }
    if (!reference.getFileReferenceSet().isUrlEncoded()) {
      return variants;
    }
    List<Object> encodedVariants = new ArrayList<Object>(variants.length + additionalItems.size());
    for (int i = 0; i < candidates.length; i++) {
      final PsiElement element = candidates[i];
      if (element instanceof PsiNamedElement) {
        final PsiNamedElement psiElement = (PsiNamedElement) element;
        String name = psiElement.getName();
        final String encoded = reference.encode(name, psiElement);
        if (encoded == null) continue;
        if (!encoded.equals(name)) {
          final Icon icon =
              psiElement.getIcon(Iconable.ICON_FLAG_READ_STATUS | Iconable.ICON_FLAG_VISIBILITY);
          LookupElementBuilder item =
              FileInfoManager.getFileLookupItem(candidates[i], encoded, icon);
          encodedVariants.add(item.withTailText(" (" + name + ")"));
        } else {
          encodedVariants.add(variants[i]);
        }
      }
    }
    encodedVariants.addAll(additionalItems);
    return ArrayUtil.toObjectArray(encodedVariants);
  }