| package com.intellij.tasks.youtrack; |
| |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.tasks.impl.TaskUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.hash.LinkedHashMap; |
| import org.jdom.Element; |
| import org.jdom.input.SAXBuilder; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.io.InputStream; |
| import java.net.URLEncoder; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*; |
| import static com.intellij.openapi.editor.HighlighterColors.BAD_CHARACTER; |
| import static com.intellij.openapi.editor.HighlighterColors.TEXT; |
| |
| /** |
| * Auxiliary class for extracting data from YouTrack intellisense responses. |
| * See http://confluence.jetbrains.com/display/YTD5/Intellisense+for+issue+search for format details. |
| * <p/> |
| * It also provides two additional classes to represent tokens highlighting and |
| * available completion items from response: {@link com.intellij.tasks.youtrack.YouTrackIntellisense.HighlightRange} |
| * and {@link com.intellij.tasks.youtrack.YouTrackIntellisense.CompletionItem}. |
| * |
| * @author Mikhail Golubev |
| */ |
| public class YouTrackIntellisense { |
| |
| /** |
| * Key used to bind YouTrackIntellisense instance to specific PsiFile |
| */ |
| public static final Key<YouTrackIntellisense> INTELLISENSE_KEY = Key.create("youtrack.intellisense"); |
| |
| private static final Logger LOG = Logger.getInstance(YouTrackIntellisense.class); |
| |
| public static final String INTELLISENSE_RESOURCE = "/rest/issue/intellisense"; |
| private static final Map<String, TextAttributes> TEXT_ATTRIBUTES = ContainerUtil.newHashMap( |
| Pair.create("field", CONSTANT.getDefaultAttributes()), |
| Pair.create("keyword", KEYWORD.getDefaultAttributes()), |
| Pair.create("string", STRING.getDefaultAttributes()), |
| Pair.create("error", BAD_CHARACTER.getDefaultAttributes()) |
| ); |
| private static final int CACHE_SIZE = 30; |
| |
| private static class SizeLimitedCache<K, V> extends LinkedHashMap<K, V> { |
| private final int myMaxSize; |
| |
| private SizeLimitedCache(int max) { |
| super((int)(max / 0.75) + 1, true); |
| myMaxSize = max; |
| } |
| |
| @Override |
| protected boolean removeEldestEntry(Map.Entry<K, V> eldest, K key, V value) { |
| return size() > myMaxSize; |
| } |
| } |
| |
| private static final Map<Pair<String, Integer>, Response> ourCache = |
| Collections.synchronizedMap(new SizeLimitedCache<Pair<String, Integer>, Response>(CACHE_SIZE)); |
| |
| @NotNull |
| private static TextAttributes getAttributeByStyleClass(@NotNull String styleClass) { |
| final TextAttributes attr = TEXT_ATTRIBUTES.get(styleClass); |
| return attr == null ? TEXT.getDefaultAttributes() : attr; |
| } |
| |
| @NotNull |
| public List<HighlightRange> fetchHighlighting(@NotNull String query, int caret) throws Exception { |
| LOG.debug("Requesting highlighting"); |
| return fetch(query, caret, true).getHighlightRanges(); |
| } |
| |
| @NotNull |
| public List<CompletionItem> fetchCompletion(@NotNull String query, int caret) throws Exception { |
| LOG.debug("Requesting completion"); |
| return fetch(query, caret, false).getCompletionItems(); |
| } |
| |
| private final YouTrackRepository myRepository; |
| |
| public YouTrackIntellisense(@NotNull YouTrackRepository repository) { |
| myRepository = repository; |
| } |
| |
| @NotNull |
| private Response fetch(@NotNull String query, int caret, boolean ignoreCaret) throws Exception { |
| LOG.debug("Query: '" + query + "' caret at: " + caret); |
| final Pair<String, Integer> lookup = Pair.create(query, caret); |
| Response response = null; |
| if (ignoreCaret) { |
| for (Pair<String, Integer> pair : ourCache.keySet()) { |
| if (pair.getFirst().equals(query)) { |
| response = ourCache.get(pair); |
| break; |
| } |
| } |
| } |
| else { |
| response = ourCache.get(lookup); |
| } |
| LOG.debug("Cache " + (response != null? "hit" : "miss")); |
| if (response == null) { |
| final String url = String.format("%s?filter=%s&caret=%d", INTELLISENSE_RESOURCE, URLEncoder.encode(query, "utf-8"), caret); |
| final long startTime = System.currentTimeMillis(); |
| response = new Response(myRepository.doREST(url, false).getResponseBodyAsStream()); |
| LOG.debug(String.format("Intellisense request to YouTrack took %d ms to complete", System.currentTimeMillis() - startTime)); |
| ourCache.put(lookup, response); |
| } |
| return response; |
| } |
| |
| public YouTrackRepository getRepository() { |
| return myRepository; |
| } |
| |
| /** |
| * Main wrapper around "IntelliSense" element in YouTrack response. It delegates further parsing |
| * to {@link com.intellij.tasks.youtrack.YouTrackIntellisense.HighlightRange} and |
| * {@link com.intellij.tasks.youtrack.YouTrackIntellisense.CompletionItem} |
| */ |
| public static class Response { |
| |
| private List<HighlightRange> myHighlightRanges; |
| private List<CompletionItem> myCompletionItems; |
| |
| public Response(@NotNull InputStream stream) throws Exception { |
| final Element root = new SAXBuilder().build(stream).getRootElement(); |
| TaskUtil.prettyFormatXmlToLog(LOG, root); |
| @NotNull final Element highlight = root.getChild("highlight"); |
| //assert highlight != null : "no '/IntelliSense/highlight' element in YouTrack response"; |
| myHighlightRanges = ContainerUtil.map(highlight.getChildren("range"), new Function<Element, HighlightRange>() { |
| @Override |
| public HighlightRange fun(Element range) { |
| return new HighlightRange(range); |
| } |
| }); |
| |
| @NotNull final Element suggest = root.getChild("suggest"); |
| //assert suggest != null : "no '/IntelliSense/suggest' element in YouTrack response"; |
| myCompletionItems = ContainerUtil.map(suggest.getChildren("item"), new Function<Element, CompletionItem>() { |
| @Override |
| public CompletionItem fun(Element item) { |
| return new CompletionItem(item); |
| } |
| }); |
| } |
| |
| @NotNull |
| public List<HighlightRange> getHighlightRanges() { |
| return myHighlightRanges; |
| } |
| |
| @NotNull |
| public List<CompletionItem> getCompletionItems() { |
| return myCompletionItems; |
| } |
| } |
| |
| /** |
| * Wrapper around content of "highlight/range" element of YouTrack response |
| */ |
| public static class HighlightRange { |
| private int myStart, myEnd; |
| private String myStyleClass; |
| |
| public HighlightRange(@NotNull Element rangeElement) { |
| //assert "range".equals(rangeElement.getName()); |
| myStart = Integer.valueOf(rangeElement.getChildText("start")); |
| myEnd = Integer.valueOf(rangeElement.getChildText("end")); |
| myStyleClass = rangeElement.getChildText("styleClass"); |
| } |
| |
| public int getStart() { |
| return myStart; |
| } |
| |
| public int getEnd() { |
| return myEnd; |
| } |
| |
| @NotNull |
| public String getStyleClass() { |
| return StringUtil.notNullize(myStyleClass); |
| } |
| |
| @NotNull |
| public TextRange getRange() { |
| return new TextRange(myStart, myEnd); |
| } |
| |
| @NotNull |
| public TextRange getTextRange() { |
| return TextRange.create(myStart, myEnd); |
| } |
| |
| @NotNull |
| public TextAttributes getTextAttributes() { |
| return getAttributeByStyleClass(myStyleClass); |
| } |
| } |
| |
| /** |
| * Wrapper around content of "suggest/item" element in YouTrack response |
| */ |
| public static class CompletionItem { |
| private TextRange myMatchRange, myCompletionRange; |
| private int myCaretPosition; |
| private String myDescription; |
| private String mySuffix; |
| private String myPrefix; |
| private String myOption; |
| private String myStyleClass; |
| |
| public CompletionItem(@NotNull Element item) { |
| //assert "item".equals(item.getName()) |
| final Element match = item.getChild("match"); |
| myMatchRange = new TextRange(Integer.parseInt(match.getAttributeValue("start")), |
| Integer.parseInt(match.getAttributeValue("end"))); |
| final Element completion = item.getChild("completion"); |
| myCompletionRange = new TextRange(Integer.parseInt(completion.getAttributeValue("start")), |
| Integer.parseInt(completion.getAttributeValue("end"))); |
| myDescription = item.getChildText("description"); |
| myOption = item.getChildText("option"); |
| mySuffix = item.getChildText("suffix"); |
| myPrefix = item.getChildText("prefix"); |
| myStyleClass = item.getChildText("styleClass"); |
| myCaretPosition = Integer.valueOf(item.getChildText("caret")); |
| } |
| |
| @NotNull |
| public TextRange getMatchRange() { |
| return myMatchRange; |
| } |
| |
| @NotNull |
| public TextRange getCompletionRange() { |
| return myCompletionRange; |
| } |
| |
| public int getCaretPosition() { |
| return myCaretPosition; |
| } |
| |
| @NotNull |
| public String getDescription() { |
| return myDescription; |
| } |
| |
| @NotNull |
| public String getSuffix() { |
| return StringUtil.notNullize(mySuffix); |
| } |
| |
| @NotNull |
| public String getPrefix() { |
| return StringUtil.notNullize(myPrefix); |
| } |
| |
| @NotNull |
| public String getOption() { |
| return myOption; |
| } |
| |
| @NotNull |
| public String getStyleClass() { |
| return StringUtil.notNullize(myStyleClass); |
| } |
| |
| @NotNull |
| TextAttributes getTextAttributes() { |
| return getAttributeByStyleClass(myStyleClass); |
| } |
| } |
| } |