summaryrefslogtreecommitdiff
path: root/plugins/tasks/tasks-core/src/com/intellij/tasks/youtrack/lang/codeinsight/YouTrackCompletionContributor.java
blob: 81326b9a1fe314b0f07e97ac4b10885618790d60 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.intellij.tasks.youtrack.lang.codeinsight;

import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.tasks.youtrack.YouTrackIntellisense;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.intellij.tasks.youtrack.YouTrackIntellisense.CompletionItem;

/**
 * @author Mikhail Golubev
 */
public class YouTrackCompletionContributor extends CompletionContributor {
  private static final Logger LOG = Logger.getInstance(YouTrackCompletionContributor.class);
  private static final int TIMEOUT = 2000; // ms

  private static final InsertHandler<LookupElement> INSERT_HANDLER = new MyInsertHandler();

  @Override
  public void fillCompletionVariants(@NotNull final CompletionParameters parameters, @NotNull CompletionResultSet result) {
    if (LOG.isDebugEnabled()) {
      LOG.debug(DebugUtil.psiToString(parameters.getOriginalFile(), true));
    }

    super.fillCompletionVariants(parameters, result);

    PsiFile file = parameters.getOriginalFile();
    final YouTrackIntellisense intellisense = file.getUserData(YouTrackIntellisense.INTELLISENSE_KEY);
    if (intellisense == null) {
      return;
    }

    final Application application = ApplicationManager.getApplication();
    Future<List<CompletionItem>> future = application.executeOnPooledThread(new Callable<List<CompletionItem>>() {
      @Override
      public List<CompletionItem> call() throws Exception {
        return intellisense.fetchCompletion(parameters.getOriginalFile().getText(), parameters.getOffset());
      }
    });
    try {
      final List<CompletionItem> suggestions = future.get(TIMEOUT, TimeUnit.MILLISECONDS);
      // actually backed by original CompletionResultSet
      result = result.withPrefixMatcher(extractPrefix(parameters)).caseInsensitive();
      result.addAllElements(ContainerUtil.map(suggestions, new Function<CompletionItem, LookupElement>() {
        @Override
        public LookupElement fun(CompletionItem item) {
          return LookupElementBuilder.create(item, item.getOption())
            .withTypeText(item.getDescription(), true)
            .withInsertHandler(INSERT_HANDLER)
            .withBoldness(item.getStyleClass().equals("keyword"));
        }
      }));
    }
    catch (Exception ignored) {
      //noinspection InstanceofCatchParameter
      if (ignored instanceof TimeoutException) {
        LOG.debug(String.format("YouTrack request took more than %d ms to complete", TIMEOUT));
      }
      LOG.debug(ignored);
    }
  }

  /**
   * Find first word left boundary before cursor and strip leading braces and '#' signs
   */
  @NotNull
  private static String extractPrefix(CompletionParameters parameters) {
    String text = parameters.getOriginalFile().getText();
    final int caretOffset = parameters.getOffset();
    if (text.isEmpty() || caretOffset == 0) {
      return "";
    }
    int stopAt = text.lastIndexOf('{', caretOffset - 1);
    // caret isn't inside braces
    if (stopAt <= text.lastIndexOf('}', caretOffset - 1)) {
      // we stay right after colon
      if (text.charAt(caretOffset - 1) == ':') {
        stopAt = caretOffset - 1;
      }
      // use rightmost word boundary as last resort
      else {
        stopAt = text.lastIndexOf(' ', caretOffset - 1);
      }
    }
    //int start = CharArrayUtil.shiftForward(text, lastSpace < 0 ? 0 : lastSpace + 1, "#{ ");
    int prefixStart = stopAt + 1;
    if (prefixStart < caretOffset && text.charAt(prefixStart) == '#') {
      prefixStart++;
    }
    return StringUtil.trimLeading(text.substring(prefixStart, caretOffset));
  }


  /**
   * Inserts additional braces around values that contains spaces, colon after attribute names
   * and '#' before short-cut attributes if any
   */
  private static class MyInsertHandler implements InsertHandler<LookupElement> {
    @Override
    public void handleInsert(InsertionContext context, LookupElement item) {
      final CompletionItem completionItem = (CompletionItem)item.getObject();
      final Document document = context.getDocument();
      final Editor editor = context.getEditor();

      context.commitDocument();
      context.setAddCompletionChar(false);

      final String prefix = completionItem.getPrefix();
      final String suffix = completionItem.getSuffix();
      String text = document.getText();
      int offset = context.getStartOffset();
      // skip possible spaces after '{', e.g. "{  My Project <caret>"
      if (prefix.endsWith("{")) {
        while (offset > prefix.length() && Character.isWhitespace(text.charAt(offset - 1))) {
          offset--;
        }
      }
      if (!prefix.isEmpty() && !hasPrefixAt(document.getText(), offset - prefix.length(), prefix)) {
        document.insertString(offset, prefix);
      }
      offset = context.getTailOffset();
      text = document.getText();
      if (suffix.startsWith("} ")) {
        while (offset < text.length() - suffix.length() && Character.isWhitespace(text.charAt(offset))) {
          offset++;
        }
      }
      if (!suffix.isEmpty() && !hasPrefixAt(text, offset, suffix)) {
        document.insertString(offset, suffix);
      }
      editor.getCaretModel().moveToOffset(context.getTailOffset());
    }
  }

  static boolean hasPrefixAt(String text, int offset, String prefix) {
    if (text.isEmpty() || offset < 0 || offset >= text.length()) {
      return false;
    }
    return text.regionMatches(true, offset, prefix, 0, prefix.length());
  }
}