From e5667565a206cacd75538b06471c14b11ad9133a Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Tue, 10 Jul 2012 20:52:09 +0400 Subject: [PATCH 01/66] IDEA-88349 reformat (of xml) --- .../intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java index c0d69b38c25c..0a2c5199d972 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java @@ -230,6 +230,7 @@ public class CodeStyleManagerImpl extends CodeStyleManager { } if (visualColumnToRestore < 0) { + editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); return; } CaretModel caretModel = editor.getCaretModel(); From d5ccdf5b6f760e71c4ffb3ec8ee0b43065e19327 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Thu, 12 Jul 2012 17:03:10 +0400 Subject: [PATCH 02/66] IDEA-66333 Quick documentation lookup on mouse hover 1. Preserve old 'mouse hover' text, just use doc links there; 2. Limit 'mouse hover' text by the header width; 3. Test data is updated; --- .../codeInsight/JavaDocumentationTest.groovy | 15 +- .../navigation/CtrlMouseHandler.java | 5 +- .../navigation/DocPreviewUtil.java | 470 ++++++++++-------- .../navigation/DocPreviewUtilTest.groovy | 23 +- .../groovy/GroovyDocumentationTest.groovy | 15 +- 5 files changed, 305 insertions(+), 223 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/JavaDocumentationTest.groovy b/java/java-tests/testSrc/com/intellij/codeInsight/JavaDocumentationTest.groovy index 18a0a5f70b3d..78cea7063755 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/JavaDocumentationTest.groovy +++ b/java/java-tests/testSrc/com/intellij/codeInsight/JavaDocumentationTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2000-2011 JetBrains s.r.o. + * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,10 @@ class Foo {{ }} ''' def ref = myFixture.file.findReferenceAt(myFixture.editor.caretModel.offset) - assert CtrlMouseHandler.getInfo(ref.resolve(), ref.element) == """Bar - java.util.List<java.lang.String> foo (java.lang.String param)""" + assertEquals ( + 'Bar
List<java.lang.String> foo (java.lang.String param)', + CtrlMouseHandler.getInfo(ref.resolve(), ref.element) + ) } public void testGenericField() { @@ -45,8 +47,9 @@ class Foo {{ }} ''' def ref = myFixture.file.findReferenceAt(myFixture.editor.caretModel.offset) - assert CtrlMouseHandler.getInfo(ref.resolve(), ref.element) == """Bar - java.lang.Integer field""" + assertEquals( + 'Bar
java.lang.Integer field', + CtrlMouseHandler.getInfo(ref.resolve(), ref.element) + ) } - } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java index 12604b88321e..ec0041d1eb6f 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java @@ -85,8 +85,7 @@ import java.util.List; public class CtrlMouseHandler extends AbstractProjectComponent { - private static final int ourQuickDocRowsNumber = getIntProperty("quick.doc.desired.rows.number", 2); - private static final int ourQuickDocSymbolsInRowNumber = getIntProperty("quick.doc.desired.symbols.in.row.number", 80); + private static final int ourQuickDocRowsNumber = getIntProperty("quick.doc.desired.rows.number", 1); private final TextAttributes ourReferenceAttributes; private HighlightersSet myHighlighter; @@ -264,7 +263,7 @@ public class CtrlMouseHandler extends AbstractProjectComponent { if (result != null) { String fullText = documentationProvider.generateDoc(element, atPointer); String qName = element instanceof PsiQualifiedNamedElement ? ((PsiQualifiedNamedElement)element).getQualifiedName() : null; - String text = DocPreviewUtil.buildPreview(result, qName, fullText, ourQuickDocRowsNumber, ourQuickDocSymbolsInRowNumber); + String text = DocPreviewUtil.buildPreview(result, qName, fullText, ourQuickDocRowsNumber); return new DocInfo(text, documentationProvider, atPointer); } return DocInfo.EMPTY; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java index c96394c3c9de..b857ab0acea1 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java @@ -18,12 +18,16 @@ package com.intellij.codeInsight.navigation; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.psi.PsiElement; import com.intellij.util.containers.Stack; +import gnu.trove.TObjectIntHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Provides utility methods for building documentation preview. @@ -35,15 +39,26 @@ import java.util.Set; */ public class DocPreviewUtil { - private static final Set TAGS_TO_ADD_LF = new HashSet(Arrays.asList("p", "blockquote", "pre")); - private static final Set TAGS_TO_IGNORE = new HashSet(Arrays.asList("style", "b", "small")); + private static final Set TAGS_TO_ADD_LF = new HashSet(); + private static final Set TAGS_TO_IGNORE = new HashSet(); + static { + for (String tag : new String[] {"p", "blockquote", "pre"}) { + TAGS_TO_ADD_LF.add(tag); + TAGS_TO_ADD_LF.add(tag.toUpperCase()); + } + + for (String tag : new String[] {"style", "b", "small"}) { + TAGS_TO_IGNORE.add(tag); + TAGS_TO_IGNORE.add(tag.toUpperCase()); + } + } private DocPreviewUtil() { } /** - * Allows to build a documentation preview from the given arguments. Basically, takes given 'full documentation', wraps it according - * to the given 'desired rows and columns per-row' arguments and returns a result. + * Allows to build a documentation preview from the given arguments. Basically, takes given 'full documentation', cuts it according + * to the given 'desired rows' argument and returns a result. * * @param header target documentation header. Is expected to be a result of the * {@link DocumentationProvider#getQuickNavigateInfo(PsiElement, PsiElement)} call @@ -52,16 +67,10 @@ public class DocPreviewUtil { * element with the given qualified name is added to the preview's end if the qName is provided then * @param fullText full documentation text (if available) * @param desiredRowsNumber maximum number of rows to use at the preview's body ('header' text is not count here) - * @param desiredSymbolsInRowNumber desired max number of columns per row * @return preview text to use for the given arguments */ @NotNull - public static String buildPreview(@NotNull String header, - @Nullable String qName, - @Nullable String fullText, - int desiredRowsNumber, - int desiredSymbolsInRowNumber) - { + public static String buildPreview(@NotNull String header, @Nullable String qName, @Nullable String fullText, int desiredRowsNumber) { if (fullText == null) { return header; } @@ -76,237 +85,283 @@ public class DocPreviewUtil { if (bodyEnd < 0) { return header; } - - String body = fullText.substring(bodyStart, bodyEnd); + + int headerEnd = fullText.indexOf(""); + if (headerEnd < 0) { + return header; + } + + String headerWithLinks = fullText.substring(bodyStart, headerEnd); + String docText = fullText.substring(headerEnd + "".length(), bodyEnd); // The algorithm is: - // 1. Process full text body as follows: - // 1.1. Count non-markup symbols until desired row symbols number is exceeded; - // 1.2. Insert
after that to start a new row; - // 1.3. Stop processing as soon as the desired rows number is reached or the text is finished; - // 2. Add closing tags for all non-matched open tags; + // 1. Get given header text and replace meaningful symbols with the links to those symbols; + // 2. Calculate max symbols per-row to use for the result as a number of non-markup symbols at the longest header line; + // 3. Process full text body as follows: + // 2.1. Count non-markup symbols until desired row symbols number is exceeded; + // 2.2. Insert
after that to start a new row (if max rows number is not reached); + // 3.3. Stop processing as soon as the desired rows number is reached or the text is finished; + // 4. Add a link to the full documentation if it's not placed inside the resulted text; + // 5. Add closing tags for all non-matched open tags; - final Context context = new Context(desiredRowsNumber, desiredSymbolsInRowNumber); - int startParseOffset = 0; - - //region 1. Prepare header to use + final Context context = new Context(desiredRowsNumber); - // Include information about the library/module location. - int bracket = header.indexOf(']'); - int lf = header.indexOf('\n'); - if (bracket > 0 && (lf < 0 || bracket < lf)) { - context.buffer.append(header.substring(0, bracket + 1)).append(" "); - } - - // Include information that is available at the given header (it's not count to the given rows/columns arguments). - startParseOffset = process(body, startParseOffset, body.length(), getHeaderParser(context, header)); - - //endregion - - //region Parse body - startParseOffset = process(body, startParseOffset, body.length(), getBodyParser(context)); - //endregion - - if (qName != null && startParseOffset < body.length()) { - context.buffer.append(String.format("<more>", qName)); - } + int columnsPerRow = parseHeader(header, headerWithLinks, context); + process(docText, new BodyCallback(context, qName, columnsPerRow)); //region Add closing tags while (!context.openTags.isEmpty()) { context.buffer.append("'); } //endregion - + return context.buffer.toString(); } + + private static int parseHeader(@NotNull String headerTemplate, @NotNull String headerWithLinks, @NotNull Context context) { + + //region Build links info. + Map links = new HashMap(); + process(headerWithLinks, new LinksCollector(links)); + //endregion + + + //region Apply links info to the header template. + String headerToUse = headerTemplate.replace("\n", "
"); + for (Map.Entry entry : links.entrySet()) { + String visibleName = entry.getKey(); + int i = visibleName.lastIndexOf('.'); + if (i > 0 && i < visibleName.length() - 1) { + visibleName = visibleName.substring(i + 1); + } + headerToUse = headerToUse.replace(entry.getKey(), String.format("%s", entry.getValue(), visibleName)); + } + context.buffer.append(headerToUse); + //endregion + + //region Update 'columns-per-row' if the header is wide. + MaxColumnCalculator calculator = new MaxColumnCalculator(); + process(headerToUse, calculator); + return calculator.maxColumn; + //endregion + } + + private enum State {TEXT, INSIDE_OPEN_TAG, INSIDE_CLOSE_TAG} @SuppressWarnings("AssignmentToForLoopParameter") - private static int process(@NotNull String text, int start, int end, @NotNull Callback callback) { + private static int process(@NotNull String text, @NotNull Callback callback) { State state = State.TEXT; - int dataStartOffset = start; - int tagNameStartOffset = start; + int dataStartOffset = 0; + int tagNameStartOffset = 0; String tagName = null; - for (; start < end; start++) { - char c = text.charAt(start); + int i = 0; + for (; i < text.length(); i++) { + char c = text.charAt(i); switch (state) { case TEXT: if (c == '<') { - if (start > dataStartOffset) { - if (!callback.onText(text.substring(dataStartOffset, start).replace(" ", " "))) { + if (i > dataStartOffset) { + if (!callback.onText(text.substring(dataStartOffset, i).replace(" ", " "))) { return dataStartOffset; } } - dataStartOffset = start; - if (start < text.length() - 1 && text.charAt(start + 1) == '/') { + dataStartOffset = i; + if (i < text.length() - 1 && text.charAt(i + 1) == '/') { state = State.INSIDE_CLOSE_TAG; - tagNameStartOffset = ++start + 1; + tagNameStartOffset = ++i + 1; } else { state = State.INSIDE_OPEN_TAG; - tagNameStartOffset = start + 1; + tagNameStartOffset = i + 1; } } break; case INSIDE_OPEN_TAG: if (c == ' ') { - tagName = text.substring(tagNameStartOffset, start); + tagName = text.substring(tagNameStartOffset, i); } else if (c == '/') { - if (start < text.length() - 1 && text.charAt(start + 1) == '>') { + if (i < text.length() - 1 && text.charAt(i + 1) == '>') { if (tagName == null) { - tagName = text.substring(tagNameStartOffset, start); + tagName = text.substring(tagNameStartOffset, i); } - if (!callback.onStandaloneTag(tagName, text.substring(dataStartOffset, start + 2))) { + if (!callback.onStandaloneTag(tagName, text.substring(dataStartOffset, i + 2))) { return dataStartOffset; } tagName = null; state = State.TEXT; - dataStartOffset = ++start + 1; + dataStartOffset = ++i + 1; break; } } else if (c == '>') { if (tagName == null) { - tagName = text.substring(tagNameStartOffset, start); + tagName = text.substring(tagNameStartOffset, i); } - if (!callback.onOpenTag(tagName, text.substring(dataStartOffset, start + 1))) { + if (!callback.onOpenTag(tagName, text.substring(dataStartOffset, i + 1))) { return dataStartOffset; } tagName = null; state = State.TEXT; - dataStartOffset = start + 1; + dataStartOffset = i + 1; } break; case INSIDE_CLOSE_TAG: if (c == '>') { if (tagName == null) { - tagName = text.substring(tagNameStartOffset, start); + tagName = text.substring(tagNameStartOffset, i); } - if (!callback.onCloseTag(tagName, text.substring(dataStartOffset, start + 1))) { + if (!callback.onCloseTag(tagName, text.substring(dataStartOffset, i + 1))) { return dataStartOffset; } tagName = null; state = State.TEXT; - dataStartOffset = start + 1; + dataStartOffset = i + 1; } } } - return start; - } - - @NotNull - private static Callback getHeaderParser(@NotNull Context context, @NotNull final String header) { - return new AbstractCallback(context, false) { - - private boolean myStop; - - @Override - public boolean onOpenTag(@NotNull String name, @NotNull String text) { - return !myStop && super.onOpenTag(name, text); - } - @Override - public boolean onText(@NotNull String text) { - boolean addLf = false; - for (String s : text.split("\n")) { - if (addLf) { - newLine(); - } - else { - addLf = true; - } - - if (s.length() <= 0) { - continue; - } - - if (!header.contains(s) && s.startsWith("java.lang.")) { - s = s.substring("java.lang.".length()); - } - - if (myStop || !header.contains(s)) { - return false; - } - - if (header.endsWith(s)) { - myStop = true; - } - - addText(s); - } - if (text.endsWith("\n")) { - newLine(); - } - return true; - } - - @Override - protected boolean canBreakBeforeText(@NotNull String text) { - // Don't allow line break before the closing type parameter bracket. - return !text.startsWith(">") && !text.startsWith(","); - } - }; - } - - @NotNull - private static Callback getBodyParser(@NotNull final Context context) { - return new AbstractCallback(context, true) { - - @Override - public boolean onText(@NotNull String text) { - return addText(text); - } - }; - } - - private static class Context { + if (dataStartOffset < text.length()) { + callback.onText(text.substring(dataStartOffset, text.length()).replace(" ", " ")); + } + return i; + } + + private static class Context { + @NotNull public final Stack openTags = new Stack(); @NotNull public final StringBuilder buffer = new StringBuilder(); public final int rows; - public final int columnsPerRow; - public int currentRow; - public int currentColumn; - public Context(int rows, int columnsPerRow) { + public Context(int rows) { this.rows = rows; - this.columnsPerRow = columnsPerRow; } } private interface Callback { boolean onOpenTag(@NotNull String name, @NotNull String text); - boolean onCloseTag(@NotNull String name, @NotNull String text); - boolean onStandaloneTag(@NotNull String name, @NotNull String text); - boolean onText(@NotNull String text); } - - private static abstract class AbstractCallback implements Callback { - @NotNull protected final Context myContext; - private final boolean myCountRows; - private boolean myScheduleNewLine; - private boolean myInsidePre; + private static class LinksCollector implements Callback { - protected AbstractCallback(@NotNull Context context, boolean countRows) { - myContext = context; - myCountRows = countRows; + private static final Pattern HREF_PATTERN = Pattern.compile("href=[\"']([^\"']+)"); + + @NotNull private final Map myLinks; + private String myHref; + + LinksCollector(@NotNull Map links) { + myLinks = links; } @Override + public boolean onOpenTag(@NotNull String name, @NotNull String text) { + if (!"a".equals(name)) { + return true; + } + Matcher matcher = HREF_PATTERN.matcher(text); + if (matcher.find()) { + myHref = matcher.group(1); + } + return true; + } + + @Override + public boolean onCloseTag(@NotNull String name, @NotNull String text) { + if ("a".equals(name)) { + myHref = null; + } + return true; + } + + @Override + public boolean onStandaloneTag(@NotNull String name, @NotNull String text) { + return true; + } + + @Override + public boolean onText(@NotNull String text) { + if (myHref != null) { + myLinks.put(text, myHref); + myHref = null; + } + return true; + } + } + + private static class MaxColumnCalculator implements Callback { + + private static final TObjectIntHashMap SUBSTITUTIONS = new TObjectIntHashMap(); + static { + SUBSTITUTIONS.put("<", 1); + SUBSTITUTIONS.put(">", 1); + SUBSTITUTIONS.put(" ", 1); + } + + public int maxColumn; + private int myCurrentColumn; + + @Override + public boolean onOpenTag(@NotNull String name, @NotNull String text) { + if ("br".equals(name)) { + myCurrentColumn = 0; + } + return true; + } + + @Override + public boolean onCloseTag(@NotNull String name, @NotNull String text) { + return true; + } + + @Override + public boolean onStandaloneTag(@NotNull String name, @NotNull String text) { + return onOpenTag(name, text); + } + + @Override + public boolean onText(@NotNull String text) { + int length = text.length(); + if (SUBSTITUTIONS.containsKey(text)) { + length = SUBSTITUTIONS.get(text); + } + myCurrentColumn += length; + maxColumn = Math.max(maxColumn, myCurrentColumn); + return true; + } + } + + private static class BodyCallback implements Callback { + + @NotNull protected final Context myContext; + private boolean myScheduleNewLine; + private boolean myInsidePre; + private boolean myDocAdded; + private int myCurrentRow; + private int myCurrentColumn; + @Nullable private String myQName; + private int myColumnsPerRow; + + + protected BodyCallback(@NotNull Context context, @Nullable String qName, final int columnsPerRow) { + myContext = context; + myQName = qName; + myColumnsPerRow = columnsPerRow; + } + public boolean onOpenTag(@NotNull String name, @NotNull String text) { if ("pre".equals(name)) { myInsidePre = true; } if (!processDelayedLfTag(name)) { - return myContext.currentRow < myContext.rows; + return myCurrentRow < myContext.rows; } if (!TAGS_TO_IGNORE.contains(name)) { - myContext.buffer.append(text); + addText(text, false); myContext.openTags.push(name); } return true; @@ -324,120 +379,123 @@ public class DocPreviewUtil { return false; } - @Override public boolean onCloseTag(@NotNull String name, @NotNull String text) { if ("pre".equals(name)) { myInsidePre = false; } if (!processDelayedLfTag(name)) { - return myContext.currentRow < myContext.rows; + return myCurrentRow < myContext.rows; } - + if (!TAGS_TO_IGNORE.contains(name)) { - myContext.buffer.append(text); + addText(text, false); myContext.openTags.remove(name); } return true; } - @Override public boolean onStandaloneTag(@NotNull String name, @NotNull String text) { if (!processDelayedLfTag(name)) { return true; } if (!TAGS_TO_IGNORE.contains(name)) { - myContext.buffer.append(text); + addText(text, false); } return true; } - @Override public boolean onText(@NotNull String text) { - myContext.buffer.append(text); - return true; - } - - protected boolean canBreakBeforeText(@NotNull String text) { - return true; - } - - protected boolean addText(@NotNull String text) { boolean addSpace = false; - if (!text.isEmpty() && (text.startsWith(" ") || text.startsWith("\t"))) { - myContext.buffer.append(text.charAt(0)); - myContext.currentColumn++; + if (!text.isEmpty() && (text.startsWith(" ") || text.startsWith("\t")) && !addText(String.valueOf(text.charAt(0)), true)) { + return false; } - String tailText = (!text.isEmpty() && (text.endsWith(" ") || text.endsWith("\t"))) ? text.substring(text.length() - 1) : null; + String tailText = (!text.isEmpty() && (text.endsWith(" ") || text.endsWith("\t"))) ? text.substring(text.length() - 1) : null; text = text.trim(); if (myInsidePre && text.contains("\n")) { boolean addLf = false; for (String s : text.split("\n")) { if (addLf) { - newLine(); - if (myContext.currentRow >= myContext.rows) { + if (!newLine()) { return false; } } else { addLf = true; } - addText(s); - if (myContext.currentRow >= myContext.rows) { - return false; - } + if (!onText(s)) return false; } - return myContext.currentRow < myContext.rows; + return true; } - + for (String s : text.split(" ")) { s = s.trim(); if (s.length() <= 0) { continue; } - + if (myScheduleNewLine && canBreakBeforeText(s)) { - newLine(); + if (!newLine()) return false; addSpace = false; - if (myContext.currentRow >= myContext.rows) { - return false; - } } - + if (addSpace) { - myContext.buffer.append(" "); - myContext.currentColumn++; + if (!addText(" ", true)) return false; } else { addSpace = true; } - - myContext.currentColumn += s.length(); - myContext.buffer.append(s); - if (myContext.currentColumn < myContext.columnsPerRow) { - continue; + + if (!addText(s, true)) return false; + } + return !(tailText != null && !addText(tailText, true)); + } + + private static boolean canBreakBeforeText(@NotNull String text) { + return !text.startsWith(">") && !text.startsWith(","); + } + + private boolean newLine() { + if (onLastRow()) { + if (myQName != null) { + myContext.buffer.append(String.format(" ...", myQName)); } - myScheduleNewLine = true; - } - if (tailText != null) { - myContext.buffer.append(tailText); - myContext.currentColumn += tailText.length(); + return false; } + addText("
", false); + myCurrentColumn = 0; + myScheduleNewLine = false; + myCurrentRow++; return true; } - protected void newLine() { - myContext.buffer.append("
"); - myContext.currentColumn = 0; - myScheduleNewLine = false; - if (myCountRows) { - myContext.currentRow++; + private boolean addText(@NotNull String text, boolean countColumns) { + if (!myDocAdded) { + myContext.buffer.append("
"); + myDocAdded = true; } + + if (!countColumns) { + myContext.buffer.append(text); + return true; + } + + int remainingColumns = myColumnsPerRow - myCurrentColumn; + if (onLastRow()) { + remainingColumns -= " ...".length(); + } + if (remainingColumns < text.length() && !newLine()) return false; + + myContext.buffer.append(text); + myCurrentColumn += text.length(); + return true; + } + + private boolean onLastRow() { + return myCurrentRow >= myContext.rows - 1; } } - - private enum State {TEXT, INSIDE_OPEN_TAG, INSIDE_CLOSE_TAG} } diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy index 75c5cb036774..a55b3eeaa8cf 100644 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy @@ -19,6 +19,7 @@ package com.intellij.codeInsight.navigation; import org.junit.Test import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue /** * @author Denis Zhdanov @@ -103,10 +104,28 @@ implements java.io.Serializab ''' def expected = '''\ -[< 1.7 >] java.lang
public final class java.lang.String
extends
Object
implements java.io.Serializable, java.lang.Comparable<java.lang.String
>, java.lang.CharSequence

The String class represents character strings. All string literals
in Java programs, such as "abc", are implemented as instances
<more>\ +java.lang
public final class String extends Object
implements Serializable, Comparable<String>, CharSequence
The String class represents character strings. All string
literals in Java programs, such as "abc", are implemented as
instances of this class.
Strings are constant; their values cannot be changed after
they are created. String buffers support mutable strings.
Because String objects are immutable they can be shared. For
example:
String str = "abc";
is equivalent to:
char data[] = {'a', 'b', 'c'}; ...\ ''' - def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText, 2, 60) + def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText, 10) + assertTrue(actual.endsWith(expected)) // Can't check for equals() because jdk name might differ on different machines. + } + + @Test + void fieldTypeSubstitution() { + def header = '''\ +Bar + java.util.List<java.lang.String> foo (java.lang.String param)\ +''' + + def fullText = '''\ + Bar
java.util.List<T> foo(T param)
\ +''' + + def expected = '''\ +Bar
List<java.lang.String> foo (java.lang.String param)\ +''' + def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText, 2) assertEquals(expected, actual) } } diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.groovy index 894f6df4e4e2..6de9b754e018 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/GroovyDocumentationTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2000-2011 JetBrains s.r.o. + * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,10 @@ class Bar { java.util.List foo(T param); } new Bar().foo(); ''' def ref = myFixture.file.findReferenceAt(myFixture.editor.caretModel.offset) - assert CtrlMouseHandler.getInfo(ref.resolve(), ref.element) == """Bar -java.util.List<java.lang.String> foo (java.lang.String param)""" + assertEquals( + """Bar
java.util.List<java.lang.String> foo (java.lang.String param)""", + CtrlMouseHandler.getInfo(ref.resolve(), ref.element) + ) } public void testGenericField() { @@ -39,8 +41,9 @@ class Bar { T field; } new Bar().field ''' def ref = myFixture.file.findReferenceAt(myFixture.editor.caretModel.offset) - assert CtrlMouseHandler.getInfo(ref.resolve(), ref.element) == """Bar -java.lang.Integer getField ()""" + assertEquals( + """Bar
java.lang.Integer getField ()""", + CtrlMouseHandler.getInfo(ref.resolve(), ref.element) + ) } - } From 81b3f9e23d3ee1705ce4dcfc3abaa205101ed0ed Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Thu, 12 Jul 2012 19:36:19 +0400 Subject: [PATCH 03/66] IDEA-66333 Quick documentation lookup on mouse hover 1. Use link for the active element; 2. Correct header width calculation; --- .../navigation/DocPreviewUtil.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java index b857ab0acea1..390a960ccbc8 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java @@ -15,10 +15,10 @@ */ package com.intellij.codeInsight.navigation; +import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.psi.PsiElement; import com.intellij.util.containers.Stack; -import gnu.trove.TObjectIntHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -106,7 +106,7 @@ public class DocPreviewUtil { final Context context = new Context(desiredRowsNumber); - int columnsPerRow = parseHeader(header, headerWithLinks, context); + int columnsPerRow = parseHeader(header, headerWithLinks, qName, context); process(docText, new BodyCallback(context, qName, columnsPerRow)); //region Add closing tags @@ -118,11 +118,18 @@ public class DocPreviewUtil { return context.buffer.toString(); } - private static int parseHeader(@NotNull String headerTemplate, @NotNull String headerWithLinks, @NotNull Context context) { + private static int parseHeader(@NotNull String headerTemplate, + @NotNull String headerWithLinks, + @Nullable String qName, + @NotNull Context context) + { //region Build links info. Map links = new HashMap(); process(headerWithLinks, new LinksCollector(links)); + if (qName != null) { + links.put(qName, DocumentationManager.PSI_ELEMENT_PROTOCOL + qName); + } //endregion @@ -294,11 +301,11 @@ public class DocPreviewUtil { private static class MaxColumnCalculator implements Callback { - private static final TObjectIntHashMap SUBSTITUTIONS = new TObjectIntHashMap(); + private static final Map SUBSTITUTIONS = new HashMap(); static { - SUBSTITUTIONS.put("<", 1); - SUBSTITUTIONS.put(">", 1); - SUBSTITUTIONS.put(" ", 1); + SUBSTITUTIONS.put("<", "<"); + SUBSTITUTIONS.put(">", ">"); + SUBSTITUTIONS.put(" ", " "); } public int maxColumn; @@ -324,11 +331,10 @@ public class DocPreviewUtil { @Override public boolean onText(@NotNull String text) { - int length = text.length(); - if (SUBSTITUTIONS.containsKey(text)) { - length = SUBSTITUTIONS.get(text); + for (Map.Entry entry : SUBSTITUTIONS.entrySet()) { + text = text.replace(entry.getKey(), entry.getValue()); } - myCurrentColumn += length; + myCurrentColumn += text.length(); maxColumn = Math.max(maxColumn, myCurrentColumn); return true; } @@ -339,7 +345,7 @@ public class DocPreviewUtil { @NotNull protected final Context myContext; private boolean myScheduleNewLine; private boolean myInsidePre; - private boolean myDocAdded; + private boolean myDocStarted; private int myCurrentRow; private int myCurrentColumn; @Nullable private String myQName; @@ -473,9 +479,9 @@ public class DocPreviewUtil { } private boolean addText(@NotNull String text, boolean countColumns) { - if (!myDocAdded) { + if (!myDocStarted) { myContext.buffer.append("
"); - myDocAdded = true; + myDocStarted = true; } if (!countColumns) { From 5586b73117f64b8fe0889d988bcb9700decfafdf Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Fri, 13 Jul 2012 08:37:59 +0400 Subject: [PATCH 04/66] IDEA-66333 Quick documentation lookup on mouse hover Updating pinned doc content on Ctrl+mouse hover --- .../documentation/DocumentationManager.java | 4 +- .../QuickDocOnMouseOverManager.java | 2 +- .../navigation/CtrlMouseHandler.java | 53 +++++++++++++++---- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java index 2d8469c26136..512b28c3ea51 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java @@ -180,8 +180,8 @@ public class DocumentationManager extends DockablePopupManager Date: Fri, 13 Jul 2012 14:27:28 +0400 Subject: [PATCH 05/66] IDEA-66333 Quick documentation lookup on mouse hover 1. Added quick doc tooltip actions which allow to view content at the big quick doc control; 2. Removed 'auto quick doc for element under mouse' functionality; --- .../options/editor/EditorOptionsPanel.form | 37 +- .../options/editor/EditorOptionsPanel.java | 95 +---- .../documentation/DocumentationManager.java | 32 +- .../QuickDocOnMouseOverManager.java | 375 ------------------ .../QuickDocOnMouseOverStartupActivity.java | 35 -- .../navigation/CtrlMouseHandler.java | 136 ++++++- ...ickDocAtPinnedWindowFromTooltipAction.java | 38 ++ .../ShowQuickDocFromTooltipAction.java | 108 +++++ .../intellij/codeInsight/hint/HintUtil.java | 19 +- .../com/intellij/ide/IdeTooltipManager.java | 18 +- .../ex/EditorSettingsExternalizable.java | 25 -- .../src/messages/ActionsBundle.properties | 4 + .../src/META-INF/LangExtensions.xml | 3 - 13 files changed, 340 insertions(+), 585 deletions(-) delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverManager.java delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverStartupActivity.java create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocAtPinnedWindowFromTooltipAction.java create mode 100644 platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocFromTooltipAction.java diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form index 3b0391ecd073..aab91296ed1b 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form @@ -304,7 +304,7 @@ - + @@ -317,7 +317,7 @@ - + @@ -333,44 +333,15 @@ - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java index 10e79040227f..334745b88800 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java @@ -21,10 +21,8 @@ import com.intellij.application.options.OptionsApplicabilityFilter; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.codeInsight.daemon.impl.IdentifierHighlighterPass; -import com.intellij.codeInsight.documentation.QuickDocOnMouseOverManager; import com.intellij.ide.ui.UISettings; import com.intellij.openapi.application.ApplicationBundle; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; @@ -41,7 +39,6 @@ import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; @@ -49,11 +46,11 @@ import java.awt.event.ItemEvent; import java.awt.event.ItemListener; public class EditorOptionsPanel { - private JPanel myBehaviourPanel; + private JPanel myBehaviourPanel; private JCheckBox myCbHighlightBraces; private static final String STRIP_CHANGED = ApplicationBundle.message("combobox.strip.modified.lines"); - private static final String STRIP_ALL = ApplicationBundle.message("combobox.strip.all"); + private static final String STRIP_ALL = ApplicationBundle.message("combobox.strip.all"); private static final String STRIP_NONE = ApplicationBundle.message("combobox.strip.none"); private JComboBox myStripTrailingSpacesCombo; @@ -66,34 +63,32 @@ public class EditorOptionsPanel { private JCheckBox myCbHighlightScope; private JTextField myClipboardContentLimitTextField; - private JCheckBox myCbSmoothScrolling; - private JCheckBox myCbVirtualPageAtBottom; - private JCheckBox myCbEnableDnD; - private JCheckBox myCbEnableWheelFontChange; - private JCheckBox myCbHonorCamelHumpsWhenSelectingByClicking; + private JCheckBox myCbSmoothScrolling; + private JCheckBox myCbVirtualPageAtBottom; + private JCheckBox myCbEnableDnD; + private JCheckBox myCbEnableWheelFontChange; + private JCheckBox myCbHonorCamelHumpsWhenSelectingByClicking; - private JPanel myHighlightSettingsPanel; + private JPanel myHighlightSettingsPanel; private JRadioButton myRbPreferScrolling; private JRadioButton myRbPreferMovingCaret; - private JCheckBox myCbRenameLocalVariablesInplace; - private JCheckBox myCbHighlightIdentifierUnderCaret; - private JCheckBox myCbEnsureBlankLineBeforeCheckBox; - private JCheckBox myShowReformatCodeDialogCheckBox; - private JCheckBox myShowOptimizeImportsDialogCheckBox; - private JCheckBox myCbUseSoftWrapsAtEditor; - private JCheckBox myCbUseSoftWrapsAtConsole; - private JCheckBox myCbUseCustomSoftWrapIndent; - private JTextField myCustomSoftWrapIndent; - private JCheckBox myCbShowAllSoftWraps; - private JCheckBox myPreselectCheckBox; - private JCheckBox myCbShowQuickDocOnCheckBox; - private JTextField myQuickDocDelayTextField; + private JCheckBox myCbRenameLocalVariablesInplace; + private JCheckBox myCbHighlightIdentifierUnderCaret; + private JCheckBox myCbEnsureBlankLineBeforeCheckBox; + private JCheckBox myShowReformatCodeDialogCheckBox; + private JCheckBox myShowOptimizeImportsDialogCheckBox; + private JCheckBox myCbUseSoftWrapsAtEditor; + private JCheckBox myCbUseSoftWrapsAtConsole; + private JCheckBox myCbUseCustomSoftWrapIndent; + private JTextField myCustomSoftWrapIndent; + private JCheckBox myCbShowAllSoftWraps; + private JCheckBox myPreselectCheckBox; private final ErrorHighlightingPanel myErrorHighlightingPanel = new ErrorHighlightingPanel(); private final MyConfigurable myConfigurable; - public EditorOptionsPanel() { + public EditorOptionsPanel(){ if (SystemInfo.isMac) { myCbEnableWheelFontChange.setText(ApplicationBundle.message("checkbox.enable.ctrl.mousewheel.changes.font.size.macos")); } @@ -111,7 +106,6 @@ public class EditorOptionsPanel { myCbRenameLocalVariablesInplace.setVisible(OptionsApplicabilityFilter.isApplicable(OptionId.RENAME_IN_PLACE)); myConfigurable = new MyConfigurable(); - initQuickDocProcessing(); initSoftWrapsSettingsProcessing(); } @@ -162,9 +156,6 @@ public class EditorOptionsPanel { } myCbEnsureBlankLineBeforeCheckBox.setSelected(editorSettings.isEnsureNewLineAtEOF()); - myCbShowQuickDocOnCheckBox.setSelected(editorSettings.isShowQuickDocOnMouseOverElement()); - myQuickDocDelayTextField.setText(Long.toString(editorSettings.getQuickDocOnMouseOverElementDelayMillis())); - myQuickDocDelayTextField.setEnabled(editorSettings.isShowQuickDocOnMouseOverElement()); // Advanced mouse myCbEnableDnD.setSelected(editorSettings.isDndEnabled()); @@ -244,17 +235,6 @@ public class EditorOptionsPanel { editorSettings.setEnsureNewLineAtEOF(myCbEnsureBlankLineBeforeCheckBox.isSelected()); - if (myCbShowQuickDocOnCheckBox.isSelected() ^ editorSettings.isShowQuickDocOnMouseOverElement()) { - boolean enabled = myCbShowQuickDocOnCheckBox.isSelected(); - editorSettings.setShowQuickDocOnMouseOverElement(enabled); - ServiceManager.getService(QuickDocOnMouseOverManager.class).setEnabled(enabled); - } - - Long quickDocDelay = getQuickDocDelayFromGui(); - if (quickDocDelay != null) { - editorSettings.setQuickDocOnMouseOverElementDelayMillis(quickDocDelay); - } - editorSettings.setDndEnabled(myCbEnableDnD.isSelected()); editorSettings.setWheelFontChangeEnabled(myCbEnableWheelFontChange.isSelected()); @@ -288,23 +268,6 @@ public class EditorOptionsPanel { restartDaemons(); } - @Nullable - private Long getQuickDocDelayFromGui() { - String quickDocDelayAsText = myQuickDocDelayTextField.getText(); - if (StringUtil.isEmptyOrSpaces(quickDocDelayAsText)) { - return null; - } - - try { - long delay = Long.parseLong(quickDocDelayAsText); - return delay > 0 ? delay : null; - } - catch (NumberFormatException e) { - // Ignore incorrect value. - return null; - } - } - public static void restartDaemons() { Project[] projects = ProjectManager.getInstance().getOpenProjects(); for (Project project : projects) { @@ -378,11 +341,6 @@ public class EditorOptionsPanel { // Strip trailing spaces, ensure EOL on EOF on save isModified |= !getStripTrailingSpacesValue().equals(editorSettings.getStripTrailingSpaces()); isModified |= isModified(myCbEnsureBlankLineBeforeCheckBox, editorSettings.isEnsureNewLineAtEOF()); - isModified |= isModified(myCbShowQuickDocOnCheckBox, editorSettings.isShowQuickDocOnMouseOverElement()); - Long quickDocDelay = getQuickDocDelayFromGui(); - if (quickDocDelay != null && !quickDocDelay.equals(Long.valueOf(editorSettings.getQuickDocOnMouseOverElementDelayMillis()))) { - return true; - } // advanced mouse isModified |= isModified(myCbEnableDnD, editorSettings.isDndEnabled()); @@ -403,9 +361,7 @@ public class EditorOptionsPanel { isModified |= myErrorHighlightingPanel.isModified(); return isModified; } - - - + private static boolean isModified(JToggleButton checkBox, boolean value) { return checkBox.isSelected() != value; } @@ -448,15 +404,6 @@ public class EditorOptionsPanel { return defaultIndent; } - private void initQuickDocProcessing() { - myCbShowQuickDocOnCheckBox.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - myQuickDocDelayTextField.setEnabled(myCbShowQuickDocOnCheckBox.isSelected()); - } - }); - } - private void initSoftWrapsSettingsProcessing() { ItemListener listener = new ItemListener() { @Override diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java index 512b28c3ea51..b42eeb14e60f 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/DocumentationManager.java @@ -325,7 +325,7 @@ public class DocumentationManager extends DockablePopupManager - * Not thread-safe. - * - * @author Denis Zhdanov - * @since 7/2/12 9:09 AM - */ -public class QuickDocOnMouseOverManager { - - @NotNull private final EditorMouseMotionListener myMouseListener = new MyEditorMouseListener(); - @NotNull private final VisibleAreaListener myVisibleAreaListener = new MyVisibleAreaListener(); - @NotNull private final CaretListener myCaretListener = new MyCaretListener(); - @NotNull private final DocumentListener myDocumentListener = new MyDocumentListener(); - @NotNull private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); - @NotNull private final Runnable myRequest = new MyShowQuickDocRequest(); - @NotNull private final Runnable myHintCloseCallback = new MyCloseDocCallback(); - @NotNull private final Map myMonitoredDocuments = new WeakHashMap(); - - private final Map myActiveElements - = new WeakHashMap(); - - /** Holds a reference (if any) to the documentation manager used last time to show an 'auto quick doc' popup. */ - @Nullable private WeakReference myDocumentationManager; - - @Nullable private DelayedQuickDocInfo myDelayedQuickDocInfo; - private boolean myEnabled; - private boolean myApplicationActive; - - public QuickDocOnMouseOverManager(@NotNull Application application) { - EditorFactory factory = EditorFactory.getInstance(); - if (factory != null) { - factory.addEditorFactoryListener(new MyEditorFactoryListener(), application); - } - - ApplicationManager.getApplication().getMessageBus().connect().subscribe( - ApplicationActivationListener.TOPIC, - new ApplicationActivationListener() { - @Override - public void applicationActivated(IdeFrame ideFrame) { - myApplicationActive = true; - } - - @Override - public void applicationDeactivated(IdeFrame ideFrame) { - myApplicationActive = false; - } - }); - } - - /** - * Instructs the manager to enable or disable 'show quick doc automatically when the mouse goes over an editor element' mode. - * - * @param enabled flag that identifies if quick doc should be automatically shown - */ - public void setEnabled(boolean enabled) { - myEnabled = enabled; - myApplicationActive = enabled; - if (!enabled) { - closeQuickDocIfPossible(); - myAlarm.cancelAllRequests(); - } - EditorFactory factory = EditorFactory.getInstance(); - if (factory == null) { - return; - } - for (Editor editor : factory.getAllEditors()) { - if (enabled) { - registerListeners(editor); - } - else { - unRegisterListeners(editor); - } - } - } - - private void registerListeners(@NotNull Editor editor) { - editor.addEditorMouseMotionListener(myMouseListener); - editor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener); - editor.getCaretModel().addCaretListener(myCaretListener); - - Document document = editor.getDocument(); - if (myMonitoredDocuments.put(document, Boolean.TRUE) == null) { - document.addDocumentListener(myDocumentListener); - } - } - - private void unRegisterListeners(@NotNull Editor editor) { - editor.removeEditorMouseMotionListener(myMouseListener); - editor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener); - editor.getCaretModel().removeCaretListener(myCaretListener); - - Document document = editor.getDocument(); - if (myMonitoredDocuments.remove(document) != null) { - document.removeDocumentListener(myDocumentListener); - } - } - - private void processMouseMove(@NotNull EditorMouseEvent e) { - if (!myApplicationActive || e.getArea() != EditorMouseEventArea.EDITING_AREA) { - // Skip if the mouse is not at the editing area. - closeQuickDocIfPossible(); - return; - } - - if (e.getMouseEvent().getModifiers() != 0) { - // Don't show the control when any modifier is active (e.g. Ctrl or Alt is hold). There is a common situation that a user - // wants to navigate via Ctrl+click or perform quick evaluate by Alt+click. - return; - } - - Editor editor = e.getEditor(); - if (editor.isOneLineMode()) { - // Don't want auto quick doc to mess at, say, editor used for debugger condition. - return; - } - - Project project = editor.getProject(); - if (project == null) { - return; - } - - DocumentationManager documentationManager = DocumentationManager.getInstance(project); - JBPopup hint = documentationManager.getDocInfoHint(); - if (hint != null) { - - // Skip the event if the control is shown because of explicit 'show quick doc' action call. - WeakReference ref = myDocumentationManager; - if (ref == null) { - return; - } - DocumentationManager manager = ref.get(); - if (manager == null || !manager.isCloseOnSneeze()) { - return; - } - - // Skip the event if the mouse is under the opened quick doc control. - Point hintLocation = hint.getLocationOnScreen(); - Dimension hintSize = hint.getSize(); - int mouseX = e.getMouseEvent().getXOnScreen(); - int mouseY = e.getMouseEvent().getYOnScreen(); - if (mouseX >= hintLocation.x && mouseX <= hintLocation.x + hintSize.width && mouseY >= hintLocation.y - && mouseY <= hintLocation.y + hintSize.height) - { - return; - } - } - - PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); - if (psiFile == null) { - closeQuickDocIfPossible(); - return; - } - - int mouseOffset = editor.logicalPositionToOffset(editor.xyToLogicalPosition(e.getMouseEvent().getPoint())); - PsiElement elementUnderMouse = psiFile.findElementAt(mouseOffset); - if (elementUnderMouse == null || elementUnderMouse instanceof PsiWhiteSpace) { - closeQuickDocIfPossible(); - return; - } - - PsiElement targetElementUnderMouse = documentationManager.findTargetElement(editor, mouseOffset, psiFile, elementUnderMouse); - if (targetElementUnderMouse == null) { - // No PSI element is located under the current mouse position - close quick doc if any. - closeQuickDocIfPossible(); - return; - } - - PsiElement activeElement = myActiveElements.get(editor); - if (targetElementUnderMouse.equals(activeElement) - && (myAlarm.getActiveRequestCount() > 0 // Request to show documentation for the target component has been already queued. - || hint != null)) // Documentation for the target component is being shown. - { - return; - } - allowUpdateFromContext(false); - closeQuickDocIfPossible(); - myActiveElements.put(editor, targetElementUnderMouse); - myDelayedQuickDocInfo = new DelayedQuickDocInfo(documentationManager, editor, targetElementUnderMouse, elementUnderMouse); - - myAlarm.cancelAllRequests(); - myAlarm.addRequest(myRequest, EditorSettingsExternalizable.getInstance().getQuickDocOnMouseOverElementDelayMillis()); - } - - private void closeQuickDocIfPossible() { - myAlarm.cancelAllRequests(); - DocumentationManager docManager = getDocManager(); - if (docManager == null) { - return; - } - - JBPopup hint = docManager.getDocInfoHint(); - if (hint == null) { - return; - } - - hint.cancel(); - myDocumentationManager = null; - } - - private void allowUpdateFromContext(boolean allow) { - DocumentationManager documentationManager = getDocManager(); - if (documentationManager != null) { - documentationManager.setAllowContentUpdateFromContext(allow); - } - } - - @Nullable - private DocumentationManager getDocManager() { - WeakReference ref = myDocumentationManager; - if (ref == null) { - return null; - } - - DocumentationManager docManager = ref.get(); - if (docManager == null) { - return null; - } - return docManager; - } - - private static class DelayedQuickDocInfo { - - @NotNull public final DocumentationManager docManager; - @NotNull public final Editor editor; - @NotNull public final PsiElement targetElement; - @NotNull public final PsiElement originalElement; - - private DelayedQuickDocInfo(@NotNull DocumentationManager docManager, - @NotNull Editor editor, @NotNull PsiElement targetElement, - @NotNull PsiElement originalElement) - { - this.docManager = docManager; - this.editor = editor; - this.targetElement = targetElement; - this.originalElement = originalElement; - } - } - - private class MyShowQuickDocRequest implements Runnable { - - private final HintManager myHintManager = HintManager.getInstance(); - - @Override - public void run() { - myAlarm.cancelAllRequests(); - - // Skip the request if it's outdated (the mouse is moved other another element). - DelayedQuickDocInfo info = myDelayedQuickDocInfo; - if (info == null || !info.targetElement.equals(myActiveElements.get(info.editor))) { - return; - } - - // Skip the request if there is a control shown as a result of explicit 'show quick doc' (Ctrl + Q) invocation. - if (info.docManager.getDocInfoHint() != null && !info.docManager.isCloseOnSneeze()) { - return; - } - - // We don't want to show a quick doc control if there is an active hint (e.g. the mouse is under an invalid element - // and corresponding error info is shown). - if (!info.docManager.hasActiveDockedDocWindow() && myHintManager.hasShownHintsThatWillHideByOtherHint(false)) { - myAlarm.addRequest(this, EditorSettingsExternalizable.getInstance().getQuickDocOnMouseOverElementDelayMillis()); - return; - } - - info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, - info.editor.offsetToVisualPosition(info.originalElement.getTextRange().getStartOffset())); - try { - info.docManager.showJavaDocInfo(info.editor, info.targetElement, info.originalElement, myHintCloseCallback, true, true); - myDocumentationManager = new WeakReference(info.docManager); - } - finally { - info.editor.putUserData(PopupFactoryImpl.ANCHOR_POPUP_POSITION, null); - } - } - } - - private class MyCloseDocCallback implements Runnable { - @Override - public void run() { - myActiveElements.clear(); - myDocumentationManager = null; - } - } - - private class MyEditorFactoryListener implements EditorFactoryListener { - @Override - public void editorCreated(@NotNull EditorFactoryEvent event) { - if (myEnabled) { - registerListeners(event.getEditor()); - } - } - - @Override - public void editorReleased(@NotNull EditorFactoryEvent event) { - if (myEnabled) { - // We do this in the 'if' block because editor logs an error on attempt to remove already released listener. - unRegisterListeners(event.getEditor()); - } - } - } - - private class MyEditorMouseListener extends EditorMouseMotionAdapter { - - @Override - public void mouseMoved(EditorMouseEvent e) { - processMouseMove(e); - } - } - - private class MyVisibleAreaListener implements VisibleAreaListener { - @Override - public void visibleAreaChanged(VisibleAreaEvent e) { - closeQuickDocIfPossible(); - } - } - - private class MyCaretListener implements CaretListener { - @Override - public void caretPositionChanged(CaretEvent e) { - allowUpdateFromContext(true); - closeQuickDocIfPossible(); - } - } - - private class MyDocumentListener extends DocumentAdapter { - @Override - public void documentChanged(DocumentEvent e) { - closeQuickDocIfPossible(); - } - } -} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverStartupActivity.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverStartupActivity.java deleted file mode 100644 index 7c9de68b4362..000000000000 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/QuickDocOnMouseOverStartupActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2000-2012 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.codeInsight.documentation; - -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.StartupActivity; - -/** - * @author Denis Zhdanov - * @since 7/2/12 9:44 AM - */ -public class QuickDocOnMouseOverStartupActivity implements StartupActivity { - - @Override - public void runActivity(Project project) { - if (EditorSettingsExternalizable.getInstance().isShowQuickDocOnMouseOverElement()) { - ServiceManager.getService(QuickDocOnMouseOverManager.class).setEnabled(true); - } - } -} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java index 739d682c6660..17385cf42106 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java @@ -29,9 +29,9 @@ import com.intellij.ide.util.EditSourceUtil; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.NavigationItem; -import com.intellij.openapi.actionSystem.IdeActions; -import com.intellij.openapi.actionSystem.MouseShortcut; -import com.intellij.openapi.actionSystem.Shortcut; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.impl.ActionButton; +import com.intellij.openapi.actionSystem.impl.PresentationFactory; import com.intellij.openapi.application.AccessToken; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; @@ -59,6 +59,8 @@ import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; @@ -72,6 +74,7 @@ import com.intellij.usageView.UsageViewShortNameLocation; import com.intellij.usageView.UsageViewTypeLocation; import com.intellij.util.Processor; import org.intellij.lang.annotations.JdkConstants; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -87,7 +90,13 @@ import java.util.List; public class CtrlMouseHandler extends AbstractProjectComponent { - private static final int ourQuickDocRowsNumber = getIntProperty("quick.doc.desired.rows.number", 1); + public static final DataKey> + ELEMENT_UNDER_MOUSE_INFO_KEY = DataKey.create("ElementUnderMouseInfo"); + + private static final AnAction[] ourTooltipActions = { + new ShowQuickDocFromTooltipAction(), new ShowQuickDocAtPinnedWindowFromTooltipAction() + }; + private static final int ourQuickDocRowsNumber = getIntProperty("quick.doc.desired.rows.number", 1); private final TextAttributes ourReferenceAttributes; private HighlightersSet myHighlighter; @@ -276,7 +285,7 @@ public class CtrlMouseHandler extends AbstractProjectComponent { String fullText = documentationProvider.generateDoc(element, atPointer); String qName = element instanceof PsiQualifiedNamedElement ? ((PsiQualifiedNamedElement)element).getQualifiedName() : null; String text = DocPreviewUtil.buildPreview(result, qName, fullText, ourQuickDocRowsNumber); - return new DocInfo(text, documentationProvider, atPointer); + return new DocInfo(text, documentationProvider, element); } return DocInfo.EMPTY; } @@ -638,18 +647,43 @@ public class CtrlMouseHandler extends AbstractProjectComponent { info.showDocInfo(myDocumentationManager); } - HyperlinkListener listener = (docInfo.docProvider == null || docInfo.context == null) + HyperlinkListener hyperlinkListener = docInfo.docProvider == null ? null - : new QuickDocHyperlinkListener(myProject, myDocumentationManager, docInfo.docProvider, docInfo.context); - JComponent label = HintUtil.createInformationLabel(docInfo.text, listener); - final LightweightHint hint = new LightweightHint(label); + : new QuickDocHyperlinkListener(myProject, myDocumentationManager, docInfo.docProvider, + info.myElementAtPointer); + final Ref quickDocPaneRef = new Ref(); + MouseListener mouseListener = new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + QuickDocInfoPane pane = quickDocPaneRef.get(); + if (pane != null) { + pane.mouseEntered(e); + } + } + + @Override + public void mouseExited(MouseEvent e) { + QuickDocInfoPane pane = quickDocPaneRef.get(); + if (pane != null) { + pane.mouseExited(e); + } + } + }; + JComponent label = HintUtil.createInformationLabel(docInfo.text, hyperlinkListener, mouseListener); + QuickDocInfoPane quickDocPane = null; + if (docInfo.documentationAnchor != null) { + quickDocPane = new QuickDocInfoPane(docInfo.documentationAnchor, info.myElementAtPointer, label); + quickDocPaneRef.set(quickDocPane); + } + + JComponent hintContent = quickDocPane == null ? label : quickDocPane; + final LightweightHint hint = new LightweightHint(hintContent); final HintManagerImpl hintManager = HintManagerImpl.getInstanceImpl(); Point p = HintManagerImpl.getHintPosition(hint, myEditor, myPosition, HintManager.ABOVE); hintManager.showEditorHint(hint, myEditor, p, HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING, 0, false, HintManagerImpl.createHintHint(myEditor, p, hint, HintManager.ABOVE).setContentActive(false)); } - } private HighlightersSet installHighlighterSet(Info info, Editor editor) { @@ -708,12 +742,88 @@ public class CtrlMouseHandler extends AbstractProjectComponent { @Nullable public final String text; @Nullable public final DocumentationProvider docProvider; - @Nullable public final PsiElement context; + @Nullable public final PsiElement documentationAnchor; - DocInfo(@Nullable String text, @Nullable DocumentationProvider provider, @Nullable PsiElement context) { + DocInfo(@Nullable String text, @Nullable DocumentationProvider provider, @Nullable PsiElement documentationAnchor) { this.text = text; docProvider = provider; - this.context = context; + this.documentationAnchor = documentationAnchor; + } + } + + private class QuickDocInfoPane extends JLayeredPane implements DataProvider { + + @NotNull private final List myButtons = new ArrayList(); + @NotNull private final Pair myElementUnderMouseInfo; + @NotNull private final JComponent myBaseDocControl; + + QuickDocInfoPane(@NotNull PsiElement documentationAnchor, @NotNull PsiElement elementUnderMouse, @NotNull JComponent baseDocControl) { + myElementUnderMouseInfo = Pair.create(documentationAnchor, elementUnderMouse); + myBaseDocControl = baseDocControl; + + PresentationFactory presentationFactory = new PresentationFactory(); + for (AnAction action : ourTooltipActions) { + Icon icon = action.getTemplatePresentation().getIcon(); + Dimension minSize = new Dimension(icon.getIconWidth(), icon.getIconHeight()); + myButtons.add(new ActionButton(action, presentationFactory.getPresentation(action), IdeTooltipManager.IDE_TOOLTIP_PLACE, minSize)); + } + Collections.reverse(myButtons); + + setPreferredSize(baseDocControl.getPreferredSize()); + setMaximumSize(baseDocControl.getMaximumSize()); + setMinimumSize(baseDocControl.getMinimumSize()); + setBackground(baseDocControl.getBackground()); + + add(baseDocControl, Integer.valueOf(0)); + for (JComponent button : myButtons) { + button.setBorder(null); + button.setBackground(baseDocControl.getBackground()); + add(button, Integer.valueOf(1)); + button.setVisible(false); + } + } + + @Override + public Object getData(@NonNls String dataId) { + return ELEMENT_UNDER_MOUSE_INFO_KEY.is(dataId) ? myElementUnderMouseInfo : null; + } + + @Override + public void doLayout() { + Rectangle bounds = getBounds(); + myBaseDocControl.setBounds(bounds); + + final int buttonsHGap = 5; + int x = bounds.width; + for (JComponent button : myButtons) { + Dimension buttonSize = button.getPreferredSize(); + x -= buttonSize.width; + button.setBounds(x, 0, buttonSize.width, buttonSize.height); + x -= buttonsHGap; + } + } + + public void mouseEntered(@NotNull MouseEvent e) { + processStateChangeIfNecessary(e.getLocationOnScreen(), true); + } + + public void mouseExited(@NotNull MouseEvent e) { + processStateChangeIfNecessary(e.getLocationOnScreen(), false); + } + + private void processStateChangeIfNecessary(@NotNull Point mouseScreenLocation, boolean mouseEntered) { + // Don't show 'view quick doc' buttons if docked quick doc control is already active. + if (myDocumentationManager.hasActiveDockedDocWindow()) { + return; + } + + // Skip event triggered when mouse leaves action button area. + if (!mouseEntered && new Rectangle(getLocationOnScreen(), getSize()).contains(mouseScreenLocation)) { + return; + } + for (JComponent button : myButtons) { + button.setVisible(mouseEntered); + } } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocAtPinnedWindowFromTooltipAction.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocAtPinnedWindowFromTooltipAction.java new file mode 100644 index 000000000000..3e3652cd9267 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocAtPinnedWindowFromTooltipAction.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInsight.navigation; + +import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +/** + * @author Denis Zhdanov + * @since 7/13/12 11:43 AM + */ +public class ShowQuickDocAtPinnedWindowFromTooltipAction extends ShowQuickDocFromTooltipAction { + + public ShowQuickDocAtPinnedWindowFromTooltipAction() { + super(AllIcons.General.Pin_tab); + } + + @Override + protected void doActionPerformed(@NotNull Pair docInfo, @NotNull DocumentationManager docManager) { + docManager.createToolWindow(docInfo.first, docInfo.second); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocFromTooltipAction.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocFromTooltipAction.java new file mode 100644 index 000000000000..c044e8b9ec98 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/ShowQuickDocFromTooltipAction.java @@ -0,0 +1,108 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.codeInsight.navigation; + +import com.intellij.codeInsight.documentation.DocumentationManager; +import com.intellij.icons.AllIcons; +import com.intellij.ide.DataManager; +import com.intellij.ide.IdeTooltip; +import com.intellij.ide.IdeTooltipManager; +import com.intellij.idea.ActionsBundle; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.lang.ref.WeakReference; + +/** + * @author Denis Zhdanov + * @since 7/13/12 10:00 AM + */ +public class ShowQuickDocFromTooltipAction extends AnAction { + + @NotNull private final IdeTooltipManager myTooltipManager = IdeTooltipManager.getInstance(); + @NotNull private final DataManager myDataManager = DataManager.getInstance(); + + private WeakReference> myInfo; + + public ShowQuickDocFromTooltipAction() { + this(AllIcons.Actions.Find); + } + + public ShowQuickDocFromTooltipAction(@NotNull Icon icon) { + String className = getClass().getName(); + String actionId = className.substring(0, className.lastIndexOf("Action")); + getTemplatePresentation().setText(ActionsBundle.actionText(actionId)); + getTemplatePresentation().setDescription(ActionsBundle.actionDescription(actionId)); + getTemplatePresentation().setIcon(icon); + } + + @Override + public void update(AnActionEvent e) { + + // We can't use data context from the given event because it's built from the focused component and IDE tooltip doesn't have focus. + IdeTooltip tooltip = myTooltipManager.getCurrentTooltip(); + if (tooltip == null) { + return; + } + + JComponent component = tooltip.getTipComponent(); + if (component == null) { + return; + } + + Pair info = CtrlMouseHandler.ELEMENT_UNDER_MOUSE_INFO_KEY.getData(myDataManager.getDataContext(component)); + if (info != null) { + // Target info is retrieved during AnAction.update() processing because IDE tooltip is closed on action activation, + // i.e. IdeTooltipManager.getCurrentComponent() returns null during AnAction.actionPerformed() execution. + myInfo = new WeakReference>(info); + } + } + + @Override + public void actionPerformed(AnActionEvent e) { + WeakReference> infoRef = myInfo; + if (infoRef == null) { + return; + } + Pair info = infoRef.get(); + if (info == null) { + return; + } + + Project project = PlatformDataKeys.PROJECT.getData(e.getDataContext()); + if (project == null) { + return; + } + + myInfo = null; + doActionPerformed(info, DocumentationManager.getInstance(project)); + } + + protected void doActionPerformed(@NotNull Pair docInfo, + @NotNull DocumentationManager docManager) + { + docManager.showJavaDocInfo(docInfo.first, docInfo.second, true, null); + } +} diff --git a/platform/platform-impl/src/com/intellij/codeInsight/hint/HintUtil.java b/platform/platform-impl/src/com/intellij/codeInsight/hint/HintUtil.java index d048ba4c5f11..fa155286244f 100644 --- a/platform/platform-impl/src/com/intellij/codeInsight/hint/HintUtil.java +++ b/platform/platform-impl/src/com/intellij/codeInsight/hint/HintUtil.java @@ -33,6 +33,9 @@ import javax.swing.border.EmptyBorder; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; public class HintUtil { public static final Color INFORMATION_COLOR = new Color(253, 254, 226); @@ -49,10 +52,13 @@ public class HintUtil { } public static JComponent createInformationLabel(@NotNull String text) { - return createInformationLabel(text, null); + return createInformationLabel(text, null, null); } - - public static JComponent createInformationLabel(@NotNull String text, @Nullable HyperlinkListener listener) { + + public static JComponent createInformationLabel(@NotNull String text, + @Nullable HyperlinkListener hyperlinkListener, + @Nullable MouseListener mouseListener) + { HintHint hintHint = new HintHint().setTextBg(INFORMATION_COLOR).setTextFg(Color.black).setFont(getBoldFont()).setAwtTooltip(true); HintLabel label = new HintLabel(); @@ -67,8 +73,11 @@ public class HintUtil { label.setOpaque(true); } - if (listener != null) { - label.myPane.addHyperlinkListener(listener); + if (hyperlinkListener != null) { + label.myPane.addHyperlinkListener(hyperlinkListener); + } + if (mouseListener != null) { + label.myPane.addMouseListener(mouseListener); } return label; diff --git a/platform/platform-impl/src/com/intellij/ide/IdeTooltipManager.java b/platform/platform-impl/src/com/intellij/ide/IdeTooltipManager.java index 9a80da99d7f0..f033ac7a01b2 100644 --- a/platform/platform-impl/src/com/intellij/ide/IdeTooltipManager.java +++ b/platform/platform-impl/src/com/intellij/ide/IdeTooltipManager.java @@ -57,6 +57,8 @@ import java.awt.event.MouseEvent; public class IdeTooltipManager implements ApplicationComponent, AWTEventListener { + public static final String IDE_TOOLTIP_PLACE = "IdeTooltip"; + public static final Color GRAPHITE_COLOR = new Color(100, 100, 100, 230); private RegistryValue myIsEnabled; @@ -64,8 +66,8 @@ public class IdeTooltipManager implements ApplicationComponent, AWTEventListener private Component myQueuedComponent; private BalloonImpl myCurrentTipUi; - private MouseEvent myCurrentEvent; - private boolean myCurrentTipIsCentered; + private MouseEvent myCurrentEvent; + private boolean myCurrentTipIsCentered; private Runnable myHideRunnable; @@ -75,12 +77,12 @@ public class IdeTooltipManager implements ApplicationComponent, AWTEventListener private final Alarm myAlarm = new Alarm(); - private int myX; - private int myY; + private int myX; + private int myY; private RegistryValue myMode; private IdeTooltip myCurrentTooltip; - private Runnable myShowRequest; + private Runnable myShowRequest; private IdeTooltip myQueuedTooltip; @@ -341,7 +343,11 @@ public class IdeTooltipManager implements ApplicationComponent, AWTEventListener } }, tooltip.getDismissDelay()); } - + + @Nullable + public IdeTooltip getCurrentTooltip() { + return myCurrentTooltip; + } public Color getTextForeground(boolean awtTooltip) { return useGraphite(awtTooltip) ? Color.white : UIUtil.getToolTipForeground(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java index 476863b6adaf..0be8fe3d7f4d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorSettingsExternalizable.java @@ -19,7 +19,6 @@ import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.ExportableApplicationComponent; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces; import com.intellij.openapi.options.OptionsBundle; import com.intellij.openapi.util.DefaultJDOMExternalizer; @@ -51,8 +50,6 @@ public class EditorSettingsExternalizable implements NamedJDOMExternalizable, Ex public boolean IS_CARET_INSIDE_TABS; @NonNls public String STRIP_TRAILING_SPACES = "Changed"; public boolean IS_ENSURE_NEWLINE_AT_EOF = false; - public boolean SHOW_QUICK_DOC_ON_MOUSE_OVER_ELEMENT = false; - public long QUICK_DOC_ON_MOUSE_OVER_DELAY_MS = 500; public boolean IS_CARET_BLINKING = true; public int CARET_BLINKING_PERIOD = 500; public boolean IS_RIGHT_MARGIN_SHOWN = true; @@ -368,28 +365,6 @@ public class EditorSettingsExternalizable implements NamedJDOMExternalizable, Ex myOptions.STRIP_TRAILING_SPACES = stripTrailingSpaces; } - public boolean isShowQuickDocOnMouseOverElement() { - return myOptions.SHOW_QUICK_DOC_ON_MOUSE_OVER_ELEMENT; - } - - public void setShowQuickDocOnMouseOverElement(boolean show) { - myOptions.SHOW_QUICK_DOC_ON_MOUSE_OVER_ELEMENT = show; - } - - public long getQuickDocOnMouseOverElementDelayMillis() { - return myOptions.QUICK_DOC_ON_MOUSE_OVER_DELAY_MS; - } - - public void setQuickDocOnMouseOverElementDelayMillis(long delay) throws IllegalArgumentException { - if (delay <= 0) { - throw new IllegalArgumentException(String.format( - "Non-positive delay for the 'show quick doc on mouse over element' value detected! Expected positive value but got %d", - delay - )); - } - myOptions.QUICK_DOC_ON_MOUSE_OVER_DELAY_MS = delay; - } - public boolean isRefrainFromScrolling() { return myOptions.REFRAIN_FROM_SCROLLING; } diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index 4f3403375faf..a4d30ecdfa98 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -201,6 +201,10 @@ action.CompareTwoFiles.text=Compare Two _Files action.CompareTwoFiles.description=Compare two selected files action.CompareFileWithEditor.text=Co_mpare File with Editor action.CompareFileWithEditor.description=Compare selected file with editor +action.ShowQuickDocFromTooltip.text=Full documentation +action.ShowQuickDocFromTooltip.description=Show full documentation +action.ShowQuickDocAtPinnedWindowFromTooltip.text=Full documentation in pinned window +action.ShowQuickDocAtPinnedWindowFromTooltip.description=Show full documentation in pinned window group.LocalHistory.text=Local _History action.LocalHistory.ShowHistory.text=Show _History diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 9e224fcc49ed..d051b1750bac 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -734,9 +734,6 @@ serviceImplementation="com.intellij.ide.todo.TodoConfiguration"/> - - - From b1ed6b4646946d163164d235cc13f89e82a0c9f9 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Fri, 13 Jul 2012 15:47:08 +0400 Subject: [PATCH 06/66] IDEA-66333 Quick documentation lookup on mouse hover Don't add documentation snippet to the Ctrl+mouse hover control as it already provide action buttons to open complete documentation control --- .../navigation/CtrlMouseHandler.java | 19 +- .../navigation/DocPreviewUtil.java | 312 +----------------- 2 files changed, 11 insertions(+), 320 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java index 17385cf42106..9ae3a8632684 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/CtrlMouseHandler.java @@ -93,10 +93,9 @@ public class CtrlMouseHandler extends AbstractProjectComponent { public static final DataKey> ELEMENT_UNDER_MOUSE_INFO_KEY = DataKey.create("ElementUnderMouseInfo"); - private static final AnAction[] ourTooltipActions = { + private static final AnAction[] ourTooltipActions = { new ShowQuickDocFromTooltipAction(), new ShowQuickDocAtPinnedWindowFromTooltipAction() }; - private static final int ourQuickDocRowsNumber = getIntProperty("quick.doc.desired.rows.number", 1); private final TextAttributes ourReferenceAttributes; private HighlightersSet myHighlighter; @@ -233,20 +232,6 @@ public class CtrlMouseHandler extends AbstractProjectComponent { return "CtrlMouseHandler"; } - private static int getIntProperty(@NotNull String propertyName, int defaultValue) { - String valueAsString = System.getProperty(propertyName); - if (valueAsString == null) { - return defaultValue; - } - - try { - return Integer.parseInt(valueAsString); - } - catch (Exception e) { - return defaultValue; - } - } - private static BrowseMode getBrowseMode(@JdkConstants.InputEventMask int modifiers) { if (modifiers != 0) { final Keymap activeKeymap = KeymapManager.getInstance().getActiveKeymap(); @@ -284,7 +269,7 @@ public class CtrlMouseHandler extends AbstractProjectComponent { if (result != null) { String fullText = documentationProvider.generateDoc(element, atPointer); String qName = element instanceof PsiQualifiedNamedElement ? ((PsiQualifiedNamedElement)element).getQualifiedName() : null; - String text = DocPreviewUtil.buildPreview(result, qName, fullText, ourQuickDocRowsNumber); + String text = DocPreviewUtil.buildPreview(result, qName, fullText); return new DocInfo(text, documentationProvider, element); } return DocInfo.EMPTY; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java index 390a960ccbc8..898a06f1bc12 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/navigation/DocPreviewUtil.java @@ -18,14 +18,11 @@ package com.intellij.codeInsight.navigation; import com.intellij.codeInsight.documentation.DocumentationManager; import com.intellij.lang.documentation.DocumentationProvider; import com.intellij.psi.PsiElement; -import com.intellij.util.containers.Stack; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,26 +36,12 @@ import java.util.regex.Pattern; */ public class DocPreviewUtil { - private static final Set TAGS_TO_ADD_LF = new HashSet(); - private static final Set TAGS_TO_IGNORE = new HashSet(); - static { - for (String tag : new String[] {"p", "blockquote", "pre"}) { - TAGS_TO_ADD_LF.add(tag); - TAGS_TO_ADD_LF.add(tag.toUpperCase()); - } - - for (String tag : new String[] {"style", "b", "small"}) { - TAGS_TO_IGNORE.add(tag); - TAGS_TO_IGNORE.add(tag.toUpperCase()); - } - } - private DocPreviewUtil() { } /** - * Allows to build a documentation preview from the given arguments. Basically, takes given 'full documentation', cuts it according - * to the given 'desired rows' argument and returns a result. + * Allows to build a documentation preview from the given arguments. Basically, takes given 'header' text and tries to modify + * it by using hyperlink information encapsulated at the given 'full text'. * * @param header target documentation header. Is expected to be a result of the * {@link DocumentationProvider#getQuickNavigateInfo(PsiElement, PsiElement)} call @@ -66,91 +49,31 @@ public class DocPreviewUtil { * (according to the given 'desired rows and columns per-row' arguments). A link that points to the * element with the given qualified name is added to the preview's end if the qName is provided then * @param fullText full documentation text (if available) - * @param desiredRowsNumber maximum number of rows to use at the preview's body ('header' text is not count here) - * @return preview text to use for the given arguments */ @NotNull - public static String buildPreview(@NotNull String header, @Nullable String qName, @Nullable String fullText, int desiredRowsNumber) { + public static String buildPreview(@NotNull final String header, @Nullable final String qName, @Nullable final String fullText) { if (fullText == null) { return header; } - int bodyStart = fullText.indexOf(""); - if (bodyStart < 0) { - return header; - } - bodyStart += "".length(); - - int bodyEnd = fullText.indexOf(""); - if (bodyEnd < 0) { - return header; - } - - int headerEnd = fullText.indexOf(""); - if (headerEnd < 0) { - return header; - } - - String headerWithLinks = fullText.substring(bodyStart, headerEnd); - String docText = fullText.substring(headerEnd + "".length(), bodyEnd); - - // The algorithm is: - // 1. Get given header text and replace meaningful symbols with the links to those symbols; - // 2. Calculate max symbols per-row to use for the result as a number of non-markup symbols at the longest header line; - // 3. Process full text body as follows: - // 2.1. Count non-markup symbols until desired row symbols number is exceeded; - // 2.2. Insert
after that to start a new row (if max rows number is not reached); - // 3.3. Stop processing as soon as the desired rows number is reached or the text is finished; - // 4. Add a link to the full documentation if it's not placed inside the resulted text; - // 5. Add closing tags for all non-matched open tags; - - final Context context = new Context(desiredRowsNumber); - - int columnsPerRow = parseHeader(header, headerWithLinks, qName, context); - process(docText, new BodyCallback(context, qName, columnsPerRow)); - - //region Add closing tags - while (!context.openTags.isEmpty()) { - context.buffer.append("'); - } - //endregion - - return context.buffer.toString(); - } - - private static int parseHeader(@NotNull String headerTemplate, - @NotNull String headerWithLinks, - @Nullable String qName, - @NotNull Context context) - { - - //region Build links info. + // Build links info. Map links = new HashMap(); - process(headerWithLinks, new LinksCollector(links)); + process(fullText, new LinksCollector(links)); if (qName != null) { links.put(qName, DocumentationManager.PSI_ELEMENT_PROTOCOL + qName); } - //endregion - - //region Apply links info to the header template. - String headerToUse = headerTemplate.replace("\n", "
"); + // Apply links info to the header template. + String result = header.replace("\n", "
"); for (Map.Entry entry : links.entrySet()) { String visibleName = entry.getKey(); int i = visibleName.lastIndexOf('.'); if (i > 0 && i < visibleName.length() - 1) { visibleName = visibleName.substring(i + 1); } - headerToUse = headerToUse.replace(entry.getKey(), String.format("%s", entry.getValue(), visibleName)); + result = result.replace(entry.getKey(), String.format("%s", entry.getValue(), visibleName)); } - context.buffer.append(headerToUse); - //endregion - - //region Update 'columns-per-row' if the header is wide. - MaxColumnCalculator calculator = new MaxColumnCalculator(); - process(headerToUse, calculator); - return calculator.maxColumn; - //endregion + return result; } private enum State {TEXT, INSIDE_OPEN_TAG, INSIDE_CLOSE_TAG} @@ -235,17 +158,6 @@ public class DocPreviewUtil { return i; } - private static class Context { - - @NotNull public final Stack openTags = new Stack(); - @NotNull public final StringBuilder buffer = new StringBuilder(); - public final int rows; - - public Context(int rows) { - this.rows = rows; - } - } - private interface Callback { boolean onOpenTag(@NotNull String name, @NotNull String text); boolean onCloseTag(@NotNull String name, @NotNull String text); @@ -298,210 +210,4 @@ public class DocPreviewUtil { return true; } } - - private static class MaxColumnCalculator implements Callback { - - private static final Map SUBSTITUTIONS = new HashMap(); - static { - SUBSTITUTIONS.put("<", "<"); - SUBSTITUTIONS.put(">", ">"); - SUBSTITUTIONS.put(" ", " "); - } - - public int maxColumn; - private int myCurrentColumn; - - @Override - public boolean onOpenTag(@NotNull String name, @NotNull String text) { - if ("br".equals(name)) { - myCurrentColumn = 0; - } - return true; - } - - @Override - public boolean onCloseTag(@NotNull String name, @NotNull String text) { - return true; - } - - @Override - public boolean onStandaloneTag(@NotNull String name, @NotNull String text) { - return onOpenTag(name, text); - } - - @Override - public boolean onText(@NotNull String text) { - for (Map.Entry entry : SUBSTITUTIONS.entrySet()) { - text = text.replace(entry.getKey(), entry.getValue()); - } - myCurrentColumn += text.length(); - maxColumn = Math.max(maxColumn, myCurrentColumn); - return true; - } - } - - private static class BodyCallback implements Callback { - - @NotNull protected final Context myContext; - private boolean myScheduleNewLine; - private boolean myInsidePre; - private boolean myDocStarted; - private int myCurrentRow; - private int myCurrentColumn; - @Nullable private String myQName; - private int myColumnsPerRow; - - - protected BodyCallback(@NotNull Context context, @Nullable String qName, final int columnsPerRow) { - myContext = context; - myQName = qName; - myColumnsPerRow = columnsPerRow; - } - - public boolean onOpenTag(@NotNull String name, @NotNull String text) { - if ("pre".equals(name)) { - myInsidePre = true; - } - if (!processDelayedLfTag(name)) { - return myCurrentRow < myContext.rows; - } - - if (!TAGS_TO_IGNORE.contains(name)) { - addText(text, false); - myContext.openTags.push(name); - } - return true; - } - - private boolean processDelayedLfTag(@NotNull String name) { - if (!TAGS_TO_ADD_LF.contains(name)) { - if (myScheduleNewLine) { - newLine(); - } - return true; - } - - myScheduleNewLine = true; - return false; - } - - public boolean onCloseTag(@NotNull String name, @NotNull String text) { - if ("pre".equals(name)) { - myInsidePre = false; - } - - if (!processDelayedLfTag(name)) { - return myCurrentRow < myContext.rows; - } - - if (!TAGS_TO_IGNORE.contains(name)) { - addText(text, false); - myContext.openTags.remove(name); - } - return true; - } - - public boolean onStandaloneTag(@NotNull String name, @NotNull String text) { - if (!processDelayedLfTag(name)) { - return true; - } - - if (!TAGS_TO_IGNORE.contains(name)) { - addText(text, false); - } - return true; - } - - public boolean onText(@NotNull String text) { - boolean addSpace = false; - if (!text.isEmpty() && (text.startsWith(" ") || text.startsWith("\t")) && !addText(String.valueOf(text.charAt(0)), true)) { - return false; - } - - String tailText = (!text.isEmpty() && (text.endsWith(" ") || text.endsWith("\t"))) ? text.substring(text.length() - 1) : null; - - text = text.trim(); - if (myInsidePre && text.contains("\n")) { - boolean addLf = false; - for (String s : text.split("\n")) { - if (addLf) { - if (!newLine()) { - return false; - } - } - else { - addLf = true; - } - if (!onText(s)) return false; - } - return true; - } - - for (String s : text.split(" ")) { - s = s.trim(); - if (s.length() <= 0) { - continue; - } - - if (myScheduleNewLine && canBreakBeforeText(s)) { - if (!newLine()) return false; - addSpace = false; - } - - if (addSpace) { - if (!addText(" ", true)) return false; - } - else { - addSpace = true; - } - - if (!addText(s, true)) return false; - } - return !(tailText != null && !addText(tailText, true)); - } - - private static boolean canBreakBeforeText(@NotNull String text) { - return !text.startsWith(">") && !text.startsWith(","); - } - - private boolean newLine() { - if (onLastRow()) { - if (myQName != null) { - myContext.buffer.append(String.format(" ...", myQName)); - } - return false; - } - addText("
", false); - myCurrentColumn = 0; - myScheduleNewLine = false; - myCurrentRow++; - return true; - } - - private boolean addText(@NotNull String text, boolean countColumns) { - if (!myDocStarted) { - myContext.buffer.append("
"); - myDocStarted = true; - } - - if (!countColumns) { - myContext.buffer.append(text); - return true; - } - - int remainingColumns = myColumnsPerRow - myCurrentColumn; - if (onLastRow()) { - remainingColumns -= " ...".length(); - } - if (remainingColumns < text.length() && !newLine()) return false; - - myContext.buffer.append(text); - myCurrentColumn += text.length(); - return true; - } - - private boolean onLastRow() { - return myCurrentRow >= myContext.rows - 1; - } - } } From 677303b39aa5d526f3e8623cdcf6cff74ed3408f Mon Sep 17 00:00:00 2001 From: Eugene Zhuravlev Date: Fri, 13 Jul 2012 13:47:27 +0200 Subject: [PATCH 07/66] IDEA-88779 java.lang.IndexOutOfBoundsException: queue is empty --- .../testSrc/org/jetbrains/ether/StorageDumper.java | 4 +++- .../src/com/intellij/openapi/project/DumbServiceImpl.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/jps/jps-builders/testSrc/org/jetbrains/ether/StorageDumper.java b/jps/jps-builders/testSrc/org/jetbrains/ether/StorageDumper.java index 21fd56336451..dec30170dbce 100644 --- a/jps/jps-builders/testSrc/org/jetbrains/ether/StorageDumper.java +++ b/jps/jps-builders/testSrc/org/jetbrains/ether/StorageDumper.java @@ -1,5 +1,6 @@ package org.jetbrains.ether; +import com.intellij.openapi.util.io.FileUtil; import org.jetbrains.ether.dependencyView.Mappings; import java.io.File; @@ -118,8 +119,9 @@ public class StorageDumper { } else { try { - final String outputPath = (oath == null ? "" : oath) + File.separator + "snapshot-" + new SimpleDateFormat("dd-MM-yy(hh:mm:ss)").format(new Date()) + ".log"; + final File outputPath = new File(oath == null ? "" : oath, "snapshot-" + new SimpleDateFormat("dd-MM-yy(hh-mm-ss)").format(new Date()) + ".log"); final File dataStorageRoot = new File(path + File.separator + "mappings"); + FileUtil.createIfDoesntExist(outputPath); final Mappings mappings = new Mappings(dataStorageRoot, true); final PrintStream p = new PrintStream(outputPath); diff --git a/platform/platform-impl/src/com/intellij/openapi/project/DumbServiceImpl.java b/platform/platform-impl/src/com/intellij/openapi/project/DumbServiceImpl.java index 76797c6f7201..0d7fc2e1d4d7 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/DumbServiceImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/project/DumbServiceImpl.java @@ -364,7 +364,7 @@ public class DumbServiceImpl extends DumbService { public void run() { IndexUpdateRunnable nextUpdateRunnable = null; try { - nextUpdateRunnable = myUpdatesQueue.pullFirst(); + nextUpdateRunnable = myUpdatesQueue.isEmpty()? null : myUpdatesQueue.pullFirst(); if (nextUpdateRunnable == null) { // really terminate the task myActionQueue.offer(NULL_ACTION); From 2daa65e13a500b0776e7911e81a683ff5e673a91 Mon Sep 17 00:00:00 2001 From: Kirill Likhodedov Date: Fri, 13 Jul 2012 15:48:12 +0400 Subject: [PATCH 08/66] [github] IDEA-88522 Support show commit on github in Annotated view Introduce the AnnotationGutterActionProvider extension point to let plugins add custom actions to the annotation popup. Create GithubShowCommitInBrowserFromAnnotateAction, and also move common stuff from the similar action for the log to GithubShowCommitInBrowserAction. --- .../src/META-INF/VcsExtensionPoints.xml | 1 + .../AnnotationGutterActionProvider.java | 39 + .../vcs/actions/AnnotateToggleAction.java | 27 +- .../vcs/actions/AnnotationPresentation.java | 5 + plugins/github/src/META-INF/plugin.xml | 3 +- .../github/GithubOpenInBrowserAction.java | 368 ++++---- .../GithubShowCommitInBrowserAction.java | 79 +- ...ShowCommitInBrowserFromAnnotateAction.java | 105 +++ ...ithubShowCommitInBrowserFromLogAction.java | 97 +++ .../jetbrains/plugins/github/GithubUtil.java | 814 +++++++++--------- .../GithubAnnotationGutterActionProvider.java | 35 + 11 files changed, 900 insertions(+), 673 deletions(-) create mode 100644 platform/vcs-api/src/com/intellij/openapi/vcs/annotate/AnnotationGutterActionProvider.java create mode 100644 plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromAnnotateAction.java create mode 100644 plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromLogAction.java create mode 100644 plugins/github/src/org/jetbrains/plugins/github/ui/GithubAnnotationGutterActionProvider.java diff --git a/platform/platform-resources/src/META-INF/VcsExtensionPoints.xml b/platform/platform-resources/src/META-INF/VcsExtensionPoints.xml index 046a54fe4195..fc4d0c88bbe1 100644 --- a/platform/platform-resources/src/META-INF/VcsExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/VcsExtensionPoints.xml @@ -42,6 +42,7 @@ interface="com.intellij.openapi.vcs.actions.VcsQuickListContentProvider"/> + diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/AnnotationGutterActionProvider.java b/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/AnnotationGutterActionProvider.java new file mode 100644 index 000000000000..d3bc6fcda482 --- /dev/null +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/annotate/AnnotationGutterActionProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.vcs.annotate; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.extensions.ExtensionPointName; +import org.jetbrains.annotations.NotNull; + +/** + * Implement this to add additional custom actions to the popup invoked by right-clicking on the annotation gutter. + * + * @author Kirill Likhodedov + */ +public interface AnnotationGutterActionProvider { + + ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.vcsAnnotationGutterActionProvider"); + + /** + * Create an action that will be added to the annotation gutter popup. + * @param annotation annotation which is currently shown on the gutter. + * @return new action that can be invoked from the annotation gutter popup. + */ + @NotNull + AnAction createAction(FileAnnotation annotation); + +} diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java index 6f9ce4b23b09..a82236fd3d3a 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java @@ -17,6 +17,7 @@ package com.intellij.openapi.vcs.actions; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.Separator; import com.intellij.openapi.actionSystem.ToggleAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; @@ -207,12 +208,6 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware, Ann new AnnotationPresentation(highlighting, switcher, editorGutter, gutters, additionalActions.toArray(new AnAction[additionalActions.size()])); - for (AnAction action : additionalActions) { - if (action instanceof LineNumberListener) { - presentation.addLineNumberListener((LineNumberListener)action); - } - } - final Map bgColorMap = Registry.is("vcs.show.colored.annotations") ? computeBgColors(fileAnnotation) : null; final Map historyIds = Registry.is("vcs.show.history.numbers") ? computeLineNumbers(fileAnnotation) : null; @@ -248,9 +243,17 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware, Ann gutters.add(new HighlightedAdditionalColumn(fileAnnotation, editor, null, presentation, highlighting, bgColorMap)); final AnnotateActionGroup actionGroup = new AnnotateActionGroup(gutters, editorGutter); presentation.addAction(actionGroup, 1); - presentation.addAction(new ShowHideAdditionalInfoAction(gutters, editorGutter, actionGroup)); gutters.add(new ExtraFieldGutter(fileAnnotation, editor, presentation, bgColorMap, actionGroup)); + presentation.addAction(new ShowHideAdditionalInfoAction(gutters, editorGutter, actionGroup)); + addActionsFromExtensions(presentation, fileAnnotation); + + for (AnAction action : presentation.getActions()) { + if (action instanceof LineNumberListener) { + presentation.addLineNumberListener((LineNumberListener)action); + } + } + for (AnnotationFieldGutter gutter : gutters) { final AnnotationGutterLineConvertorProxy proxy = new AnnotationGutterLineConvertorProxy(getUpToDateLineNumber, gutter); if (gutter.isGutterAction()) { @@ -263,6 +266,16 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware, Ann } } + private static void addActionsFromExtensions(@NotNull AnnotationPresentation presentation, @NotNull FileAnnotation fileAnnotation) { + AnnotationGutterActionProvider[] extensions = AnnotationGutterActionProvider.EP_NAME.getExtensions(); + if (extensions.length > 0) { + presentation.addAction(new Separator()); + } + for (AnnotationGutterActionProvider provider : extensions) { + presentation.addAction(provider.createAction(fileAnnotation)); + } + } + @Nullable private static Map computeLineNumbers(FileAnnotation fileAnnotation) { final SortedList revisions = new SortedList(new Comparator() { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotationPresentation.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotationPresentation.java index bf36fb72b917..f2c887a5078d 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotationPresentation.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotationPresentation.java @@ -85,6 +85,11 @@ class AnnotationPresentation implements TextAnnotationPresentation { return myActions; } + @NotNull + public List getActions() { + return myActions; + } + public void addSourceSwitchListener(final Consumer listener) { mySwitchAction.addSourceSwitchListener(listener); } diff --git a/plugins/github/src/META-INF/plugin.xml b/plugins/github/src/META-INF/plugin.xml index 2a8f25f69588..104297300162 100644 --- a/plugins/github/src/META-INF/plugin.xml +++ b/plugins/github/src/META-INF/plugin.xml @@ -14,6 +14,7 @@ + @@ -28,7 +29,7 @@ - + diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubOpenInBrowserAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubOpenInBrowserAction.java index a25cb131f880..b9cc2f21dd7d 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubOpenInBrowserAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubOpenInBrowserAction.java @@ -1,200 +1,168 @@ -/* - * Copyright 2000-2010 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jetbrains.plugins.github; - -import com.intellij.ide.BrowserUtil; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.DumbAwareAction; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vfs.VirtualFile; -import git4idea.GitBranch; -import git4idea.GitUtil; -import git4idea.repo.GitRepository; -import git4idea.repo.GitRepositoryManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.plugins.github.ui.GithubLoginDialog; - -import static org.jetbrains.plugins.github.GithubUtil.*; - -/** - * Created by IntelliJ IDEA. - * - * @author oleg - * @date 12/10/10 - */ -public class GithubOpenInBrowserAction extends DumbAwareAction { - public static final String CANNOT_OPEN_IN_BROWSER = "Cannot open in browser"; - private static final Logger LOG = Logger.getInstance(GithubOpenInBrowserAction.class.getName()); - - protected GithubOpenInBrowserAction() { - super("Open in browser", "Open corresponding GitHub link in browser", GITHUB_ICON); - } - - @Override - public void update(final AnActionEvent e) { - Project project = e.getData(PlatformDataKeys.PROJECT); - VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); - if (project == null || project.isDefault() || virtualFile == null) { - setVisibleEnabled(e, false, false); - return; - } - GitRepositoryManager manager = GitUtil.getRepositoryManager(project); - - final GitRepository gitRepository = manager.getRepositoryForFile(virtualFile); - if (gitRepository == null) { - setVisibleEnabled(e, false, false); - return; - } - - // Check that given repository is properly configured git repository - if (!isRepositoryOnGitHub(gitRepository)) { - setVisibleEnabled(e, false, false); - return; - } - - ChangeListManager changeListManager = ChangeListManager.getInstance(project); - if (changeListManager.isUnversioned(virtualFile)) { - setVisibleEnabled(e, true, false); - return; - } - - Change change = changeListManager.getChange(virtualFile); - if (change != null && change.getType() == Change.Type.NEW) { - setVisibleEnabled(e, true, false); - return; - } - - setVisibleEnabled(e, true, true); - } - - @SuppressWarnings("ConstantConditions") - @Override - public void actionPerformed(final AnActionEvent e) { - final Project project = e.getData(PlatformDataKeys.PROJECT); - while (!checkCredentials(project)) { - final GithubLoginDialog dialog = new GithubLoginDialog(project); - dialog.show(); - if (!dialog.isOK()) { - return; - } - } - - final VirtualFile root = project.getBaseDir(); - GitRepositoryManager manager = GitUtil.getRepositoryManager(project); - if (manager == null) { - return; - } - final GitRepository gitRepository = manager.getRepositoryForFile(root); - // Check that given repository is properly configured git repository - final String githubRemoteUrl = findGithubRemoteUrl(gitRepository); - - final VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); - final String rootPath = root.getPath(); - final String path = virtualFile.getPath(); - if (!path.startsWith(rootPath)) { - Messages.showErrorDialog(project, "File is not under project root", CANNOT_OPEN_IN_BROWSER); - return; - } - - String branch = getBranchNameOnRemote(project, root); - if (branch == null) { - return; - } - - String relativePath = path.substring(rootPath.length()); - String urlToOpen = makeUrlToOpen(e, relativePath, branch, githubRemoteUrl); - BrowserUtil.launchBrowser(urlToOpen); - } - - private static String makeUrlToOpen(@NotNull AnActionEvent e, @NotNull String relativePath, @NotNull String branch, - @NotNull String githubRemoteUrl) { - final StringBuilder builder = new StringBuilder(); - builder.append(makeGithubRepoUrlFromRemoteUrl(githubRemoteUrl)).append("/blob/").append(branch).append(relativePath); - final Editor editor = e.getData(PlatformDataKeys.EDITOR); - if (editor != null) { - final int line = editor.getCaretModel().getLogicalPosition().line + 1; // lines are counted internally from 0, but from 1 on github - builder.append("#L").append(line); - } - return builder.toString(); - } - - @NotNull - private static String makeGithubRepoUrlFromRemoteUrl(@NotNull String remoteUrl) { - remoteUrl = removeEndingDotGit(remoteUrl); - if (remoteUrl.startsWith("http")) { - return remoteUrl; - } - if (remoteUrl.startsWith("git://")) { - return "https" + remoteUrl.substring(3); - } - return convertFromSshToHttp(remoteUrl); - } - - @NotNull - private static String convertFromSshToHttp(@NotNull String remoteUrl) { - // Format: git@github.com:account/repository - int indexOfAt = remoteUrl.indexOf("@"); - if (indexOfAt < 0) { - throw new IllegalStateException("Invalid remote Github SSH url: " + remoteUrl); - } - String withoutPrefix = remoteUrl.substring(indexOfAt + 1, remoteUrl.length()); - return "https://" + withoutPrefix.replace(':', '/'); - } - - @NotNull - private static String removeEndingDotGit(@NotNull String url) { - final String DOT_GIT = ".git"; - if (url.endsWith(DOT_GIT)) { - return url.substring(0, url.length() - DOT_GIT.length()); - } - return url; - } - - @Nullable - public static String getBranchNameOnRemote(@NotNull Project project, @NotNull VirtualFile root) { - final GitBranch tracked; - try { - final GitBranch current = GitBranch.current(project, root); - if (current == null) { - Messages.showErrorDialog(project, "Cannot find local branch", CANNOT_OPEN_IN_BROWSER); - return null; - } - tracked = current.tracked(project, root); - if (tracked == null || !tracked.isRemote()) { - Messages.showErrorDialog(project, "Cannot find tracked branch for branch: " + current.getFullName(), CANNOT_OPEN_IN_BROWSER); - return null; - } - } - catch (VcsException e1) { - Messages.showErrorDialog(project, "Error occurred while inspecting branches: " + e1, CANNOT_OPEN_IN_BROWSER); - return null; - } - String branch = tracked.getName(); - if (branch.startsWith("origin/")) { - branch = branch.substring(7); - } - return branch; - } - -} +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitBranch; +import git4idea.GitUtil; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.github.ui.GithubLoginDialog; + +import static org.jetbrains.plugins.github.GithubUtil.*; + +/** + * Created by IntelliJ IDEA. + * + * @author oleg + * @date 12/10/10 + */ +public class GithubOpenInBrowserAction extends DumbAwareAction { + public static final String CANNOT_OPEN_IN_BROWSER = "Cannot open in browser"; + private static final Logger LOG = Logger.getInstance(GithubOpenInBrowserAction.class.getName()); + + protected GithubOpenInBrowserAction() { + super("Open in browser", "Open corresponding GitHub link in browser", GITHUB_ICON); + } + + @Override + public void update(final AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); + if (project == null || project.isDefault() || virtualFile == null) { + setVisibleEnabled(e, false, false); + return; + } + GitRepositoryManager manager = GitUtil.getRepositoryManager(project); + + final GitRepository gitRepository = manager.getRepositoryForFile(virtualFile); + if (gitRepository == null) { + setVisibleEnabled(e, false, false); + return; + } + + // Check that given repository is properly configured git repository + if (!isRepositoryOnGitHub(gitRepository)) { + setVisibleEnabled(e, false, false); + return; + } + + ChangeListManager changeListManager = ChangeListManager.getInstance(project); + if (changeListManager.isUnversioned(virtualFile)) { + setVisibleEnabled(e, true, false); + return; + } + + Change change = changeListManager.getChange(virtualFile); + if (change != null && change.getType() == Change.Type.NEW) { + setVisibleEnabled(e, true, false); + return; + } + + setVisibleEnabled(e, true, true); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void actionPerformed(final AnActionEvent e) { + final Project project = e.getData(PlatformDataKeys.PROJECT); + while (!checkCredentials(project)) { + final GithubLoginDialog dialog = new GithubLoginDialog(project); + dialog.show(); + if (!dialog.isOK()) { + return; + } + } + + final VirtualFile root = project.getBaseDir(); + GitRepositoryManager manager = GitUtil.getRepositoryManager(project); + if (manager == null) { + return; + } + final GitRepository gitRepository = manager.getRepositoryForFile(root); + // Check that given repository is properly configured git repository + final String githubRemoteUrl = findGithubRemoteUrl(gitRepository); + + final VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); + final String rootPath = root.getPath(); + final String path = virtualFile.getPath(); + if (!path.startsWith(rootPath)) { + Messages.showErrorDialog(project, "File is not under project root", CANNOT_OPEN_IN_BROWSER); + return; + } + + String branch = getBranchNameOnRemote(project, root); + if (branch == null) { + return; + } + + String relativePath = path.substring(rootPath.length()); + String urlToOpen = makeUrlToOpen(e, relativePath, branch, githubRemoteUrl); + BrowserUtil.launchBrowser(urlToOpen); + } + + private static String makeUrlToOpen(@NotNull AnActionEvent e, @NotNull String relativePath, @NotNull String branch, + @NotNull String githubRemoteUrl) { + final StringBuilder builder = new StringBuilder(); + builder.append(makeGithubRepoUrlFromRemoteUrl(githubRemoteUrl)).append("/blob/").append(branch).append(relativePath); + final Editor editor = e.getData(PlatformDataKeys.EDITOR); + if (editor != null) { + final int line = editor.getCaretModel().getLogicalPosition().line + 1; // lines are counted internally from 0, but from 1 on github + builder.append("#L").append(line); + } + return builder.toString(); + } + + @Nullable + public static String getBranchNameOnRemote(@NotNull Project project, @NotNull VirtualFile root) { + final GitBranch tracked; + try { + final GitBranch current = GitBranch.current(project, root); + if (current == null) { + Messages.showErrorDialog(project, "Cannot find local branch", CANNOT_OPEN_IN_BROWSER); + return null; + } + tracked = current.tracked(project, root); + if (tracked == null || !tracked.isRemote()) { + Messages.showErrorDialog(project, "Cannot find tracked branch for branch: " + current.getFullName(), CANNOT_OPEN_IN_BROWSER); + return null; + } + } + catch (VcsException e1) { + Messages.showErrorDialog(project, "Error occurred while inspecting branches: " + e1, CANNOT_OPEN_IN_BROWSER); + return null; + } + String branch = tracked.getName(); + if (branch.startsWith("origin/")) { + branch = branch.substring(7); + } + return branch; + } + +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserAction.java index 6187986fb9cc..479033f3edbb 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserAction.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserAction.java @@ -16,104 +16,35 @@ package org.jetbrains.plugins.github; import com.intellij.ide.BrowserUtil; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.PlatformDataKeys; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitUtil; -import git4idea.GitVcs; -import git4idea.history.browser.GitCommit; import git4idea.repo.GitRepository; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * @author Kirill Likhodedov */ -public class GithubShowCommitInBrowserAction extends DumbAwareAction { +abstract class GithubShowCommitInBrowserAction extends DumbAwareAction { public GithubShowCommitInBrowserAction() { super("Open in Browser", "Open the selected commit in browser", GithubUtil.GITHUB_ICON); } - @Override - public void update(AnActionEvent e) { - EventData eventData = collectData(e); - e.getPresentation().setVisible(eventData != null); - e.getPresentation().setEnabled(eventData != null); - } - - @Nullable - private static EventData collectData(AnActionEvent e) { - Project project = e.getData(PlatformDataKeys.PROJECT); - if (project == null || project.isDefault()) { - return null; - } - - GitCommit commit = e.getData(GitVcs.GIT_COMMIT); - if (commit == null) { - return null; - } - - VirtualFile root = commit.getRoot(); - GitRepository repository = GitUtil.getRepositoryManager(project).getRepositoryForRoot(root); - if (repository == null || !GithubUtil.isRepositoryOnGitHub(repository)) { - return null; - } - - return new EventData(project, repository, commit); - } - - @Override - public void actionPerformed(AnActionEvent e) { - EventData eventData = collectData(e); - if (eventData == null) { - return; - } - - GitRepository repository = eventData.getRepository(); + protected static void openInBrowser(Project project, GitRepository repository, String revisionHash) { String url = GithubUtil.findGithubRemoteUrl(repository); if (url == null) { GithubUtil.LOG.info(String.format("Repository is not under GitHub. Root: %s, Remotes: %s", repository.getRoot(), GitUtil.getPrintableRemotes(repository.getRemotes()))); return; } - - String userAndRepository = GithubUtil.getUserAndRepositoryOrShowError(eventData.getProject(), url); + url = GithubUtil.makeGithubRepoUrlFromRemoteUrl(url); + String userAndRepository = GithubUtil.getUserAndRepositoryOrShowError(project, url); if (userAndRepository == null) { return; } - String githubUrl = "https://github.com/" + userAndRepository + "/commit/" + eventData.getCommit(); + String githubUrl = "https://github.com/" + userAndRepository + "/commit/" + revisionHash; BrowserUtil.launchBrowser(githubUrl); } - private static class EventData { - @NotNull private final Project myProject; - @NotNull private final GitRepository myRepository; - @NotNull private final GitCommit myCommit; - - private EventData(@NotNull Project project, @NotNull GitRepository repository, @NotNull GitCommit commit) { - myProject = project; - myRepository = repository; - myCommit = commit; - } - - @NotNull - public Project getProject() { - return myProject; - } - - @NotNull - public GitRepository getRepository() { - return myRepository; - } - - @NotNull - public GitCommit getCommit() { - return myCommit; - } - } - } diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromAnnotateAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromAnnotateAction.java new file mode 100644 index 000000000000..2455847ab35c --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromAnnotateAction.java @@ -0,0 +1,105 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.annotate.FileAnnotation; +import com.intellij.openapi.vcs.annotate.LineNumberListener; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Kirill Likhodedov + */ +public class GithubShowCommitInBrowserFromAnnotateAction extends GithubShowCommitInBrowserAction implements LineNumberListener { + + private final FileAnnotation myAnnotation; + private int myLineNumber = -1; + + public GithubShowCommitInBrowserFromAnnotateAction(FileAnnotation annotation) { + super(); + myAnnotation = annotation; + } + + @Override + public void update(AnActionEvent e) { + EventData eventData = calcData(e); + final boolean enabled = myLineNumber != -1 && myAnnotation.getLineRevisionNumber(myLineNumber) != null; + e.getPresentation().setEnabled(eventData != null && enabled); + e.getPresentation().setVisible(eventData != null && GithubUtil.isRepositoryOnGitHub(eventData.getRepository())); + } + + @Override + public void actionPerformed(AnActionEvent e) { + EventData eventData = calcData(e); + if (eventData == null) { + return; + } + + final VcsRevisionNumber revisionNumber = myAnnotation.getLineRevisionNumber(myLineNumber); + if (revisionNumber != null) { + openInBrowser(eventData.getProject(), eventData.getRepository(), revisionNumber.asString()); + } + } + + @Nullable + private static EventData calcData(AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE); + if (project == null || virtualFile == null) { + return null; + } + GitRepository repository = GitUtil.getRepositoryManager(project).getRepositoryForFile(virtualFile); + if (repository == null) { + return null; + } + + return new EventData(project, repository); + } + + @Override + public void consume(Integer integer) { + myLineNumber = integer; + } + + private static class EventData { + @NotNull private final Project myProject; + @NotNull private final GitRepository myRepository; + + private EventData(@NotNull Project project, @NotNull GitRepository repository) { + myProject = project; + myRepository = repository; + } + + @NotNull + public Project getProject() { + return myProject; + } + + @NotNull + public GitRepository getRepository() { + return myRepository; + } + + } + +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromLogAction.java b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromLogAction.java new file mode 100644 index 000000000000..17545e8bca37 --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubShowCommitInBrowserFromLogAction.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github; + +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import git4idea.GitVcs; +import git4idea.history.browser.GitCommit; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Kirill Likhodedov + */ +public class GithubShowCommitInBrowserFromLogAction extends GithubShowCommitInBrowserAction { + + @Override + public void update(AnActionEvent e) { + EventData eventData = collectData(e); + e.getPresentation().setVisible(eventData != null && GithubUtil.isRepositoryOnGitHub(eventData.getRepository())); + e.getPresentation().setEnabled(eventData != null); + } + + @Nullable + private static EventData collectData(AnActionEvent e) { + Project project = e.getData(PlatformDataKeys.PROJECT); + if (project == null || project.isDefault()) { + return null; + } + + GitCommit commit = e.getData(GitVcs.GIT_COMMIT); + if (commit == null) { + return null; + } + + VirtualFile root = commit.getRoot(); + GitRepository repository = GitUtil.getRepositoryManager(project).getRepositoryForRoot(root); + if (repository == null) { + return null; + } + + return new EventData(project, repository, commit); + } + + @Override + public void actionPerformed(AnActionEvent e) { + EventData eventData = collectData(e); + if (eventData != null) { + openInBrowser(eventData.getProject(), eventData.getRepository(), eventData.getCommit().getHash().getValue()); + } + } + + private static class EventData { + @NotNull private final Project myProject; + @NotNull private final GitRepository myRepository; + @NotNull private final GitCommit myCommit; + + private EventData(@NotNull Project project, @NotNull GitRepository repository, @NotNull GitCommit commit) { + myProject = project; + myRepository = repository; + myCommit = commit; + } + + @NotNull + public Project getProject() { + return myProject; + } + + @NotNull + public GitRepository getRepository() { + return myRepository; + } + + @NotNull + public GitCommit getCommit() { + return myCommit; + } + } + +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java b/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java index 54eb48a85ab9..a0ab2f64d989 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java +++ b/plugins/github/src/org/jetbrains/plugins/github/GithubUtil.java @@ -1,391 +1,423 @@ -/* - * Copyright 2000-2011 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jetbrains.plugins.github; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.IconLoader; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.tasks.github.GithubApiUtil; -import git4idea.config.GitVcsApplicationSettings; -import git4idea.config.GitVersion; -import git4idea.i18n.GitBundle; -import git4idea.repo.GitRemote; -import git4idea.repo.GitRepository; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.plugins.github.ui.GithubLoginDialog; - -import javax.swing.*; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Various utility methods for the GutHub plugin. - * - * @author oleg - * @author Kirill Likhodedov - */ -public class GithubUtil { - - public static final Icon GITHUB_ICON = IconLoader.getIcon("/org/jetbrains/plugins/github/github_icon.png"); - - public static final Logger LOG = Logger.getInstance("github"); - - static final String GITHUB_NOTIFICATION_GROUP = "github"; - - /** - * @deprecated The host may be defined in different formats. Use {@link com.intellij.tasks.github.GithubApiUtil#getApiUrl(String)} instead. - */ - @Deprecated - public static String getHttpsUrl() { - return "https://" + GithubSettings.getInstance().getHost(); - } - - /** - * @deprecated TODO Use background progress - */ - @Deprecated - public static T accessToGithubWithModalProgress(final Project project, final Computable computable) { - final Ref result = new Ref(); - ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { - public void run(@NotNull ProgressIndicator indicator) { - result.set(computable.compute()); - } - }); - return result.get(); - } - - /** - * @deprecated TODO Use background progress - */ - @Deprecated - public static void accessToGithubWithModalProgress(final Project project, final Runnable runnable) { - ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { - public void run(@NotNull ProgressIndicator indicator) { - runnable.run(); - } - }); - } - - private static boolean testConnection(final String url, final String login, final String password) { - GithubUser user = retrieveCurrentUserInfo(url, login, password); - return user != null; - } - - @Nullable - private static GithubUser retrieveCurrentUserInfo(@NotNull String url, @NotNull String login, @NotNull String password) { - try { - JsonElement result = GithubApiUtil.getRequest(url, login, password, "/user"); - return parseUserInfo(result); - } - catch (IOException e) { - LOG.info(e); - return null; - } - } - - @Nullable - private static GithubUser parseUserInfo(@Nullable JsonElement result) { - if (result == null) { - return null; - } - if (!result.isJsonObject()) { - LOG.error(String.format("Unexpected JSON result format: %s", result)); - return null; - } - - JsonObject obj = (JsonObject)result; - if (!obj.has("plan")) { - return null; - } - GithubUser.Plan plan = parsePlan(obj.get("plan")); - return new GithubUser(plan); - } - - @NotNull - private static GithubUser.Plan parsePlan(JsonElement plan) { - if (!plan.isJsonObject()) { - return GithubUser.Plan.FREE; - } - return GithubUser.Plan.fromString(plan.getAsJsonObject().get("name").getAsString()); - } - - @NotNull - private static List getAvailableRepos(@NotNull String url, @NotNull String login, @NotNull String password, - boolean ownOnly) { - final String request = (ownOnly ? "/user/repos" : "/user/watched"); - try { - JsonElement result = GithubApiUtil.getRequest(url, login, password, request); - if (result == null) { - return Collections.emptyList(); - } - return parseRepositoryInfos(result); - } - catch (IOException e) { - LOG.error(e); - return Collections.emptyList(); - } - } - - @NotNull - private static List parseRepositoryInfos(@NotNull JsonElement result) { - if (!result.isJsonArray()) { - LOG.assertTrue(result.isJsonObject(), String.format("Unexpected JSON result format: %s", result)); - return Collections.singletonList(parseSingleRepositoryInfo(result.getAsJsonObject())); - } - - List repositories = new ArrayList(); - for (JsonElement element : result.getAsJsonArray()) { - LOG.assertTrue(element.isJsonObject(), - String.format("This element should be a JsonObject: %s%nTotal JSON response: %n%s", element, result)); - repositories.add(parseSingleRepositoryInfo(element.getAsJsonObject())); - } - return repositories; - } - - @NotNull - private static RepositoryInfo parseSingleRepositoryInfo(@NotNull JsonObject result) { - String name = result.get("name").getAsString(); - String cloneUrl = result.get("clone_url").getAsString(); - String ownerName = result.get("owner").getAsJsonObject().get("login").getAsString(); - String parentName = result.has("parent") ? result.get("parent").getAsJsonObject().get("full_name").getAsString(): null; - boolean fork = result.get("fork").getAsBoolean(); - return new RepositoryInfo(name, cloneUrl, ownerName, parentName, fork); - } - - @Nullable - private static RepositoryInfo getDetailedRepoInfo(@NotNull String url, @NotNull String login, @NotNull String password, - @NotNull String owner, @NotNull String name) { - try { - final String request = "/repos/" + owner + "/" + name; - JsonElement jsonObject = GithubApiUtil.getRequest(url, login, password, request); - if (jsonObject == null) { - LOG.info(String.format("Information about repository is unavailable. Owner: %s, Name: %s", owner, name)); - return null; - } - return parseSingleRepositoryInfo(jsonObject.getAsJsonObject()); - } - catch (IOException e) { - LOG.info(String.format("Exception was thrown when trying to retrieve information about repository. Owner: %s, Name: %s", - owner, name)); - return null; - } - } - - public static boolean isPrivateRepoAllowed(final String url, final String login, final String password) { - GithubUser user = retrieveCurrentUserInfo(url, login, password); - if (user == null) { - return false; - } - return user.getPlan().isPrivateRepoAllowed(); - } - - public static boolean checkCredentials(final Project project) { - final GithubSettings settings = GithubSettings.getInstance(); - return checkCredentials(project, settings.getHost(), settings.getLogin(), settings.getPassword()); - } - - public static boolean checkCredentials(final Project project, final String url, final String login, final String password) { - if (StringUtil.isEmptyOrSpaces(url) || StringUtil.isEmptyOrSpaces(login) || StringUtil.isEmptyOrSpaces(password)){ - return false; - } - return accessToGithubWithModalProgress(project, new Computable() { - @Override - public Boolean compute() { - ProgressManager.getInstance().getProgressIndicator().setText("Trying to login to GitHub"); - return testConnection(url, login, password); - } - }); - } - - /** - * Shows GitHub login settings if credentials are wrong or empty and return the list of all the watched repos by user - * @param project - * @return - */ - @Nullable - public static List getAvailableRepos(final Project project, final boolean ownOnly) { - while (!checkCredentials(project)){ - final GithubLoginDialog dialog = new GithubLoginDialog(project); - dialog.show(); - if (!dialog.isOK()){ - return null; - } - } - // Otherwise our credentials are valid and they are successfully stored in settings - final GithubSettings settings = GithubSettings.getInstance(); - final String validPassword = settings.getPassword(); - return accessToGithubWithModalProgress(project, new Computable>() { - @Override - public List compute() { - ProgressManager.getInstance().getProgressIndicator().setText("Extracting info about available repositories"); - return getAvailableRepos(settings.getHost(), settings.getLogin(), validPassword, ownOnly); - } - }); - } - - /** - * Shows GitHub login settings if credentials are wrong or empty and return the list of all the watched repos by user - * @param project - * @return - */ - @Nullable - public static RepositoryInfo getDetailedRepositoryInfo(final Project project, final String owner, final String name) { - final GithubSettings settings = GithubSettings.getInstance(); - final String password = settings.getPassword(); - final Boolean validCredentials = accessToGithubWithModalProgress(project, new Computable() { - @Override - public Boolean compute() { - ProgressManager.getInstance().getProgressIndicator().setText("Trying to login to GitHub"); - return testConnection(settings.getHost(), settings.getLogin(), password); - } - }); - if (validCredentials == null) { - return null; - } - if (!validCredentials){ - final GithubLoginDialog dialog = new GithubLoginDialog(project); - dialog.show(); - if (!dialog.isOK()) { - return null; - } - } - // Otherwise our credentials are valid and they are successfully stored in settings - final String validPassword = settings.getPassword(); - return accessToGithubWithModalProgress(project, new Computable() { - @Nullable - @Override - public RepositoryInfo compute() { - ProgressManager.getInstance().getProgressIndicator().setText("Extracting detailed info about repository ''" + name + "''"); - return getDetailedRepoInfo(settings.getHost(), settings.getLogin(), validPassword, owner, name); - } - }); - } - - @Nullable - public static GitRemote findGitHubRemoteBranch(@NotNull GitRepository repository) { - // i.e. find origin which points on my github repo - // Check that given repository is properly configured git repository - for (GitRemote gitRemote : repository.getRemotes()) { - if (getGithubUrl(gitRemote) != null){ - return gitRemote; - } - } - return null; - } - - @Nullable - public static String getGithubUrl(final GitRemote gitRemote){ - final GithubSettings githubSettings = GithubSettings.getInstance(); - final String host = githubSettings.getHost(); - final String username = githubSettings.getLogin(); - - // TODO this doesn't work with organizational accounts - final String userRepoMarkerSSHProtocol = host + ":" + username + "/"; - final String userRepoMarkerOtherProtocols = host + "/" + username + "/"; - for (String pushUrl : gitRemote.getUrls()) { - if (pushUrl.contains(userRepoMarkerSSHProtocol) || pushUrl.contains(userRepoMarkerOtherProtocols)) { - return pushUrl; - } - } - return null; - } - - public static boolean testGitExecutable(final Project project) { - final GitVcsApplicationSettings settings = GitVcsApplicationSettings.getInstance(); - final String executable = settings.getPathToGit(); - final GitVersion version; - try { - version = GitVersion.identifyVersion(executable); - } catch (Exception e) { - Messages.showErrorDialog(project, e.getMessage(), GitBundle.getString("find.git.error.title")); - return false; - } - - if (!version.isSupported()) { - Messages.showWarningDialog(project, GitBundle.message("find.git.unsupported.message", version.toString(), GitVersion.MIN), - GitBundle.getString("find.git.success.title")); - return false; - } - return true; - } - - static boolean isRepositoryOnGitHub(@NotNull GitRepository repository) { - return findGithubRemoteUrl(repository) != null; - } - - @Nullable - static String findGithubRemoteUrl(@NotNull GitRepository repository) { - for (GitRemote remote : repository.getRemotes()) { - for (String url : remote.getUrls()) { - if (isGithubUrl(url)) { - return url; - } - } - } - return null; - } - - private static boolean isGithubUrl(@NotNull String url) { - return url.contains("github.com"); - } - - static void setVisibleEnabled(AnActionEvent e, boolean visible, boolean enabled) { - e.getPresentation().setVisible(visible); - e.getPresentation().setEnabled(enabled); - } - - @Nullable - public static String getUserAndRepositoryOrShowError(@NotNull Project project, @NotNull String url) { - int index = -1; - if (url.startsWith(getHttpsUrl())) { - index = url.lastIndexOf('/'); - if (index == -1) { - Messages.showErrorDialog(project, "Cannot extract info about repository name: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); - return null; - } - index = url.substring(0, index).lastIndexOf('/'); - if (index == -1) { - Messages.showErrorDialog(project, "Cannot extract info about repository owner: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); - return null; - } - } - else { - index = url.lastIndexOf(':'); - if (index == -1) { - Messages.showErrorDialog(project, "Cannot extract info about repository name and owner: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); - return null; - } - } - String repoInfo = url.substring(index + 1); - if (repoInfo.endsWith(".git")) { - repoInfo = repoInfo.substring(0, repoInfo.length() - 4); - } - return repoInfo; - } -} +/* + * Copyright 2000-2011 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.tasks.github.GithubApiUtil; +import git4idea.config.GitVcsApplicationSettings; +import git4idea.config.GitVersion; +import git4idea.i18n.GitBundle; +import git4idea.repo.GitRemote; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.github.ui.GithubLoginDialog; + +import javax.swing.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Various utility methods for the GutHub plugin. + * + * @author oleg + * @author Kirill Likhodedov + */ +public class GithubUtil { + + public static final Icon GITHUB_ICON = IconLoader.getIcon("/org/jetbrains/plugins/github/github_icon.png"); + + public static final Logger LOG = Logger.getInstance("github"); + + static final String GITHUB_NOTIFICATION_GROUP = "github"; + + /** + * @deprecated The host may be defined in different formats. Use {@link com.intellij.tasks.github.GithubApiUtil#getApiUrl(String)} instead. + */ + @Deprecated + public static String getHttpsUrl() { + return "https://" + GithubSettings.getInstance().getHost(); + } + + /** + * @deprecated TODO Use background progress + */ + @Deprecated + public static T accessToGithubWithModalProgress(final Project project, final Computable computable) { + final Ref result = new Ref(); + ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { + public void run(@NotNull ProgressIndicator indicator) { + result.set(computable.compute()); + } + }); + return result.get(); + } + + /** + * @deprecated TODO Use background progress + */ + @Deprecated + public static void accessToGithubWithModalProgress(final Project project, final Runnable runnable) { + ProgressManager.getInstance().run(new Task.Modal(project, "Access to GitHub", true) { + public void run(@NotNull ProgressIndicator indicator) { + runnable.run(); + } + }); + } + + private static boolean testConnection(final String url, final String login, final String password) { + GithubUser user = retrieveCurrentUserInfo(url, login, password); + return user != null; + } + + @Nullable + private static GithubUser retrieveCurrentUserInfo(@NotNull String url, @NotNull String login, @NotNull String password) { + try { + JsonElement result = GithubApiUtil.getRequest(url, login, password, "/user"); + return parseUserInfo(result); + } + catch (IOException e) { + LOG.info(e); + return null; + } + } + + @Nullable + private static GithubUser parseUserInfo(@Nullable JsonElement result) { + if (result == null) { + return null; + } + if (!result.isJsonObject()) { + LOG.error(String.format("Unexpected JSON result format: %s", result)); + return null; + } + + JsonObject obj = (JsonObject)result; + if (!obj.has("plan")) { + return null; + } + GithubUser.Plan plan = parsePlan(obj.get("plan")); + return new GithubUser(plan); + } + + @NotNull + private static GithubUser.Plan parsePlan(JsonElement plan) { + if (!plan.isJsonObject()) { + return GithubUser.Plan.FREE; + } + return GithubUser.Plan.fromString(plan.getAsJsonObject().get("name").getAsString()); + } + + @NotNull + private static List getAvailableRepos(@NotNull String url, @NotNull String login, @NotNull String password, + boolean ownOnly) { + final String request = (ownOnly ? "/user/repos" : "/user/watched"); + try { + JsonElement result = GithubApiUtil.getRequest(url, login, password, request); + if (result == null) { + return Collections.emptyList(); + } + return parseRepositoryInfos(result); + } + catch (IOException e) { + LOG.error(e); + return Collections.emptyList(); + } + } + + @NotNull + private static List parseRepositoryInfos(@NotNull JsonElement result) { + if (!result.isJsonArray()) { + LOG.assertTrue(result.isJsonObject(), String.format("Unexpected JSON result format: %s", result)); + return Collections.singletonList(parseSingleRepositoryInfo(result.getAsJsonObject())); + } + + List repositories = new ArrayList(); + for (JsonElement element : result.getAsJsonArray()) { + LOG.assertTrue(element.isJsonObject(), + String.format("This element should be a JsonObject: %s%nTotal JSON response: %n%s", element, result)); + repositories.add(parseSingleRepositoryInfo(element.getAsJsonObject())); + } + return repositories; + } + + @NotNull + private static RepositoryInfo parseSingleRepositoryInfo(@NotNull JsonObject result) { + String name = result.get("name").getAsString(); + String cloneUrl = result.get("clone_url").getAsString(); + String ownerName = result.get("owner").getAsJsonObject().get("login").getAsString(); + String parentName = result.has("parent") ? result.get("parent").getAsJsonObject().get("full_name").getAsString(): null; + boolean fork = result.get("fork").getAsBoolean(); + return new RepositoryInfo(name, cloneUrl, ownerName, parentName, fork); + } + + @Nullable + private static RepositoryInfo getDetailedRepoInfo(@NotNull String url, @NotNull String login, @NotNull String password, + @NotNull String owner, @NotNull String name) { + try { + final String request = "/repos/" + owner + "/" + name; + JsonElement jsonObject = GithubApiUtil.getRequest(url, login, password, request); + if (jsonObject == null) { + LOG.info(String.format("Information about repository is unavailable. Owner: %s, Name: %s", owner, name)); + return null; + } + return parseSingleRepositoryInfo(jsonObject.getAsJsonObject()); + } + catch (IOException e) { + LOG.info(String.format("Exception was thrown when trying to retrieve information about repository. Owner: %s, Name: %s", + owner, name)); + return null; + } + } + + public static boolean isPrivateRepoAllowed(final String url, final String login, final String password) { + GithubUser user = retrieveCurrentUserInfo(url, login, password); + if (user == null) { + return false; + } + return user.getPlan().isPrivateRepoAllowed(); + } + + public static boolean checkCredentials(final Project project) { + final GithubSettings settings = GithubSettings.getInstance(); + return checkCredentials(project, settings.getHost(), settings.getLogin(), settings.getPassword()); + } + + public static boolean checkCredentials(final Project project, final String url, final String login, final String password) { + if (StringUtil.isEmptyOrSpaces(url) || StringUtil.isEmptyOrSpaces(login) || StringUtil.isEmptyOrSpaces(password)){ + return false; + } + return accessToGithubWithModalProgress(project, new Computable() { + @Override + public Boolean compute() { + ProgressManager.getInstance().getProgressIndicator().setText("Trying to login to GitHub"); + return testConnection(url, login, password); + } + }); + } + + /** + * Shows GitHub login settings if credentials are wrong or empty and return the list of all the watched repos by user + * @param project + * @return + */ + @Nullable + public static List getAvailableRepos(final Project project, final boolean ownOnly) { + while (!checkCredentials(project)){ + final GithubLoginDialog dialog = new GithubLoginDialog(project); + dialog.show(); + if (!dialog.isOK()){ + return null; + } + } + // Otherwise our credentials are valid and they are successfully stored in settings + final GithubSettings settings = GithubSettings.getInstance(); + final String validPassword = settings.getPassword(); + return accessToGithubWithModalProgress(project, new Computable>() { + @Override + public List compute() { + ProgressManager.getInstance().getProgressIndicator().setText("Extracting info about available repositories"); + return getAvailableRepos(settings.getHost(), settings.getLogin(), validPassword, ownOnly); + } + }); + } + + /** + * Shows GitHub login settings if credentials are wrong or empty and return the list of all the watched repos by user + * @param project + * @return + */ + @Nullable + public static RepositoryInfo getDetailedRepositoryInfo(final Project project, final String owner, final String name) { + final GithubSettings settings = GithubSettings.getInstance(); + final String password = settings.getPassword(); + final Boolean validCredentials = accessToGithubWithModalProgress(project, new Computable() { + @Override + public Boolean compute() { + ProgressManager.getInstance().getProgressIndicator().setText("Trying to login to GitHub"); + return testConnection(settings.getHost(), settings.getLogin(), password); + } + }); + if (validCredentials == null) { + return null; + } + if (!validCredentials){ + final GithubLoginDialog dialog = new GithubLoginDialog(project); + dialog.show(); + if (!dialog.isOK()) { + return null; + } + } + // Otherwise our credentials are valid and they are successfully stored in settings + final String validPassword = settings.getPassword(); + return accessToGithubWithModalProgress(project, new Computable() { + @Nullable + @Override + public RepositoryInfo compute() { + ProgressManager.getInstance().getProgressIndicator().setText("Extracting detailed info about repository ''" + name + "''"); + return getDetailedRepoInfo(settings.getHost(), settings.getLogin(), validPassword, owner, name); + } + }); + } + + @Nullable + public static GitRemote findGitHubRemoteBranch(@NotNull GitRepository repository) { + // i.e. find origin which points on my github repo + // Check that given repository is properly configured git repository + for (GitRemote gitRemote : repository.getRemotes()) { + if (getGithubUrl(gitRemote) != null){ + return gitRemote; + } + } + return null; + } + + @Nullable + public static String getGithubUrl(final GitRemote gitRemote){ + final GithubSettings githubSettings = GithubSettings.getInstance(); + final String host = githubSettings.getHost(); + final String username = githubSettings.getLogin(); + + // TODO this doesn't work with organizational accounts + final String userRepoMarkerSSHProtocol = host + ":" + username + "/"; + final String userRepoMarkerOtherProtocols = host + "/" + username + "/"; + for (String pushUrl : gitRemote.getUrls()) { + if (pushUrl.contains(userRepoMarkerSSHProtocol) || pushUrl.contains(userRepoMarkerOtherProtocols)) { + return pushUrl; + } + } + return null; + } + + public static boolean testGitExecutable(final Project project) { + final GitVcsApplicationSettings settings = GitVcsApplicationSettings.getInstance(); + final String executable = settings.getPathToGit(); + final GitVersion version; + try { + version = GitVersion.identifyVersion(executable); + } catch (Exception e) { + Messages.showErrorDialog(project, e.getMessage(), GitBundle.getString("find.git.error.title")); + return false; + } + + if (!version.isSupported()) { + Messages.showWarningDialog(project, GitBundle.message("find.git.unsupported.message", version.toString(), GitVersion.MIN), + GitBundle.getString("find.git.success.title")); + return false; + } + return true; + } + + static boolean isRepositoryOnGitHub(@NotNull GitRepository repository) { + return findGithubRemoteUrl(repository) != null; + } + + @Nullable + static String findGithubRemoteUrl(@NotNull GitRepository repository) { + for (GitRemote remote : repository.getRemotes()) { + for (String url : remote.getUrls()) { + if (isGithubUrl(url)) { + return url; + } + } + } + return null; + } + + private static boolean isGithubUrl(@NotNull String url) { + return url.contains("github.com"); + } + + static void setVisibleEnabled(AnActionEvent e, boolean visible, boolean enabled) { + e.getPresentation().setVisible(visible); + e.getPresentation().setEnabled(enabled); + } + + @Nullable + public static String getUserAndRepositoryOrShowError(@NotNull Project project, @NotNull String url) { + int index = -1; + if (url.startsWith(getHttpsUrl())) { + index = url.lastIndexOf('/'); + if (index == -1) { + Messages.showErrorDialog(project, "Cannot extract info about repository name: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); + return null; + } + index = url.substring(0, index).lastIndexOf('/'); + if (index == -1) { + Messages.showErrorDialog(project, "Cannot extract info about repository owner: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); + return null; + } + } + else { + index = url.lastIndexOf(':'); + if (index == -1) { + Messages.showErrorDialog(project, "Cannot extract info about repository name and owner: " + url, GithubOpenInBrowserAction.CANNOT_OPEN_IN_BROWSER); + return null; + } + } + String repoInfo = url.substring(index + 1); + if (repoInfo.endsWith(".git")) { + repoInfo = repoInfo.substring(0, repoInfo.length() - 4); + } + return repoInfo; + } + + @NotNull + static String makeGithubRepoUrlFromRemoteUrl(@NotNull String remoteUrl) { + remoteUrl = removeEndingDotGit(remoteUrl); + if (remoteUrl.startsWith("http")) { + return remoteUrl; + } + if (remoteUrl.startsWith("git://")) { + return "https" + remoteUrl.substring(3); + } + return convertFromSshToHttp(remoteUrl); + } + + @NotNull + private static String convertFromSshToHttp(@NotNull String remoteUrl) { + // Format: git@github.com:account/repository + int indexOfAt = remoteUrl.indexOf("@"); + if (indexOfAt < 0) { + throw new IllegalStateException("Invalid remote Github SSH url: " + remoteUrl); + } + String withoutPrefix = remoteUrl.substring(indexOfAt + 1, remoteUrl.length()); + return "https://" + withoutPrefix.replace(':', '/'); + } + + @NotNull + private static String removeEndingDotGit(@NotNull String url) { + final String DOT_GIT = ".git"; + if (url.endsWith(DOT_GIT)) { + return url.substring(0, url.length() - DOT_GIT.length()); + } + return url; + } +} diff --git a/plugins/github/src/org/jetbrains/plugins/github/ui/GithubAnnotationGutterActionProvider.java b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubAnnotationGutterActionProvider.java new file mode 100644 index 000000000000..7b7a7c1bcb9e --- /dev/null +++ b/plugins/github/src/org/jetbrains/plugins/github/ui/GithubAnnotationGutterActionProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.github.ui; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.vcs.annotate.AnnotationGutterActionProvider; +import com.intellij.openapi.vcs.annotate.FileAnnotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.github.GithubShowCommitInBrowserFromAnnotateAction; + +/** + * @author Kirill Likhodedov + */ +public class GithubAnnotationGutterActionProvider implements AnnotationGutterActionProvider { + + @NotNull + @Override + public AnAction createAction(@NotNull FileAnnotation annotation) { + return new GithubShowCommitInBrowserFromAnnotateAction(annotation); + } + +} From 59ab803d7fe48cbc3848c1f1b7610a7cd7d8e04d Mon Sep 17 00:00:00 2001 From: Vassiliy Kudryashov Date: Fri, 13 Jul 2012 15:54:03 +0400 Subject: [PATCH 09/66] IDEA-88715 Exception on "Restart "Rerun failed tests"" --- .../execution/actions/AbstractRerunFailedTestsAction.java | 3 ++- .../com/intellij/execution/runners/ExecutionEnvironment.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/java/execution/impl/src/com/intellij/execution/actions/AbstractRerunFailedTestsAction.java b/java/execution/impl/src/com/intellij/execution/actions/AbstractRerunFailedTestsAction.java index cc1e787aab22..37da6d4417d1 100644 --- a/java/execution/impl/src/com/intellij/execution/actions/AbstractRerunFailedTestsAction.java +++ b/java/execution/impl/src/com/intellij/execution/actions/AbstractRerunFailedTestsAction.java @@ -104,7 +104,8 @@ public class AbstractRerunFailedTestsAction extends AnAction { profile.getProject(), myEnvironment.getRunnerSettings(), myEnvironment.getConfigurationSettings(), - null)); + null, + myEnvironment.getRunnerAndConfigurationSettings())); } catch (ExecutionException e1) { LOG.error(e1); diff --git a/platform/lang-api/src/com/intellij/execution/runners/ExecutionEnvironment.java b/platform/lang-api/src/com/intellij/execution/runners/ExecutionEnvironment.java index 58385f88e55f..60339169e0fe 100644 --- a/platform/lang-api/src/com/intellij/execution/runners/ExecutionEnvironment.java +++ b/platform/lang-api/src/com/intellij/execution/runners/ExecutionEnvironment.java @@ -40,13 +40,14 @@ public class ExecutionEnvironment extends UserDataHolderBase { @Nullable private RunnerSettings myRunnerSettings; @Nullable private ConfigurationPerRunnerSettings myConfigurationSettings; - @Nullable private RunnerAndConfigurationSettings myRunnerAndConfigurationSettings; + @Nullable private final RunnerAndConfigurationSettings myRunnerAndConfigurationSettings; @Nullable private final RunContentDescriptor myContentToReuse; @TestOnly public ExecutionEnvironment() { myProject = null; myContentToReuse = null; + myRunnerAndConfigurationSettings = null; } public ExecutionEnvironment(@NotNull final ProgramRunner runner, From 7e3260a591108e0bb1e159677455d3c84f2a8751 Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Fri, 13 Jul 2012 15:58:13 +0400 Subject: [PATCH 10/66] IDEA-66333 Quick documentation lookup on mouse hover Test data is updated to the updated functionality --- .../codeInsight/navigation/DocPreviewUtilTest.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy index a55b3eeaa8cf..e9f4e1ebf11d 100644 --- a/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy +++ b/platform/lang-impl/testSources/com/intellij/codeInsight/navigation/DocPreviewUtilTest.groovy @@ -104,10 +104,10 @@ implements java.io.Serializab ''' def expected = '''\ -java.lang
public final class
String extends Object
implements Serializable, Comparable<String>, CharSequence
The String class represents character strings. All string
literals in Java programs, such as "abc", are implemented as
instances of this class.
Strings are constant; their values cannot be changed after
they are created. String buffers support mutable strings.
Because String objects are immutable they can be shared. For
example:
String str = "abc";
is equivalent to:
char data[] = {'a', 'b', 'c'}; ...\ +java.lang
public final class String extends Object
implements Serializable, Comparable<String>, CharSequence\ ''' - def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText, 10) + def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText) assertTrue(actual.endsWith(expected)) // Can't check for equals() because jdk name might differ on different machines. } @@ -123,9 +123,9 @@ Bar ''' def expected = '''\ -Bar
List<java.lang.String> foo (java.lang.String param)\ +Bar
List<String> foo (String param)\ ''' - def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText, 2) + def actual = DocPreviewUtil.buildPreview(header, "java.lang.String", fullText) assertEquals(expected, actual) } } From e02bf8f82fa669b7f8ecd03cef80245b95ebd985 Mon Sep 17 00:00:00 2001 From: Dmitry Avdeev Date: Fri, 13 Jul 2012 17:03:40 +0400 Subject: [PATCH 11/66] fixing AllTests --- .../com/intellij/xml/index/XmlSchemaIndexTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/xml/tests/src/com/intellij/xml/index/XmlSchemaIndexTest.java b/xml/tests/src/com/intellij/xml/index/XmlSchemaIndexTest.java index 75dac9bdce79..5bef5b6be5e2 100644 --- a/xml/tests/src/com/intellij/xml/index/XmlSchemaIndexTest.java +++ b/xml/tests/src/com/intellij/xml/index/XmlSchemaIndexTest.java @@ -9,6 +9,7 @@ import com.intellij.util.containers.ContainerUtil; import java.io.IOException; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -65,18 +66,16 @@ public class XmlSchemaIndexTest extends CodeInsightFixtureTestCase { final Collection files = XmlTagNamesIndex.getFilesByTagName("bean", project); assertEquals(1, files.size()); - final Collection files1 = XmlTagNamesIndex.getFilesByTagName("schema", project); + final Collection files1 = XmlTagNamesIndex.getFilesByTagName("web-app", project); assertEquals(files1.toString(), 2, files1.size()); - Collection names = ContainerUtil.map(files1, new Function() { + List names = new ArrayList(ContainerUtil.map(files1, new Function() { @Override public String fun(VirtualFile virtualFile) { return virtualFile.getName(); } - }); - List expected = Arrays.asList("XMLSchema.xsd", "XMLSchema.xsd"); - names.removeAll(expected); - assertTrue(files1.toString(), names.isEmpty()); + })); + assertEquals(Arrays.asList("web-app_3_0.xsd", "web-app_2_5.xsd"), names); } public void testNamespaceIndex() { From 5fd3191f604250f115a80d3d4bce837e0331d178 Mon Sep 17 00:00:00 2001 From: Oleg Sukhodolsky Date: Fri, 13 Jul 2012 17:04:46 +0400 Subject: [PATCH 12/66] EA-37261: (code cleanup) PsiDocumentManager.getDocument() returns @Nullable value --- platform/core-api/src/com/intellij/psi/PsiDocumentManager.java | 1 + .../src/com/intellij/psi/impl/PsiDocumentManagerImpl.java | 1 + 2 files changed, 2 insertions(+) diff --git a/platform/core-api/src/com/intellij/psi/PsiDocumentManager.java b/platform/core-api/src/com/intellij/psi/PsiDocumentManager.java index f31778d64f97..0973bca9e97d 100644 --- a/platform/core-api/src/com/intellij/psi/PsiDocumentManager.java +++ b/platform/core-api/src/com/intellij/psi/PsiDocumentManager.java @@ -72,6 +72,7 @@ public abstract class PsiDocumentManager { * @param file the file for which the document is requested. * @return the document instance, or null if the file is binary or has no associated document. */ + @Nullable public abstract Document getDocument(@NotNull PsiFile file); /** diff --git a/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java b/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java index d8f47c2a1947..9e3734e9de52 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java +++ b/platform/lang-impl/src/com/intellij/psi/impl/PsiDocumentManagerImpl.java @@ -203,6 +203,7 @@ public class PsiDocumentManagerImpl extends PsiDocumentManager implements Projec return ((PsiManagerEx)myPsiManager).getFileManager().findFile(virtualFile); } + @Nullable @Override public Document getDocument(@NotNull PsiFile file) { if (file instanceof PsiBinaryFile) return null; From 5af924c22f185bab7367564cefe2c060b827bc12 Mon Sep 17 00:00:00 2001 From: Oleg Sukhodolsky Date: Fri, 13 Jul 2012 17:12:04 +0400 Subject: [PATCH 13/66] EA-37261: we should not extract into file with unknown type (otherwise it will be treated as a binary one) --- .../intellij/refactoring/lang/ExtractIncludeDialog.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platform/lang-impl/src/com/intellij/refactoring/lang/ExtractIncludeDialog.java b/platform/lang-impl/src/com/intellij/refactoring/lang/ExtractIncludeDialog.java index c3a0556e5848..7f1524c7b073 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/lang/ExtractIncludeDialog.java +++ b/platform/lang-impl/src/com/intellij/refactoring/lang/ExtractIncludeDialog.java @@ -20,6 +20,8 @@ import com.intellij.ide.util.DirectoryUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.ex.FileTypeChooser; import com.intellij.openapi.help.HelpManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -140,6 +142,11 @@ public class ExtractIncludeDialog extends DialogWrapper { return; } + final FileType type = FileTypeChooser.getKnownFileTypeOrAssociate(targetFileName); + if (type == null) { + return; + } + CommandProcessor.getInstance().executeCommand(project, new Runnable() { public void run() { final Runnable action = new Runnable() { From 3082f9dd70d888d78139c2073a8e7e77b1b3ee47 Mon Sep 17 00:00:00 2001 From: Dmitry Avdeev Date: Fri, 13 Jul 2012 17:23:51 +0400 Subject: [PATCH 14/66] diagnostics for blinking test --- .../intellij/codeInsight/daemon/XmlHighlightingTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/xml/tests/src/com/intellij/codeInsight/daemon/XmlHighlightingTest.java b/xml/tests/src/com/intellij/codeInsight/daemon/XmlHighlightingTest.java index 6fbef29a707e..5eb87b91d94b 100644 --- a/xml/tests/src/com/intellij/codeInsight/daemon/XmlHighlightingTest.java +++ b/xml/tests/src/com/intellij/codeInsight/daemon/XmlHighlightingTest.java @@ -59,7 +59,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.*; -@SuppressWarnings({"HardCodedStringLiteral"}) +@SuppressWarnings({"HardCodedStringLiteral", "ConstantConditions"}) public class XmlHighlightingTest extends DaemonAnalyzerTestCase { private static final String BASE_PATH = "/xml/"; @@ -636,7 +636,10 @@ public class XmlHighlightingTest extends DaemonAnalyzerTestCase { public void testXHtmlValidation2() throws Exception { disableHtmlSupport(); try { - doTest(); + configureByFile(getFullRelativeTestName()); + XmlFile descriptorFile = ((XmlFile)getFile()).getRootTag().getDescriptor().getNSDescriptor().getDescriptorFile(); + assertEquals("xhtml1-transitional.xsd", descriptorFile.getVirtualFile().getName()); + doDoTest(true, true, true); } finally { enableHtmlSupport(); From 3a1451abf385ca75fa95e4c22f7583e11861eeba Mon Sep 17 00:00:00 2001 From: Eugene Zhuravlev Date: Fri, 13 Jul 2012 15:28:14 +0200 Subject: [PATCH 15/66] do not load snappy implementation if its usage is explicitly disabled via system property --- .../src/com/intellij/util/indexing/IOUtils.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/util/indexing/IOUtils.java b/platform/lang-impl/src/com/intellij/util/indexing/IOUtils.java index 783c4a8cb08f..85064910d31d 100644 --- a/platform/lang-impl/src/com/intellij/util/indexing/IOUtils.java +++ b/platform/lang-impl/src/com/intellij/util/indexing/IOUtils.java @@ -31,9 +31,14 @@ public class IOUtils { private static volatile boolean canUseSnappy; static { try { - Field impl = Snappy.class.getDeclaredField("impl"); - impl.setAccessible(true); - canUseSnappy = impl.get(null) != null && System.getProperty("idea.no.snappy") == null; + if (System.getProperty("idea.no.snappy") == null) { // if enabled + Field impl = Snappy.class.getDeclaredField("impl"); + impl.setAccessible(true); + canUseSnappy = impl.get(null) != null; + } + else { + canUseSnappy = false; + } } catch (Throwable e) {} } From 5d97d62ce279b070cf3579b1d38ee067c3dfd481 Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 13 Jul 2012 15:32:11 +0200 Subject: [PATCH 16/66] IDEA-53612: show empty options for all unique descriptions --- .../daemon/impl/LocalInspectionsPass.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/LocalInspectionsPass.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/LocalInspectionsPass.java index 93126fa3acda..7055f9957b4d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/LocalInspectionsPass.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/LocalInspectionsPass.java @@ -47,6 +47,7 @@ import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.Trinity; import com.intellij.openapi.util.text.StringUtil; @@ -490,7 +491,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass } }, myProject.getDisposed(), 200); - private final Set emptyActionRegistered = Collections.synchronizedSet(new THashSet()); + private final Set> emptyActionRegistered = Collections.synchronizedSet(new THashSet>()); private void addDescriptorIncrementally(@NotNull final ProblemDescriptor descriptor, @NotNull final LocalInspectionToolWrapper tool, @@ -569,7 +570,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(myProject).getInspectionProfile(); PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); InjectedLanguageManager ilManager = InjectedLanguageManager.getInstance(myProject); - Set emptyActionRegistered = new THashSet(); + Set> emptyActionRegistered = new THashSet>(); for (Map.Entry> entry : result.entrySet()) { indicator.checkCanceled(); @@ -593,7 +594,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass } private void createHighlightsForDescriptor(List outInfos, - Set emptyActionRegistered, + Set> emptyActionRegistered, InjectedLanguageManager ilManager, PsiFile file, Document documentRange, @@ -633,7 +634,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass private HighlightInfo createHighlightInfo(@NotNull ProblemDescriptor descriptor, @NotNull LocalInspectionToolWrapper tool, @NotNull HighlightInfoType level, - @NotNull Set emptyActionRegistered, + @NotNull Set> emptyActionRegistered, @NotNull PsiElement element) { @NonNls String message = ProblemDescriptionNode.renderDescriptionMessage(descriptor, element); @@ -665,7 +666,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass private static void registerQuickFixes(final LocalInspectionToolWrapper tool, final ProblemDescriptor descriptor, @NotNull HighlightInfo highlightInfo, - final Set emptyActionRegistered) { + final Set> emptyActionRegistered) { final HighlightDisplayKey key = HighlightDisplayKey.find(tool.getShortName()); boolean needEmptyAction = true; final QuickFix[] fixes = descriptor.getFixes(); @@ -685,7 +686,7 @@ public class LocalInspectionsPass extends ProgressableTextEditorHighlightingPass if (((ProblemDescriptorImpl)descriptor).getEnforcedTextAttributes() != null) { needEmptyAction = false; } - if (needEmptyAction && emptyActionRegistered.add(new TextRange(highlightInfo.fixStartOffset, highlightInfo.fixEndOffset))) { + if (needEmptyAction && emptyActionRegistered.add(Pair.create(new TextRange(highlightInfo.fixStartOffset, highlightInfo.fixEndOffset), tool.getShortName()))) { EmptyIntentionAction emptyIntentionAction = new EmptyIntentionAction(tool.getDisplayName()); QuickFixAction.registerQuickFixAction(highlightInfo, emptyIntentionAction, key); } From 7de8a25c0b1b4f4434136dc1001b7718eedefaf5 Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 13 Jul 2012 15:48:22 +0200 Subject: [PATCH 17/66] introduce variable: forbid duplicate replacement in for statement condition (IDEA-88758) --- .../IntroduceVariableBase.java | 53 +++++++++++++------ .../InsideForLoop.after.java | 11 ++++ .../introduceVariable/InsideForLoop.java | 10 ++++ .../refactoring/IntroduceVariableTest.java | 4 ++ 4 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.after.java create mode 100644 java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.java diff --git a/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java index 71c219343e19..067f5856e68f 100644 --- a/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java +++ b/java/java-impl/src/com/intellij/refactoring/introduceVariable/IntroduceVariableBase.java @@ -49,6 +49,8 @@ import com.intellij.psi.impl.source.jsp.jspJava.JspCodeBlock; import com.intellij.psi.impl.source.jsp.jspJava.JspHolderMethod; import com.intellij.psi.impl.source.resolve.DefaultParameterTypeInferencePolicy; import com.intellij.psi.impl.source.tree.java.ReplaceExpressionUtil; +import com.intellij.psi.scope.processor.VariablesProcessor; +import com.intellij.psi.scope.util.PsiScopesUtil; import com.intellij.psi.util.PsiExpressionTrimRenderer; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtil; @@ -522,21 +524,9 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { if (!CommonRefactoringUtil.checkReadOnlyStatus(project, file)) return false; - PsiElement containerParent = tempContainer; - PsiElement lastScope = tempContainer; - while (true) { - if (containerParent instanceof PsiFile) break; - if (containerParent instanceof PsiMethod) break; - containerParent = containerParent.getParent(); - if (containerParent instanceof PsiCodeBlock) { - lastScope = containerParent; - } - } - - final ExpressionOccurrenceManager occurenceManager = new ExpressionOccurrenceManager(expr, lastScope, - NotInSuperCallOccurrenceFilter.INSTANCE); - final PsiExpression[] occurrences = occurenceManager.getOccurrences(); - final PsiElement anchorStatementIfAll = occurenceManager.getAnchorStatementForAll(); + final ExpressionOccurrenceManager occurrenceManager = createOccurrenceManager(expr, tempContainer); + final PsiExpression[] occurrences = occurrenceManager.getOccurrences(); + final PsiElement anchorStatementIfAll = occurrenceManager.getAnchorStatementForAll(); final LinkedHashMap> occurrencesMap = ContainerUtil.newLinkedHashMap(); @@ -550,8 +540,8 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { supportProvider.isInplaceIntroduceAvailable(expr, nameSuggestionContext) && !ApplicationManager.getApplication().isUnitTestMode() && !isInJspHolderMethod(expr); - final boolean inFinalContext = occurenceManager.isInFinalContext(); - final InputValidator validator = new InputValidator(this, project, anchorStatementIfAll, anchorStatement, occurenceManager); + final boolean inFinalContext = occurrenceManager.isInFinalContext(); + final InputValidator validator = new InputValidator(this, project, anchorStatementIfAll, anchorStatement, occurrenceManager); final TypeSelectorManagerImpl typeSelectorManager = new TypeSelectorManagerImpl(project, originalType, expr, occurrences); final boolean[] wasSucceed = new boolean[]{true}; final Pass callback = new Pass() { @@ -613,6 +603,35 @@ public abstract class IntroduceVariableBase extends IntroduceHandlerBase { return wasSucceed[0]; } + private static ExpressionOccurrenceManager createOccurrenceManager(PsiExpression expr, PsiElement tempContainer) { + boolean skipForStatement = true; + final PsiForStatement forStatement = PsiTreeUtil.getParentOfType(expr, PsiForStatement.class); + if (forStatement != null) { + final VariablesProcessor variablesProcessor = new VariablesProcessor(false) { + @Override + protected boolean check(PsiVariable var, ResolveState state) { + return PsiTreeUtil.isAncestor(forStatement.getInitialization(), var, true); + } + }; + PsiScopesUtil.treeWalkUp(variablesProcessor, expr, null); + skipForStatement = variablesProcessor.size() == 0; + } + + PsiElement containerParent = tempContainer; + PsiElement lastScope = tempContainer; + while (true) { + if (containerParent instanceof PsiFile) break; + if (containerParent instanceof PsiMethod) break; + if (!skipForStatement && containerParent instanceof PsiForStatement) break; + containerParent = containerParent.getParent(); + if (containerParent instanceof PsiCodeBlock) { + lastScope = containerParent; + } + } + + return new ExpressionOccurrenceManager(expr, lastScope, NotInSuperCallOccurrenceFilter.INSTANCE); + } + private static boolean isInJspHolderMethod(PsiExpression expr) { final PsiElement parent1 = expr.getParent(); if (parent1 == null) { diff --git a/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.after.java b/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.after.java new file mode 100644 index 000000000000..93ae967b3a49 --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.after.java @@ -0,0 +1,11 @@ +public class C { + + public C(int[] ints) { + for (int i = 0; i < length && ints[i] > 0; i++) { + int temp = ints[i]; + System.out.println(temp); + System.out.println(temp); + System.out.println(temp); + } + } +} diff --git a/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.java b/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.java new file mode 100644 index 000000000000..d5b64a2ebfac --- /dev/null +++ b/java/java-tests/testData/refactoring/introduceVariable/InsideForLoop.java @@ -0,0 +1,10 @@ +public class C { + + public C(int[] ints) { + for (int i = 0; i < length && ints[i] > 0; i++) { + System.out.println(ints[i]); + System.out.println(ints[i]); + System.out.println(ints[i]); + } + } +} diff --git a/java/java-tests/testSrc/com/intellij/refactoring/IntroduceVariableTest.java b/java/java-tests/testSrc/com/intellij/refactoring/IntroduceVariableTest.java index dda7a76bf962..2a9bcee98340 100644 --- a/java/java-tests/testSrc/com/intellij/refactoring/IntroduceVariableTest.java +++ b/java/java-tests/testSrc/com/intellij/refactoring/IntroduceVariableTest.java @@ -163,6 +163,10 @@ public class IntroduceVariableTest extends LightCodeInsightTestCase { doTest(new MockIntroduceVariableHandler("temp", true, false, false, "int")); } + public void testInsideForLoop() throws Exception { + doTest(new MockIntroduceVariableHandler("temp", true, false, false, "int")); + } + public void testDuplicateGenericExpressions() throws Exception { doTest(new MockIntroduceVariableHandler("temp", true, false, false, "Foo2")); } From def5a84bb9d523979e734f0fd5da5c0c27dd9e7c Mon Sep 17 00:00:00 2001 From: Alexander Lobas Date: Fri, 13 Jul 2012 18:36:34 +0400 Subject: [PATCH 18/66] v7.GridLayout --- .../designer/model/views-meta-model.xml | 3107 +++++++++-------- 1 file changed, 1568 insertions(+), 1539 deletions(-) diff --git a/plugins/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml b/plugins/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml index 1c71fbec8a32..8879306301f5 100644 --- a/plugins/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml +++ b/plugins/android-designer/src/com/intellij/android/designer/model/views-meta-model.xml @@ -1,1540 +1,1569 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - - - - - - - ]]> - - - - - - - - - - - - - - - - - - - - -